# TradingView alert message JSON: placeholders and payloads

URL: https://fincodelabs.com/en/blog/tradingview-alert-message-json · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: TradingView automation · Published: 2026-10-24 · Updated: 2026-11-05

How to write TradingView alert message JSON: documented placeholders, alert_message in Pine strategies, payload design and a tested Python validator.

**Short answer:** TradingView alert message JSON is the text you put in an alert's Message field, or build in Pine Script with alert() or alert_message, so that the webhook body is a JSON object. If the message is valid JSON, TradingView sends it with an application/json content type, otherwise as text/plain. Placeholders such as {{ticker}}, {{close}}, {{timenow}} and {{strategy.order.action}} are replaced with live values when the alert fires, so the template must remain valid JSON after substitution.

The alert message is the only data a [TradingView webhook bot](https://fincodelabs.com/en/blog/tradingview-webhook-bot) receives. TradingView's Help Center describes no way to add custom headers or sign the request, so everything the bridge needs, from symbol and side to an authentication secret and a unique alert ID, has to be in that text.

Below: the placeholders TradingView documents as of October 2026, per-order messages in Pine Script, a tested Python validator and common mistakes.

## How does the alert message become the webhook body?

When an alert with a Webhook URL fires, TradingView replaces the placeholders and posts the resulting message as the body of an HTTP request.

- **Content type.** Valid JSON is sent as application/json; anything else, including JSON broken by an empty value, arrives as text/plain, which a bridge should treat as a failed alert.
- **Length.** The alert dialog's Message field takes up to 4,000 characters, messages built with Pine's alert() function or alert_message argument up to 40,960, and the same limits apply to the request body (as of October 2026).
- **Source of the text.** Most alerts use the dialog text; alert() calls build the message in code; strategy order-fill alerts use the dialog text, which can pull per-order text through {{strategy.order.alert_message}}.
- **Snapshot.** An alert keeps a copy of the script and inputs from its creation; recreate it after code changes.

TradingView's [Terms of Use](https://www.tradingview.com/policies/), read on 5 November 2026, describe alerts and webhooks as licensed for display use and prohibit using them for automated trading, including through third-party products or services. Read those terms and take legal advice before relying on this route; it is your agreement with TradingView.

## TradingView placeholders for webhook JSON (as of October 2026)

TradingView's Help Center documents the following placeholders.

**Documented alert placeholders**

| Placeholder | Value |
| --- | --- |
| {{ticker}} | Symbol ticker, such as BTCUSD or AAPL |
| {{exchange}} | Exchange of the symbol; ends in _DL or _DLY for delayed data |
| {{open}}, {{high}}, {{low}}, {{close}}, {{volume}} | Values of the bar the alert triggered on; simple price alerts use 1-minute bars |
| {{time}} | Bar open time in UTC, formatted like 2019-08-27T09:56:00Z |
| {{timenow}} | Time the alert fired, same format, to the nearest second |
| {{interval}} | Chart timeframe; always 1 for price alerts and Range charts |
| {{plot_0}} to {{plot_19}}, {{plot("Name")}} | Indicator output series by number (first 20 only) or by plot title |
| {{syminfo.currency}}, {{syminfo.basecurrency}} | Currency of the symbol; base currency of a pair |
| {{strategy.order.action}} | buy or sell for the filled order |
| {{strategy.order.contracts}}, {{strategy.order.price}} | Quantity and price of the filled order |
| {{strategy.order.id}}, {{strategy.order.comment}} | Order ID; the comment, or the ID when no comment is set |
| {{strategy.order.alert_message}} | The alert_message argument of the order command |
| {{strategy.position_size}} | Current strategy position, as in Pine's strategy.position_size |
| {{strategy.market_position}}, {{strategy.prev_market_position}} | long, flat or short, now and before |
| {{strategy.market_position_size}}, {{strategy.prev_market_position_size}} | Absolute position size, now and before |

Strategy placeholders work only in strategy alerts. On an alert that fires before the bar closes, {{close}} is the latest price, not the final close, and {{strategy.order.price}} is a simulated fill, not your exchange price.

## How to write a JSON template that survives substitution

Write the template so that it stays valid JSON whatever a placeholder turns into. A strategy alert template might look like this:

```json
{
  "v": 1,
  "secret": "replace-with-a-long-random-string",
  "symbol": "{{ticker}}",
  "side": "{{strategy.order.action}}",
  "qty": "{{strategy.order.contracts}}",
  "ref_price": "{{close}}",
  "alert_id": "{{ticker}}-{{interval}}-{{strategy.order.id}}-{{time}}",
  "sent_at": "{{timenow}}"
}
```

Quoting every placeholder, numeric ones included, is the defensive choice: a missing or odd value still yields valid JSON, which the bridge rejects with a clear reason. TradingView documents placeholder prices as fixed-point numbers with a decimal point; parse them as decimals, not binary floats, then round to the exchange's tick and lot sizes.

> **Hypothetical filled message.** For a long entry on BTCUSDT the template could arrive as {"v": 1, "secret": "...", "symbol": "BTCUSDT", "side": "buy", "qty": "0.01", "ref_price": "67250.5", "alert_id": "BTCUSDT-60-Long-2026-10-24T14:00:00Z", "sent_at": "2026-10-24T14:00:03Z"}. All values are hypothetical.

## alert() and alert_message in Pine Script strategies

Pine Script builds messages in code with alert() or with the alert_message argument of order commands.

- **alert(message, freq)** works in indicators and strategies, with alert.freq_once_per_bar (default), alert.freq_once_per_bar_close or alert.freq_all. In strategies, alert() calls run at bar close by default regardless of freq unless calc_on_every_tick is enabled. The user selects Any alert() function call when creating the alert.
- **alert_message** is an argument of strategy.entry(), strategy.exit(), strategy.order(), strategy.close() and strategy.close_all(). When the order fills, its text replaces {{strategy.order.alert_message}} in the dialog's Message field. Order-fill alerts trigger at the moment of execution.

A hypothetical strategy that attaches a small JSON object to each order (syntax illustration only, not compiled in TradingView and not a trading signal):

```pine
//@version=6
strategy("Hypothetical webhook payloads", overlay = true)
fast = ta.ema(close, 12)
slow = ta.ema(close, 26)
if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long,
         alert_message = '{"type":"entry","ref":"' + str.tostring(close, format.mintick) + '"}')
if ta.crossunder(fast, slow)
    strategy.close("Long", alert_message = '{"type":"exit"}')
```

The dialog's Message field can then nest the per-order text as an object:

```json
{"v": 1, "secret": "replace-with-a-long-random-string", "symbol": "{{ticker}}",
 "side": "{{strategy.order.action}}", "qty": "{{strategy.order.contracts}}",
 "alert_id": "{{ticker}}-{{interval}}-{{strategy.order.id}}-{{time}}", "sent_at": "{{timenow}}",
 "order": {{strategy.order.alert_message}}}
```

Three documented rules matter here (as of October 2026):

- Placeholders cannot be used inside alert_message, and the documentation lists them only for alertcondition() and the dialog; build dynamic values in code with str.tostring() and variables such as syminfo.ticker.
- An order command without alert_message turns the placeholder into an empty string, so the nested template becomes invalid JSON and arrives as text/plain.
- str.tostring() returns the string NaN for an na value. NaN is not a valid JSON number under RFC 8259, and str.tostring(value, format.mintick) rounds prices to the symbol's tick size.

## Designing a robust payload: version, secret and idempotency key

A robust payload is versioned, authenticated, unique and easy to validate.

**Recommended payload fields**

| Field | Purpose |
| --- | --- |
| v | Schema version, so the bridge can reject old templates after a format change |
| secret | Long random value known only to the bridge; never an exchange key or password |
| alert_id | Idempotency key. {{ticker}}-{{interval}}-{{strategy.order.id}}-{{time}} collapses repeats on one bar, but also blocks a genuine second fill of that order ID on the same bar. {{strategy.order.id}}-{{timenow}} catches only TradingView's resends: a second alert or a repeating frequency gets a new time and a new ID. For once-per-bar indicator alerts, use {{ticker}}-{{interval}}-{{time}} plus the side. |
| symbol, side, qty | What to trade; map TradingView tickers to exchange symbols on the server |
| sent_at | {{timenow}}, used to reject stale deliveries |
| ref_price | For slippage monitoring only, never the order price without checks |

A secret in the body is a shared password, not a signature: anyone who sees a message can replay it. Combine it with HTTPS, TradingView's source IP addresses, a timestamp window and a store of processed alert IDs. Exchange keys stay on the server with trade-only permissions, as described in [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

## Validating TradingView webhook JSON in Python

The validator below runs before anything reaches an exchange: it parses strictly, compares the secret in constant time, enforces an allowlist and types, and rejects stale messages. For a complete web service, see [a tested webhook receiver in Python](https://fincodelabs.com/en/blog/tradingview-webhook-python).

```python
import hmac
import json
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation

SECRET = b"replace-with-a-long-random-string"   # never an exchange key or password
SYMBOLS = {"BTCUSDT", "ETHUSDT"}                # allowlist for this bot
VERSIONS = {1}

def reject_constant(name):                      # json.loads accepts NaN by default
    raise ValueError(f"non-standard JSON value {name}")

def validate(body, now, max_age_s=60):
    try:
        msg = json.loads(body, parse_constant=reject_constant)
    except RecursionError:                      # a body of thousands of opening brackets
        raise ValueError("JSON nested too deeply") from None
    if not isinstance(msg, dict):
        raise ValueError("message is not a JSON object")
    if not hmac.compare_digest(str(msg.get("secret", "")).encode(errors="replace"), SECRET):
        raise ValueError("bad secret")
    v, symbol = msg.get("v"), msg.get("symbol")
    if not isinstance(v, int) or v not in VERSIONS:
        raise ValueError("unknown schema version")
    if not isinstance(symbol, str) or symbol not in SYMBOLS:
        raise ValueError("symbol not allowed")
    if msg.get("side") not in ("buy", "sell"):
        raise ValueError("side must be buy or sell")
    try:
        qty = Decimal(str(msg.get("qty")))
    except InvalidOperation:
        raise ValueError("qty is not a number") from None
    if not qty.is_finite() or qty <= 0:
        raise ValueError("qty must be a positive number")
    sent = datetime.strptime(str(msg.get("sent_at")), "%Y-%m-%dT%H:%M:%SZ")
    age = (now - sent.replace(tzinfo=timezone.utc)).total_seconds()
    if not -5 <= age <= max_age_s:
        raise ValueError(f"alert is {age:.0f} s old")
    alert_id = msg.get("alert_id")
    if not isinstance(alert_id, str) or not alert_id:
        raise ValueError("missing alert_id")
    return alert_id, msg["side"], symbol, qty

good = ('{"v": 1, "secret": "replace-with-a-long-random-string", "symbol": "BTCUSDT", '
        '"side": "buy", "qty": "0.01", "ref_price": "67250.5", '
        '"alert_id": "BTCUSDT-60-Long-2026-10-24T14:00:00Z", "sent_at": "2026-10-24T14:00:03Z"}')
tests = {
    "good": good,
    "NaN qty": good.replace('"0.01"', "NaN"),
    "empty alert_message": '{"v": 1, "order": , "sent_at": "2026-10-24T14:00:03Z"}',
    "two minutes late": good.replace('14:00:03Z"}', '13:58:03Z"}'),
}
now = datetime(2026, 10, 24, 14, 0, 5, tzinfo=timezone.utc)   # hypothetical arrival time
for name, body in tests.items():
    try:
        print(f"{name}: accepted {validate(body, now)}")
    except ValueError as err:
        print(f"{name}: rejected ({err})")
```

Running it prints:

```
good: accepted ('BTCUSDT-60-Long-2026-10-24T14:00:00Z', 'buy', 'BTCUSDT', Decimal('0.01'))
NaN qty: rejected (non-standard JSON value NaN)
empty alert_message: rejected (Expecting value: line 1 column 19 (char 18))
two minutes late: rejected (alert is 122 s old)
```

Python's json module accepts NaN and Infinity by default, outside the JSON specification, so parse_constant rejects them. The secret is checked before any other field, and wrong types, such as a list as v or symbol, or a body nested too deeply, are rejected without an uncaught error. Decimal keeps 0.01 exact. The 60-second window passes resends, which come 5 seconds apart, to the deduplication step in the [webhook bot guide](https://fincodelabs.com/en/blog/tradingview-webhook-bot), while refusing messages that arrive minutes late.

## Common mistakes with TradingView alert JSON

Most broken automations trace back to a handful of template errors.

- Typographic quotes or a trailing comma pasted from a document editor, which silently turn the message into text/plain.
- Strategy placeholders in an indicator alert, where they do not work.
- Nesting {{strategy.order.alert_message}} while some order commands have no alert_message.
- Confusing {{time}} (bar open) with {{timenow}} (fire time), or assuming local time; both are UTC.
- Matching {{exchange}} exactly when delayed data adds _DL or _DLY.
- Treating {{close}} or {{strategy.order.price}} as the price you will get.

Templates, parsing rules and risk checks belong in the specification of a [TradingView automation](https://fincodelabs.com/en/services/tradingview-automation) project, as in the [order bridge scenario](https://fincodelabs.com/en/projects/tradingview-order-bridge). A valid payload says nothing about whether the strategy works, and automated orders can lose money; verify the logic separately, for example with a [Python re-implementation](https://fincodelabs.com/en/blog/pine-script-to-python).

## Frequently asked questions

### Does TradingView send webhooks as JSON or plain text?

It depends on the message. If the alert message is valid JSON, TradingView sends it with an application/json content type; otherwise it uses text/plain, so a broken template still arrives, but as plain text.

### Can I add custom headers or a signature to a TradingView webhook?

TradingView's Help Center describes no option for custom headers or signing; only the alert message is sent. Authenticate with a secret in the message, the published source IP addresses and, over HTTPS, TradingView's webhook certificate.

### Why is {{strategy.order.alert_message}} empty?

The order command that filled had no alert_message argument, so Pine replaced the placeholder with an empty string, which can make a JSON template invalid. Strategy placeholders also work only in strategy alerts.

### What time zone do {{time}} and {{timenow}} use?

Both are UTC, formatted like 2019-08-27T09:56:00Z. {{time}} is the bar's opening time and {{timenow}} the moment the alert fired, to the nearest second.

### How long can a TradingView alert message be?

As of October 2026, 4,000 characters in the Message field of the alert dialog and 40,960 characters for messages built with alert() or alert_message; the same limits apply to the webhook body.

## Sources

- [TradingView Help Center: How to use a variable value in alert (placeholders)](https://www.tradingview.com/support/solutions/43000531021-how-to-use-a-variable-value-in-alert/)
- [TradingView Help Center: How to configure webhook alerts](https://www.tradingview.com/support/solutions/43000529348-how-to-configure-webhook-alerts/)
- [TradingView Help Center: Alert name and message size limits](https://www.tradingview.com/support/solutions/43000773947-alert-name-and-message-size-limits/)
- [TradingView Help Center: Webhook authentication](https://www.tradingview.com/support/solutions/43000680459-webhook-authentication/)
- [TradingView Help Center: Webhook resubmission](https://www.tradingview.com/support/solutions/43000735201-webhook-resubmission/)
- [TradingView Help Center: Strategy alerts](https://www.tradingview.com/support/solutions/43000481368-strategy-alerts/)
- [TradingView Pine Script v6 User Manual: Alerts](https://www.tradingview.com/pine-script-docs/concepts/alerts/)
- [TradingView Pine Script v6 Reference Manual (alert, strategy.entry, str.tostring, format.mintick)](https://www.tradingview.com/pine-script-reference/v6/)
- [RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format](https://www.rfc-editor.org/rfc/rfc8259.html)
- [Python documentation: json (parse_constant, NaN handling)](https://docs.python.org/3/library/json.html)
- [Python documentation: hmac.compare_digest](https://docs.python.org/3/library/hmac.html)
- [Python documentation: decimal](https://docs.python.org/3/library/decimal.html)
- [TradingView: Terms of Use (read 5 November 2026)](https://www.tradingview.com/policies/)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/tradingview-alert-message-json

Cite as: Fincode, "TradingView alert message JSON: placeholders and payloads", updated November 5, 2026, https://fincodelabs.com/en/blog/tradingview-alert-message-json
