TradingView Webhook Automation: Alerts, JSON, Bridges to MT5
How TradingView webhooks work end to end: alertcondition versus alert(), strategy order placeholders, building the JSON, a receiver, and getting orders into MetaTrader.
TradingView Webhook Automation: Alerts, JSON, Bridges to MT5
TLDR
A TradingView webhook is an HTTP POST that TradingView sends to a URL you specify whenever one of your alerts fires; the body of the request is the alert message, and if that message is valid JSON it arrives with a JSON content type. TradingView does not place orders for you. Automated trading from a webhook means you write (or rent) a receiver that validates the message, translates it into a broker or exchange order, and answers quickly. This article covers the Pine Script side (alertcondition, alert(), strategy order placeholders, bar-close confirmation), the message design, a small receiver, and the two realistic ways to get the order into MetaTrader 5.
What a TradingView webhook actually is
Every TradingView alert has a condition and a set of notification channels: pop-up, email, mobile push, sound, and, on paid plans, a webhook URL. (Which plans include webhooks changes from time to time; the current tiers are on tradingview.com/pricing.) When the condition is met, TradingView evaluates the alert's message text, substitutes any placeholders, and sends it as the body of an HTTP POST to your URL. There is no retry logic you can rely on, no signature header, and no custom headers; you get a URL on port 80 or 443 and a body. TradingView's Help Center documents the IP addresses alerts are sent from, so you can allow-list them at your firewall, and it documents a short request timeout, so your receiver should acknowledge first and process afterwards.
Three things follow from that design. First, authentication has to live inside the message body, usually as a secret field, because there is nowhere else to put it. Second, idempotency is your problem: if you reload a chart or an alert fires twice for the same bar on two devices' alert definitions, your receiver must recognize a repeat. Third, the alert is bound to the script version that existed when you created it. Edit the Pine Script and the existing alert keeps running the old code until you delete and recreate it; a surprising number of "my webhook stopped matching my chart" reports are exactly this.
The Pine Script side: three ways an alert can fire
Pine Script v6 gives an indicator or strategy three distinct mechanisms, and they behave differently in the alert dialog, in what the message can contain, and in when they fire.
1. alertcondition(): static conditions for indicators
alertcondition(condition, title, message) registers a named condition that appears in the alert dialog under the indicator's name. It only works in indicators, not strategies. The message argument must be a constant string, so you cannot concatenate a computed value into it at runtime, but it can contain placeholders such as {{ticker}}, {{close}}, {{timenow}}, {{interval}} and {{plot_0}} or {{plot("Name")}}, which TradingView substitutes when the alert fires. The frequency (Once per bar, Once per bar close, Only once, Once per minute) is chosen by the user in the dialog, not by the script. For webhook use, "Once per bar close" is the setting that matches a bar-close backtest.
2. alert(): dynamic messages from indicators or strategies
alert(message, freq) is called from inside the script's logic. The message is a series string, so you can build it from live values with str.tostring() and str.format(), and the frequency is set in code: alert.freq_once_per_bar, alert.freq_once_per_bar_close, or alert.freq_all. The user creates a single alert with the condition "Any alert() function call" and every alert() the script executes goes through it. This is the most flexible option and the one most custom bridges are built on, because the script controls both the content and the timing.
3. Strategy order-fill alerts and alert_message
For a strategy, the alert dialog offers "Order fills events" (and, if the script also calls alert(), "Order fills and alert() function calls"). An order-fill alert fires when the broker emulator fills an order, and the message can use strategy placeholders: {{strategy.order.action}}, {{strategy.order.contracts}}, {{strategy.order.price}}, {{strategy.order.id}}, {{strategy.order.comment}}, {{strategy.market_position}}, {{strategy.prev_market_position}}, {{strategy.position_size}}, and {{strategy.order.alert_message}}. That last one is substituted with whatever string you passed as the alert_message argument of strategy.entry(), strategy.exit(), strategy.order() or strategy.close(). The pattern that works: put the whole JSON body in alert_message inside the script, and set the alert's message field to exactly {{strategy.order.alert_message}}. The script then owns the wire format and the user cannot mistype it in the dialog.
| Mechanism | Script type | Message content | Timing controlled by | Best for |
|---|---|---|---|---|
| alertcondition() | Indicator only | Constant string plus {{placeholders}} | User, in the alert dialog | Simple signal indicators sold or shared with others |
| alert() | Indicator or strategy | Any string built at runtime | Script, via freq argument | Custom bridges that need computed sizes, levels or IDs |
| Order fills + alert_message | Strategy only | strategy.* placeholders plus per-order alert_message | Broker emulator fill time | Mirroring a backtested strategy's exact entries and exits |
When exactly does a strategy alert fire?
This is the part that bites people who tested on bar closes. With the default calc_on_every_tick = false, a strategy evaluates its logic once per bar at the bar's close. An order generated at that evaluation is a market order, and the emulator fills market orders at the open of the next bar, which in real time is the first tick after the close. The order-fill alert therefore fires at the start of the next bar, not at the moment your condition became true. If your receiver fills at the broker a second or two later, the price you get is close to that next-bar open, which is what the backtest assumed. That is good. What is not good is turning on calc_on_every_tick, or using process_orders_on_close without understanding it, and then wondering why live fills do not resemble the tester.
For indicator-based alert() calls, the equivalent discipline is alert.freq_once_per_bar_close or wrapping the call in barstate.isconfirmed. Without that, an RSI cross that is true on an intrabar tick and false by the close will still have sent your webhook. The non-repainting indicators article goes into barstate and request.security() in depth; for webhooks the rule is simple: never let a message leave on an unconfirmed bar unless you deliberately trade intrabar and have tested it that way.
An original indicator with both alertcondition and alert()
The script below plots RSI and exposes two static conditions for the dialog, then also sends a dynamic JSON alert on confirmed bars. Note the two different ways the RSI value reaches the message: {{plot_0}} in the static message, str.tostring() in the dynamic one.
//@version=6 indicator("RSI cross webhook demo", overlay = false)len = input.int(14, "RSI length") upper = input.int(70, "Overbought") lower = input.int(30, "Oversold")
r = ta.rsi(close, len) plot(r, "RSI", color.new(color.purple, 0)) // this is plot_0 hline(upper), hline(lower)
exitOversold = ta.crossover(r, lower) exitOverbought = ta.crossunder(r, upper)
// Static conditions. The message is a constant string; placeholders are filled by TradingView. alertcondition(exitOversold, "RSI exits oversold", '{"ticker":"{{ticker}}","event":"rsi_up","rsi":"{{plot_0}}","time":"{{timenow}}"}') alertcondition(exitOverbought, "RSI exits overbought", '{"ticker":"{{ticker}}","event":"rsi_down","rsi":"{{plot_0}}","time":"{{timenow}}"}')
// Dynamic message, sent only when the bar is closed, through an "Any alert() function call" alert. if barstate.isconfirmed and (exitOversold or exitOverbought) evt = exitOversold ? "rsi_up" : "rsi_down" body = '{"ticker":"' + syminfo.ticker + '","event":"' + evt + '","rsi":"' + str.tostring(r, "#.##") + '","tf":"' + timeframe.period + '"}' alert(body, alert.freq_once_per_bar_close)
The duplicated barstate.isconfirmed and alert.freq_once_per_bar_close are belt and braces: the frequency argument alone already restricts the call to the bar close, and the isconfirmed guard makes the intent visible to whoever reads the script next.
An original strategy that owns its webhook payload
Here the JSON is assembled in the script and attached to each order through alert_message. The user's alert message field contains only {{strategy.order.alert_message}}. Because the strategy placeholders are substituted after the fill, the quantity and price in the payload are the emulator's, which is what you want for reconciliation.
//@version=6 strategy("EMA cross webhook demo", overlay = true, calc_on_every_tick = false, default_qty_type = strategy.fixed, default_qty_value = 1)fastLen = input.int(20, "Fast EMA") slowLen = input.int(50, "Slow EMA") secret = input.string("change-me", "Webhook passphrase")
fast = ta.ema(close, fastLen) slow = ta.ema(close, slowLen) plot(fast, "Fast EMA", color.teal) plot(slow, "Slow EMA", color.orange)
// Returns the JSON body for an order. {{...}} placeholders are expanded by TradingView at send time. payload(action) => '{"secret":"' + secret + '","ticker":"{{ticker}}","action":"' + action + '","qty":"{{strategy.order.contracts}}","price":"{{strategy.order.price}}",' + '"position":"{{strategy.market_position}}","id":"{{strategy.order.id}}","time":"{{timenow}}"}'
if ta.crossover(fast, slow) strategy.entry("L", strategy.long, alert_message = payload("buy")) if ta.crossunder(fast, slow) strategy.entry("S", strategy.short, alert_message = payload("sell"))
Two design notes. The passphrase is an input rather than a literal so that the published script does not contain it; anyone with the script source would otherwise have your secret. And "action" is a word your receiver defines, not strategy.order.action, so the receiver is not coupled to TradingView's vocabulary; if you later add a "flatten" or "reduce" action the Pine side changes, the receiver contract does not.
Designing the message
Keep the body small, flat and self-describing. A receiver should be able to answer four questions from it without any chart context: who sent this (secret), what instrument (ticker, and ideally exchange), what to do (action and quantity or risk), and which event this is (a key that identifies the bar and the signal so repeats can be dropped). Put numbers in as strings when they come from placeholders; TradingView substitutes text, and an unquoted {{close}} on a symbol with unusual formatting can produce invalid JSON. Parse on the receiving side with proper number conversion.
Avoid sending derived instructions you could compute server side. "Buy 0.37 lots" baked into the alert means the Pine Script had to know your account equity. Sending "buy, risk 1 percent, stop 30 pips" lets the receiver, which can query the account, do the arithmetic (the lot-size calculation is shown step by step in what is an expert advisor). Symbol names are the other classic failure: TradingView's {{ticker}} for a forex pair is EURUSD, your broker's symbol may be EURUSD.m or EURUSD.r. A mapping table on the receiver, not in the script, is the clean answer.
A minimal receiver that acknowledges fast
The receiver has one job inside the request: validate, deduplicate, enqueue, respond. Order placement happens in a worker so the response goes back within TradingView's timeout regardless of how slow the broker API is. This Python example uses Flask and the standard library; the same shape works in Node, Go or anything else.
from flask import Flask, request, abort import hmac, json, queue, threadingapp = Flask(name) SECRET = "change-me" # load from an environment variable in real deployments work = queue.Queue() seen = set() # replace with a store that survives restarts
@app.post("/tv") def tv(): data = request.get_json(force=True, silent=True) # TradingView sends JSON content type when the body is valid JSON if not isinstance(data, dict): abort(400) if not hmac.compare_digest(str(data.get("secret", "")), SECRET): abort(403) key = "|".join(str(data.get(k, "")) for k in ("ticker", "time", "action", "id")) if key in seen: return "duplicate", 200 seen.add(key) work.put(data) return "ok", 200 # answer first; the broker call happens in the worker
def worker(): while True: job = work.get() # translate and place the order with your broker or exchange SDK here print("order", json.dumps(job)) work.task_done()
threading.Thread(target=worker, daemon=True).start()
if name == "main": app.run(host="0.0.0.0", port=80)
What a production version adds: TLS termination (run it behind a reverse proxy with a certificate), an allow-list of TradingView's published sender addresses, persistent deduplication keyed on the alert's time and ID, structured logging of every message whether or not it was accepted, a heartbeat so you notice when alerts stop arriving, and a per-symbol lock so two alerts for the same instrument cannot race each other to the broker.
Getting the order into MetaTrader 5
MetaTrader cannot receive an inbound HTTP request. An EA can only make outbound calls with WebRequest(), and only to URLs listed under Tools, Options, Expert Advisors, Allow WebRequest. That leaves two workable patterns.
Pattern A: the EA polls your receiver
The receiver stores accepted messages in a queue. An EA with a timer asks for the next message every second or two, executes it, and acknowledges it. Nothing but the terminal and your server are involved, which is why this is the most common design for a self-hosted webhook trading bridge. The cost is latency equal to the poll interval plus processing, which for bar-close strategies on anything above the one-minute chart is irrelevant.
#include <Trade\Trade.mqh> input string InpUrl = "https://bridge.example.com/next"; // must be in Allow WebRequest list input int InpPollS = 2; // seconds between polls CTrade trade;int OnInit() { EventSetTimer(InpPollS); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); }
void OnTimer() { char post[], result[]; string resHeaders; int code = WebRequest("GET", InpUrl, "", 5000, post, result, resHeaders); if(code != 200) { if(code == -1) Print("WebRequest error ", GetLastError()); return; }
string body = CharArrayToString(result); if(StringLen(body) == 0) return; // queue empty
// Receiver hands the EA a boring line: action,symbol,lots e.g. buy,EURUSD,0.10 string f[]; if(StringSplit(body, ',', f) != 3) { Print("bad line: ", body); return; } double lots = StringToDouble(f[2]); bool ok = false; if(f[0] == "buy") ok = trade.Buy(lots, f[1]); if(f[0] == "sell") ok = trade.Sell(lots, f[1]); if(f[0] == "flat" && PositionSelect(f[1])) ok = trade.PositionClose(f[1]); if(!ok) Print("trade failed: ", trade.ResultRetcode(), " ", trade.ResultRetcodeDescription()); }
The receiver, not the EA, converts the JSON into that comma line, maps the ticker to the broker symbol, and sizes the position. WebRequest() is synchronous, cannot be called from indicators, and does not work inside the Strategy Tester, so test the EA's trade logic separately with a stub.
Pattern B: a local agent writes to the terminal
A small program on the same Windows machine as the terminal receives from your server (or directly from TradingView if the machine is reachable) and drops files into the terminal's MQL5\Files folder, or talks to the EA over a named pipe. The EA reads them in OnTimer(). This avoids WebRequest entirely and keeps everything on one box, at the cost of running one more process. Commercial relays such as PineConnector use a variation of this with their own EA; the trade-offs between renting and owning that layer are the subject of PineConnector alternatives.
Delivery options compared
| Route | Moving parts you run | Works with MT4/MT5 | Latency driver | Where it fails |
|---|---|---|---|---|
| Webhook to your own receiver, EA polls | Receiver, EA | Yes | Poll interval | Your server uptime; WebRequest allow-list forgotten after reinstall |
| Webhook to your own receiver, direct broker or exchange API | Receiver | No (no MT needed) | Broker API round trip | API key scope and rate limits; symbol mapping |
| Webhook to a third-party relay with its EA | None, you rent | Yes | Their queue plus their EA's poll | Vendor outage sits in your execution path; limited custom logic |
| Pine Script converted to MQL5 | EA only | Yes | None; signal computed in terminal | Needs a faithful port and re-test; no TradingView data feed |
If the strategy is stable and the account is on MetaTrader, the last row is often the quieter life: no alerts to expire, no server to patch, and the Strategy Tester available for every change. That is the Pine Script to MQL5 conversion path. If the edge depends on TradingView-only data or you iterate on the Pine Script weekly, the webhook route keeps the brain where you develop it.
Worked example: a bar-close signal, end to end
Take the EMA strategy above on a 1 hour EURUSD chart, Pattern A with a 2 second poll. The timeline of one signal, with the assumptions stated:
- 10:00:00 The 09:00 bar closes. The strategy evaluates on its last update, finds the crossover, and queues a market order.
- 10:00:00 plus the first tick The emulator fills the order at the 10:00 bar's open. The order-fill alert fires with the payload, including {{strategy.order.price}} equal to that open.
- Within about a second TradingView's POST reaches your receiver, which checks the secret, builds the key EURUSD|time|buy|L, finds it new, enqueues, and returns 200.
- Up to 2 seconds later The EA's timer fires, fetches "buy,EURUSD.m,0.33", and calls trade.Buy(). The 0.33 came from the receiver applying the 1 percent risk rule to the account it queried.
- Broker fill at the current ask, which may differ from the emulator's open by the spread plus whatever moved in those seconds.
So the worst-case delay introduced by this design is the poll interval plus network and broker time, and the systematic difference from the backtest is the spread (TradingView's emulator fills at the chart price unless you configure slippage and commission in the strategy properties). For a 1 hour strategy this is a rounding error; for a 1 minute scalper it is the strategy. Choose the architecture after you know which one you are building, not before.
Failure modes checklist
- Alert expired or stopped. Alerts can have an expiration and are stopped when the chart's script errors. Monitor for silence, not just for errors.
- Alert bound to an old script version. After every Pine edit, delete and recreate alerts that should use the new logic.
- Timeout. Doing the broker call inside the request handler makes TradingView mark the delivery failed even when the order went through. Acknowledge, then execute.
- Duplicates. Two alerts on the same strategy (one per device, or an old one you forgot) produce two POSTs. Key your deduplication on something the bar defines, not on arrival time.
- Symbol mismatch. Map {{ticker}} and {{exchange}} to the broker's symbol on the receiver; log and refuse unknown symbols instead of guessing.
- Repainting source. A webhook built on an indicator that uses request.security() without a confirmed offset will send messages that the historical chart later denies ever happened.
- Weekend and holiday opens. A strategy that fires on Sunday's first bar meets a wide spread and a thin book. Add a session filter on the receiver as well as in the script.
FAQ
Does TradingView automated trading work without a webhook?
TradingView's integrated broker panel lets you place orders by hand from the chart on supported brokers, and it does not execute alerts automatically. For unattended execution you need a webhook plus a receiver, a relay service, or a conversion of the logic into a platform-native bot such as an expert advisor.
Can I use alertcondition() in a strategy?
No. alertcondition() is for indicators. Strategies use order-fill alerts with alert_message, alert() calls, or both. If you need a strategy's backtest and an indicator's dialog conditions, the usual approach is two scripts sharing a library.
Why does {{strategy.order.alert_message}} arrive empty?
Because the order that filled did not pass an alert_message argument. strategy.exit() and strategy.close() need their own alert_message; the one from strategy.entry() does not carry over to the exit. Check every order call.
Is JSON required in the alert message?
No. TradingView sends whatever text you put there; if it parses as JSON the request carries a JSON content type, otherwise plain text. JSON is simply easier to validate and extend on the receiver.
How do I secure a webhook with no signature header?
Put a long random secret in the body, compare it with a constant-time comparison, serve the endpoint over HTTPS on an unguessable path, allow-list TradingView's published sender addresses, and reject anything that does not parse into the exact shape you expect. Treat the receiver as internet-facing, because it is.
Can the webhook send my MT5 account's position size back to TradingView?
No. The channel is one way, from TradingView to you. If the Pine Script needs to know account state, the strategy has to model it (strategy.position_size is the emulator's view, not the broker's), or you move the decision to the receiver.
Where Viprasol fits
Viprasol builds the parts of this chain that are not clicking in a dialog: Pine Script indicators and strategies with correct alert design through the TradingView Pine Script development service, self-hosted receivers and MetaTrader bridge EAs through webhook trading bridge development, and full conversions when the quieter path makes more sense. If you already have a strategy and want to know which route fits your account and timeframe, describe the setup and we will tell you which one we would build and why; pricing tiers are on the pricing page.
Automating alerts does not reduce market risk; leveraged trading can lose more than your deposit, and a bug in any link of this chain can place trades you did not intend. Test on a demo account first. This article is educational, not investment advice.
External Resources
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
Ready to Automate Your Trading?
Discuss a custom Expert Advisor with defined strategy rules, risk controls and a testing plan.
Free consultation • No commitment • Response within 24 hours
Need a custom EA or trading bot built?
We build MT4/MT5 Expert Advisors around your strategy, broker and risk requirements. Each project has an agreed scope, testing plan and individual quote.