Skip to content
GetProfitable
Search

Messages, placeholders and webhook JSON

Lesson 14 · about 11 min

An alert message is the only thing that leaves TradingView. A phone notification just needs to be readable; a webhook that drives a bot or a journal needs to be machine-readable and exactly right. This lesson covers what you can put in a message, how to build JSON without breaking it, and what actually happens when TradingView posts to your URL.

Placeholders

In alertcondition() messages and in the alert dialog's message box, TradingView substitutes {{name}} tokens at fire time:

Placeholder Value
{{ticker}}, {{exchange}} symbol and exchange
{{open}} {{high}} {{low}} {{close}} {{volume}} values of the bar that fired
{{time}} bar open time, ISO format
{{timenow}} time the alert fired
{{interval}} chart timeframe
{{plot_0}} ... {{plot_19}} value of the n-th plot
{{plot("Title")}} value of the plot with that title

Strategy order-fill alerts add {{strategy.order.action}} (buy or sell), {{strategy.order.contracts}}, {{strategy.order.price}}, {{strategy.order.id}}, {{strategy.order.comment}}, {{strategy.order.alert_message}}, {{strategy.position_size}} and {{strategy.market_position}} (long, short or flat).

{{plot("Title")}} is why plot titles matter. A plotted stop level can be included in an alertcondition() message even though the message string is fixed at compile time:

//@version=6
indicator("Placeholder demo", overlay=true)
ema = ta.ema(close, 21)
stop = close - 2 * ta.atr(14)
plot(ema, "EMA 21", color=color.orange)
plot(stop, "Stop", color=color.red, display=display.none)
alertcondition(ta.crossover(close, ema), "Long setup",
     "{{ticker}} {{interval}}: long at {{close}}, stop {{plot(\"Stop\")}}")

display=display.none keeps the stop out of the chart while still making it available to the placeholder.

Building JSON with alert()

A webhook receiver almost always wants JSON. With alert() you build the string at runtime. Pine supports single-quoted strings, which lets you write double quotes inside without escaping:

//@version=6
indicator("Webhook JSON", overlay=true)
ema = ta.ema(close, 21)
atr = ta.atr(14)
crossUp = ta.crossover(close, ema)
plot(ema, "EMA 21", color=color.orange)
if crossUp
    json = '{"symbol":"' + syminfo.ticker + '","exchange":"' + syminfo.prefix +
         '","side":"buy","price":' + str.tostring(close, format.mintick) +
         ',"stop":' + str.tostring(close - 2 * atr, format.mintick) +
         ',"tf":"' + timeframe.period + '","time":' + str.tostring(time) + '}'
    alert(json, alert.freq_once_per_bar_close)

This produces something like {"symbol":"ES1!","exchange":"CME_MINI","side":"buy","price":5432.25,"stop":5412.25,"tf":"5","time":1718632800000}. Numbers are unquoted, strings quoted. Test the output by pasting a fired message into any JSON validator before pointing a bot at it; a missing comma is silent in Pine and fatal downstream.

str.format() is the alternative for templated text, but its {0} placeholders collide with JSON braces and require quoting rules that are easy to get wrong, so concatenation is the safer habit for JSON.

For alertcondition(), JSON works too, as long as the values are placeholders:

alertcondition(crossUp, "Long JSON",
     '{"symbol":"{{ticker}}","side":"buy","price":{{close}},"tf":"{{interval}}"}')

What a webhook actually does

In the alert dialog, the "Webhook URL" field accepts an HTTPS address. When the alert fires, TradingView sends an HTTP POST to that URL with the message as the request body and a content type based on the message: if the message is valid JSON it is sent as application/json, otherwise as plain text. There is no authentication header, no signing, and the request comes from TradingView's published IP ranges. Anyone who knows your URL can post to it, so receivers should check a secret inside the body ("key":"...") and restrict to TradingView's IPs.

Other practical points:

  • Webhooks are available on paid plans only.
  • Delivery is one attempt; if your server is down, the alert is lost. Log every request on receipt.
  • The order of two alerts fired in the same second is not guaranteed.
  • The message body limit is generous for JSON but not unlimited; keep payloads short.

Message hygiene

  • Include the symbol, timeframe, side and the price the script saw. A message that says "Buy!" is useless an hour later.
  • Include a version or strategy name so old alerts are identifiable when you have several running.
  • Format prices with format.mintick; sending 5432.2500000001 to an exchange API is a rejected order.
  • Never put anything in a message you would not want in a log file. Messages are stored in your alert log and in whatever receives them.

Key idea: alertcondition() messages use {{placeholders}} filled at fire time; alert() messages are built in code with concatenation. Webhooks POST the raw message to your URL with no authentication, so design the receiver to verify it.

Try it: Build a JSON alert() message containing symbol, side, price, a computed stop and a "key" field with a made-up secret. Fire it into a free request-capture service to see the exact body. Then write the same message with alertcondition() placeholders and compare which fields you cannot include.

Recap

  • Placeholders: {{ticker}}, {{close}}, {{interval}}, {{plot("Title")}}, and {{strategy.order.*}} for order fills.
  • plot(..., display=display.none) exposes a value to placeholders without drawing it.
  • Build JSON in alert() with single-quoted strings and concatenation; validate a fired sample.
  • Webhooks POST the message to an HTTPS URL, unauthenticated, one attempt; verify a secret in the body and log receipts.
  • Every message needs symbol, timeframe, side, tick-rounded price, and a name to identify the source.

See it drawn

Original diagrams for the ideas on this page. Illustrative, not real market data.

One daily candle broken into four six-hour candlesA tall daily candle on the left and the four six-hour candles that make it up on the right, with dashed lines linking the day's open to the first candle and the day's close to the last.ONE DAILY CANDLEFOUR 6-HOUR CANDLEScloseopenhighlow=00:0006:0012:0018:00one dayThe same trading, summed up in one bar or spelled out in four.
How timeframes stack up. A daily candle is not different data, only coarser data: it opens where the first six-hour candle opened, closes where the last one closed, and its wicks reach the highest and lowest prices any of the four touched.
A fast and a slow moving average crossingA jagged price line with two smoother average lines through it; the fast average dips below the slow one on the left and cuts back above it in the middle, where a circle marks the crossing.pricefast averageslow averagefast crosses belowfast crosses abovethe slow averageAverages of recent closes; the fast one reacts sooner than the slow one.
Fast and slow moving averages crossing. A moving average is the average of the last few closing prices, redrawn each period. An average over fewer periods turns sooner than one over many, so the two lines cross whenever the recent pace of the market changes.