TradingView → tvhook

Your alert message, exactly specified

Paste your personal webhook URL into the Webhook URL field in TradingView. Paste one of the JSON messages below into Message — or use the Pine helpers to build it.

For AI agents (Claude / Codex) writing Pine Script: fetch /llms.txt and paste it into the prompt. It holds this whole contract, the templates and the checklist in plain text.

No source: TV, no order.
The source value must be exactly TV (case-sensitive). Missing or different values are ignored with HTTP 200 and are not recorded as trades.

BingX signals place real orders: MARKET entry, STOP_MARKET stop-loss, TAKE_PROFIT_MARKET take-profit, reduce-only MARKET exit (hedge mode). Quantity is never taken from the alert: the server sizes each entry from your console settings (leverage, margin %) and a live free-margin read. Any failure after the entry fill is recorded with a status that asks you to check the exchange.

30-second summary

  • POST https://tvhook.app/hook/<token>. The body must be JSON — TradingView sends the alert Message verbatim, so the Message itself is the JSON.
  • One object per alert: source, action (entry|exit), symbol, direction (long|short), exchange (bingx), ep, sl, tp. Optional alert_id.
  • Quantity and leverage are never in the payload. The server sizes each entry from your console settings (leverage, margin %, ladder) and a live free-margin read.
  • entry = MARKET order + STOP_MARKET stop-loss + TAKE_PROFIT_MARKET take-profit on the exchange. One position per symbol and direction.
  • exit = reduce-only MARKET close of the tracked quantity, then the resting stops are cancelled.
  • Pine: alert(msg, alert.freq_once_per_bar) + alert condition "Any alert() function call" + barstate.isrealtime guard + hand-built JSON string.

Webhook contract

tvhook — TradingView payload specification
FieldRequiredDescriptionExample
sourceYesExact source marker. Echo messages never pass this gate.TV
actionYesentry opens a position; exit closes the tracked position with a reduce-only MARKET order.entry
symbolYesExchange symbol. BTCUSDT, BTC-USDT, BTCUSDT.P and BINGX:BTCUSDT.P all map to BingX BTC-USDT (prefix and .P stripped).BTCUSDT
directionYeslong or short only (buy/sell are rejected). On exit it must match the open position.long
exchangeYesbingx (the only supported exchange).bingx
epYesEntry/reference price; a finite positive number or decimal string. Required on exit too (ignored there).60000
tpYesTake-profit price (TAKE_PROFIT_MARKET). Entry: above ep for long, below for short. Exit: any positive value.65000
slYesStop-loss price (STOP_MARKET). Entry: below ep for long, above for short. Exit: any positive value.58000
alert_idNoIdempotency tag, letters/digits/_.- up to 64 characters. Identical signals within 10 minutes are dropped as duplicates; a per-bar alert_id keeps them apart.bar-1726200000

Entry price order is enforced: long needs sl < ep < tp, short needs tp < ep < sl (strict). Anything else is HTTP 400. Exit alerts must still carry positive ep/tp/sl — put the current price in all three.

Entry alert example

Exit alert example · closes the tracked long position

An exit only executes when tvhook tracks an open position for the same symbol and direction; otherwise it is recorded as rejected. The reduce-only MARKET close goes first, then the resting stops are cancelled.

Supported formats

Send valid JSON, either the direct object shown here or {"content":"source: TV\nsymbol: BTCUSDT\n…"}. When content exists, only content is parsed. Its key: value pairs may be separated by newlines, spaces, commas, or pipes; duplicate fields are rejected. Do not send API keys or secrets in an alert.

Responses and rejection codes

HTTP codes: unknown token 404 · invalid JSON or contract violation 400 · body over 16 KB 413 · database failure 500. Every accepted signal returns 200 with {id, status, error_code} — a rejection is still HTTP 200, so read the body. source ≠ TV → 200 ignored. Duplicate within 10 minutes → 200 duplicate.

statuserror_codeMeaning
rejectedinvalid_symbolSymbol did not normalise to BASE-QUOTE.
rejectedkeys_missing / keys_invalid / keys_ambiguousNo usable BingX API key saved in the console.
rejectedposition_existsEntry while a position for that symbol and direction is already tracked. Send exit first.
rejectedno_positionExit with no tracked position for that symbol.
rejecteddirection_mismatchExit direction differs from the open position. The position is never flipped.
rejectedposition_lockedAnother exit or reconcile owns that position right now; retry after a minute.
sizing_failedqty_below_minimum / no_available_margin / contract_missingNothing sent: computed quantity below the contract minimum, or no free margin.
leverage_failedbingx_<code>Leverage call refused; nothing sent.
entry_rejectedbingx_<code> / order_<status>Exchange refused the MARKET entry; nothing open. bingx_101414 = margin mode "Separate Cross" (see exchange setup).
entry_unknown / entry_unconfirmed<transport> / fill_<status>Order may exist: parked as needs_review, cron reconciles every minute.
sl_attach_failedbingx_<code>Entry filled but the stop could not be attached: position open_no_sl until cron re-protects it.
fillednull / tp_attach_failedEntry filled and protected (TP attach may have failed — SL is in place).
exit_rejected / exit_unknownbingx_<code> / qty_unknown / <transport>Close not confirmed; stops left resting; cron retries every minute.
closednull / already_closed / sl_filled / tp_filledClosed or reconciled. sl_filled / tp_filled = the exchange stop closed it, pnl recorded.

Pine templates (copy & paste)

Pine v6. The helpers hand-assemble the JSON; numbers use str.tostring(x, format.mintick); alert_id is the bar time. Written in the same style as a live-verified test script but not compiled here — compile once in the Pine editor before trusting them.

Helpers — paste once above your logic

strategy() insertion example

indicator() insertion example

Creating the alert in TradingView (3 steps)

  1. Add the script to the chart, then open Alerts and create an alert on it (not on the symbol).
  2. Condition: pick the script and choose "Any alert() function call" — the Message box is ignored; alert() supplies the JSON.
  3. Notifications: enable Webhook URL and paste https://tvhook.app/hook/<token> from your console. Expiration: open-ended.

Common mistakes checklist

  • Message is plain text "source: TV …" instead of JSON → 400 Invalid JSON. Text form only works wrapped as {"content":"…"}.
  • alert() not guarded by barstate.isrealtime → state is consumed while history replays and nothing fires live.
  • alert() without alert.freq_once_per_bar → several webhooks per bar.
  • Alert condition set to Crossing / Greater than / order fills instead of "Any alert() function call" → the JSON never leaves TradingView.
  • exit alert missing ep/sl/tp → 400. Fill all three with the current price.
  • direction "buy" / "sell" → 400. Use long / short.
  • SL on the wrong side (long with sl ≥ ep, short with sl ≤ ep) → 400.
  • Symbol that does not normalise (odd suffix/prefix) → rejected invalid_symbol. BTCUSDT, BTCUSDT.P, BINGX:BTCUSDT.P are fine.
  • BingX margin mode "Separate Cross" (NEW) → every API order refused with bingx_101414. Use Cross, Isolated or Separate Isolated.
  • Second entry while one is open → position_exists. Exit direction ≠ open direction → direction_mismatch.
  • Identical signal within 10 minutes without alert_id → duplicate, silently not executed.
  • Never put API keys, secrets or the webhook token in the alert body. Extra fields (qty, leverage) are ignored.

TP/SL and settlement

  • Stops rest on the exchange (reduce-only, hedge mode). When price hits them BingX closes the position without any alert.
  • The next exit alert for that symbol and direction finds nothing live, looks up the tracked stop orders and records closed + sl_filled (loss) or tp_filled (win) with exit price and pnl. No new order is sent.
  • Parked positions (needs_review, open_no_sl) are swept by cron every minute: refused closes are re-sent, naked positions get stops re-attached, gone positions are settled.
  • Always emit an exit alert whenever your strategy considers the trade over (TP, SL, time stop, opposite signal). Without it the journal row stays open and the next entry is rejected with position_exists.
  • No partial exits: an exit closes the whole tracked quantity. To reverse, send exit then entry in the other direction.
  • The anti-martingale ladder (if enabled) moves leverage after each settled win or loss; the payload never controls it.

Exchange setup (BingX USDT-M perpetuals)

  • Margin mode per symbol: Cross, Isolated or Separate Isolated. Not "Separate Cross" — that mode rejects API order sources (101414, observed 2026-09-14).
  • Position mode: hedge (LONG / SHORT sides). tvhook sets leverage per side before each entry.
  • API key on a sub-account, trading permission only, no withdrawal permission. The console refuses keys unless you confirm withdrawal is disabled; tvhook does not verify permissions through the API — you do.
  • Sizing: leverage 1–100 (default 3), margin % of available balance per entry 0.1–95 (default 10). notional = available × margin% × leverage, quantity floored to the contract step. Too small → sizing_failed qty_below_minimum.
  • Start with leverage 3, margin 1–5 % and a 30-second-bar test script; confirm one entry → exit round trip in the journal before wiring a real strategy.