Edgoro
Platforms

How to send TradingView alerts to a webhook: JSON messages, placeholders and a Python receiver

TradingView webhook alerts step by step: the requirements, a JSON message with the right placeholders, Pine alert() code and a tested Python receiver.

A webhook turns a TradingView alert into an HTTP request to a server you choose: your own bot, a Telegram relay or a service that places orders with a broker. Setting it up takes five minutes. Getting it to work reliably takes knowing a few rules that the alert dialog doesn't show: which placeholders need quotes, when TradingView sends JSON and when it sends plain text, which ports it accepts and how long it waits for an answer.

This guide goes from the alert dialog to a receiver that runs. The TradingView facts come from its help pages, the Python receiver was run and tested, and the Pine Script passes Edgoro Pine Check.

What you need

  • A TradingView plan with webhooks (a paid plan) and two-factor authentication turned on: TradingView only allows webhook alerts on accounts with 2FA.
  • A public address on port 80 or 443. TradingView rejects other ports, so https://bot.example.com/tradingview works and http://203.0.113.5:5000/tradingview doesn't. IPv6 isn't supported.
  • A server that answers within three seconds. TradingView cancels the request when the remote server takes longer.
  • Optional: an IP allowlist. TradingView sends webhooks from 52.89.214.238, 34.212.75.30, 54.218.53.128 and 52.32.178.7.

Step 1: create the alert

Open the alert dialog on the chart and pick the condition: an indicator and one of its alert conditions, "Any alert() function call" for a script that uses alert(), or a strategy for alerts on its order fills. For signals calculated on closed bars, set the frequency to Once per bar close; otherwise the alert can fire on a signal that disappears before the bar closes.

Write the message (Step 2), then in the alert's notification settings tick Webhook URL and paste your address. An alert keeps a copy of the script and its inputs from the moment you create it, so after any change to the code or the settings, delete the alert and create it again.

Step 2: write the message as JSON

When the alert fires, TradingView sends an HTTP POST to your URL with the message as the body. If the message is valid JSON, the request carries Content-Type: application/json; anything else goes as text/plain. Most receivers expect JSON, so this is the step that breaks most setups.

{
  "secret": "replace-with-a-long-random-string",
  "ticker": "{{ticker}}",
  "action": "buy",
  "price": {{close}},
  "time": "{{timenow}}"
}

Placeholders such as {{ticker}} are replaced before sending, so the rule is about the value they produce:

PlaceholderProducesWrite it as
{{ticker}}, {{exchange}}, {{interval}}Text: BTCUSDT, BINANCE, 60"{{ticker}}" with quotes
{{close}}, {{open}}, {{high}}, {{low}}, {{volume}}A number{{close}} (quotes optional)
{{time}}, {{timenow}}A UTC date: 2026-11-11T14:03:27Z"{{timenow}}"
{{plot_0}} to {{plot_19}}, {{plot("Name")}}An indicator plot's value{{plot_0}}
{{strategy.order.action}}, {{strategy.market_position}}Text (strategy alerts only)"{{strategy.order.action}}"
{{strategy.order.contracts}}, {{strategy.order.price}}A number (strategy alerts only){{strategy.order.contracts}}

The message above reaches your server as {"secret": "...", "ticker": "BTCUSDT", "action": "buy", "price": 89250.5, "time": "2026-11-11T14:03:27Z"}. Remove the quotes around {{ticker}} and it arrives as "ticker": BTCUSDT, which isn't JSON: TradingView sends it as text and a JSON parser rejects it. Curly quotes from a notes app, single quotes and a comma after the last field break it the same way.

The free TradingView webhook alert builder renders a message the way TradingView would, shows the content type it will get and flags each of these mistakes before you save the alert.

Step 3 (optional): build the message in Pine Script

There are two ways for your own script to raise an alert, and they handle values differently:

  • alertcondition() takes a constant message, known when the script compiles. Values that change bar to bar can only come from placeholders, which TradingView fills in.
  • alert() takes a message built while the script runs. You assemble it from Pine values (syminfo.ticker, str.tostring(close)) and one alert set to "Any alert() function call" covers every signal.

Here is an indicator that sends the JSON of Step 2 with alert(), plus the RSI value:

//@version=6
indicator("RSI webhook alerts (example)", overlay = false)

len    = input.int(14, "RSI length", minval = 2)
lower  = input.float(30, "Oversold")
upper  = input.float(70, "Overbought")
secret = input.string("replace-with-a-long-random-string", "Webhook secret")

r       = ta.rsi(close, len)
crossUp = ta.crossover(r, lower)
crossDn = ta.crossunder(r, upper)

// A number for JSON: na has no JSON form, so it becomes null.
jsonNum(float x) =>
    na(x) ? "null" : str.tostring(x)

if crossUp or crossDn
    side = crossUp ? "buy" : "sell"
    msg  = '{"secret": "' + secret + '", "ticker": "' + syminfo.ticker + '", "action": "' + side + '", "price": ' + jsonNum(close) + ', "rsi": ' + jsonNum(math.round(r, 2)) + ', "time": "' + str.tostring(time_close) + '"}'
    alert(msg, alert.freq_once_per_bar_close)

plot(r, "RSI", color = color.purple)
hline(lower, "Oversold")
hline(upper, "Overbought")

Pine accepts single-quoted strings, so the double quotes of the JSON need no escaping. time_close is a Unix time in milliseconds, which the receiver below only uses to recognize a repeated alert. The RSI rule is a placeholder for your own signal, not a strategy.

Strategies. On a strategy, an alert can fire on every order fill, and the dialog message can use {{strategy.order.action}}, {{strategy.order.contracts}} and the other strategy placeholders. To write the message in the script instead, pass alert_message to strategy.entry(), strategy.exit() or strategy.close(), and put {{strategy.order.alert_message}} alone in the Message box. TradingView's documentation says placeholders can't be used inside the alert_message argument, so build that text from Pine values too. More on both functions in Pine Script alerts: alertcondition() vs alert().

Step 4: a Python receiver that survives real alerts

A receiver has four jobs: read the body whatever its content type, reject strangers, ignore a repeated alert, and answer fast. This one does all four with Flask:

import hmac
import json
import os
import queue
import threading
import time

from flask import Flask, jsonify, request

SECRET = os.environ["ALERT_SECRET"]  # the same long random string as in the alert message
app = Flask(__name__)
jobs: "queue.Queue[dict]" = queue.Queue()
seen: dict[str, float] = {}  # alert key -> when it was first received
seen_lock = threading.Lock()


def handle(alert: dict) -> None:
    # The slow part goes here: log the alert, size the order, call your broker or exchange (demo account first).
    print("alert", alert["ticker"], alert["action"], alert["price"], flush=True)


def worker() -> None:
    while True:
        alert = jobs.get()
        try:
            handle(alert)
        except Exception as exc:  # one bad alert must not stop the worker
            print("error", exc, flush=True)


@app.post("/tradingview")
def tradingview():
    # TradingView only sets Content-Type: application/json when the message is valid JSON, so parse the raw body.
    try:
        alert = json.loads(request.get_data(as_text=True))
    except ValueError:
        return jsonify(error="body is not JSON"), 400
    if not isinstance(alert, dict) or not hmac.compare_digest(str(alert.get("secret", "")), SECRET):
        return jsonify(error="forbidden"), 403
    missing = [k for k in ("ticker", "action", "price", "time") if k not in alert]
    if missing:
        return jsonify(error=f"missing fields: {missing}"), 422
    if alert["action"] not in ("buy", "sell"):
        return jsonify(error="action must be buy or sell"), 422

    # The same alert twice (same symbol, side and firing time) is handled once.
    key = f'{alert["ticker"]}|{alert["action"]}|{alert["time"]}'
    now = time.time()
    with seen_lock:
        for k in [k for k, t in seen.items() if now - t > 3600]:
            del seen[k]
        if key in seen:
            return jsonify(status="duplicate ignored"), 200
        seen[key] = now

    del alert["secret"]
    jobs.put(alert)
    return jsonify(status="queued"), 200  # answer at once: TradingView cancels requests after 3 seconds


threading.Thread(target=worker, daemon=True).start()

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000)  # serve it to the internet over HTTPS on port 443

What each part is for:

  • request.get_data() instead of request.json. Flask's request.json only parses bodies sent as application/json. Reading the raw text accepts a valid JSON message whatever header it came with, and returns a clear 400 when it isn't JSON.
  • hmac.compare_digest compares the secret in constant time. The secret is a random string used only here, never a password or an exchange key; TradingView's help page advises against putting credentials in the webhook body.
  • The seen table drops an alert that arrives twice with the same symbol, side and time.
  • The queue and the worker thread let the route answer immediately. Placing an order through an exchange API can take longer than TradingView's three seconds; done inside the request, a slow call would get the request cancelled.

We ran it with Flask 3.1.3 on Python 3.14, against Flask's test client, with seven requests:

RequestAnswer
Valid alert, application/json200 queued
The same alert again200 duplicate ignored
Valid JSON sent as text/plain, new time200 queued
Wrong secret403 forbidden
"ticker": BTCUSDT without quotes400 body is not JSON
"action": "hold"422 action must be buy or sell
No price field422 missing fields

The worker printed the two queued alerts and nothing else.

Putting it online. Install with pip install flask and start it with ALERT_SECRET=your-random-string python receiver.py. It listens on port 5000 of the machine itself; put a reverse proxy with an HTTPS certificate in front of it, so TradingView reaches it on port 443. Keep a single process: the seen table and the queue live in memory. With several processes, move them to a database or Redis.

Step 5: test it end to end

First, without TradingView, from any terminal:

curl -X POST https://bot.example.com/tradingview -H "Content-Type: text/plain" -d '{"secret": "your-random-string", "ticker": "BTCUSDT", "action": "buy", "price": 89250.5, "time": "test-1"}'

You should get {"status": "queued"}. Then create a real alert on a 1-minute chart with a condition that fires soon, and watch the Webhook status column of the alert log in TradingView alongside your server's log. Only then point the worker at a demo account.

When it doesn't work

SymptomLikely causeFix
The alert fires, nothing reaches the serverURL on a port other than 80 or 443, an IPv6-only server, 2FA offUse HTTPS on 443 with an IPv4 address; turn on 2FA
The server gets text it can't parseThe filled-in message isn't valid JSONCheck the message in the alert builder
Requests are cancelledThe server takes more than 3 seconds to answerAnswer first, then do the work in the background
Strategy placeholders aren't filledThe alert was created on an indicator, not on a strategy's order fillsCreate the alert on the strategy
Old values after editing the scriptThe alert kept its snapshot of the scriptDelete and recreate the alert
The same signal handled twiceDuplicate delivery or two alerts on one conditionKeep the duplicate check; review your alert list

Sending alerts to a broker without a server

If you'd rather not run a receiver, services receive the webhook and place the order with a broker account for you. Two that publish their message formats, described from their own documentation:

  • TradersPost describes itself as a no-code automation platform: you send JSON to your strategy's webhook URL and it routes the order to a connected broker or exchange. Its docs name TradeStation, Tradier, Robinhood and Alpaca among the connections, and support live, paper and prop firm accounts. A message needs only ticker and action; fields such as quantity, orderType, takeProfit and stopLoss are optional.
  • PickMyTrade routes TradingView alerts to broker accounts; its docs list Tradovate, Interactive Brokers, ProjectX, TopStepX, TradeLocker and Rithmic. Its dashboard generates both the webhook URL and the JSON message with your token, and the docs advise against editing that message by hand.

Check the current documentation of any such service before connecting a live account, and start on paper or demo there too. If you only want alerts on your phone, Edgoro's Copilot gives you a personal webhook link that relays each alert to Telegram and logs it in your journal; it never places trades.

Security checklist

  • A long random secret in the message, checked on the server. No passwords, no exchange keys in alerts.
  • HTTPS only.
  • Validate everything: the symbol is one you trade, the size is within your limits, the action is one you expect.
  • Exchange API keys with trading rights only, never withdrawals.
  • A kill switch: a file or a setting the worker checks before every order.

Sources

Build it with Edgoro

Describe the signal ("alert once per bar close when RSI(14) crosses back above 30, with a JSON message containing the symbol, price and RSI") and the Pine Script generator writes the indicator with the alert() calls in Pine Script v6. For the other end, the crypto bot builder writes a Python bot for Binance, Bybit or OKX.

This guide is for education. Alerts can be delayed or missed, and webhooks can fail. Automated trading can lose money quickly, including through software errors: test on a demo or paper account first.

Frequently asked questions

How do I send TradingView alerts to a webhook?

Create the alert, write the message (JSON if your receiver expects it), tick Webhook URL in the alert's notification settings and paste your address. TradingView then sends an HTTP POST with the message as the body each time the alert fires. Webhooks need a paid plan and two-factor authentication.

Why does my TradingView webhook arrive as text/plain?

TradingView only sends application/json when the filled-in message is valid JSON. A text placeholder without quotes, curly or single quotes, or a trailing comma turns it into plain text. Check the message with a JSON validator or the alert builder.

Which port does a TradingView webhook URL need?

Port 80 or 443 only. A URL with another port, such as :5000, is rejected. Run your app behind a reverse proxy that serves HTTPS on 443.

How fast must a webhook receiver answer?

Within three seconds, or TradingView cancels the request. Answer immediately and do slow work, such as calling an exchange, in a background worker.

Can I use placeholders like {{close}} inside Pine Script alert()?

TradingView documents placeholders for the alert dialog and alertcondition() messages; alert() takes a message built while the script runs, so use Pine values such as str.tostring(close) and syminfo.ticker. Inside a strategy's alert_message argument, placeholders can't be used.

Want our studies and guides first when you search? Add Edgoro as a preferred source on Google.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro