From TradingView Alert to Webhook
Configure a TradingView alert to POST JSON to your own endpoint, then build a validated, secret-checked, idempotent Express webhook receiver that handles it safely.
Engineering content only — about software integration, not investment advice or trading signals. Connecting to live execution is your responsibility and carries real risk.
TradingView alerts can do more than show a popup — they can POST a JSON payload to a URL you control. That makes them a convenient trigger for automation. This article covers the mechanics: how the alert sends data, and how to build a receiver that validates, authenticates, and de-duplicates what arrives. What the alert means and whether you act on it is entirely your design and your responsibility.
How a TradingView alert calls a webhook
When you create an alert, TradingView offers a Webhook URL field under notifications. Paste your endpoint there. In the alert's Message box, put a JSON body — TradingView substitutes placeholders like {{ticker}} and {{close}} at fire time and sends it as an HTTP POST with Content-Type: application/json.
{
"secret": "REPLACE_WITH_A_LONG_RANDOM_STRING",
"event_id": "{{timenow}}-{{ticker}}",
"ticker": "{{ticker}}",
"action": "{{strategy.order.action}}",
"price": {{close}}
}Two realities shape the receiver design. First, the endpoint is public — anyone who learns the URL can hit it, so you must authenticate requests. Second, TradingView (and the network) may deliver the same alert more than once, so processing must be idempotent.
A minimal, safe receiver
Here's an Express endpoint that does the four things every webhook receiver needs: parse, authenticate via a shared secret, validate the shape, and de-duplicate by event id.
import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json({ limit: "16kb" }));
const SECRET = process.env.WEBHOOK_SECRET; // never hard-code this
const seen = new Set(); // swap for Redis/DB in production
// Constant-time comparison to avoid timing leaks
function validSecret(provided) {
if (typeof provided !== "string" || !SECRET) return false;
const a = Buffer.from(provided);
const b = Buffer.from(SECRET);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook/tradingview", (req, res) => {
const body = req.body ?? {};
// 1. Authenticate
if (!validSecret(body.secret)) {
return res.status(401).json({ error: "unauthorized" });
}
// 2. Validate shape
const { event_id, ticker, action, price } = body;
if (!event_id || !ticker || !["buy", "sell"].includes(action) || typeof price !== "number") {
return res.status(400).json({ error: "invalid payload" });
}
// 3. De-duplicate (idempotency)
if (seen.has(event_id)) {
return res.status(200).json({ status: "duplicate ignored" });
}
seen.add(event_id);
// 4. Hand off to your own logic — do heavy work asynchronously.
// This step is yours: what (if anything) happens next is your decision and risk.
console.log("accepted event", { event_id, ticker, action, price });
return res.status(200).json({ status: "accepted" });
});
app.listen(3000, () => console.log("listening on :3000"));Why each piece matters
- Shared secret in the body: TradingView can't set custom auth headers, so a secret inside the JSON is the practical pattern. Use a long random string, compare in constant time, and keep it in an environment variable — never in source control. Always serve the endpoint over HTTPS so the secret isn't sent in the clear.
- Validation: reject anything that doesn't match the exact shape you expect. Cap the body size and treat every field as untrusted input.
- Idempotency: key on a stable
event_id. The in-memorySetabove is illustrative only — it doesn't survive restarts or scale across instances. Use Redis with a TTL or a unique database constraint in production. - Respond fast: acknowledge with a
2xxquickly and push slow work (database writes, downstream calls) onto a queue. A webhook sender that times out will often retry, compounding your load.
Return appropriate status codes: 401 for a bad secret, 400 for malformed input, 200 once you've safely accepted (or recognized a duplicate). Senders treat non-2xx responses as failures and may retry — which is exactly why idempotency is mandatory.
Where to go next
This receiver is the front door. To see a complete, runnable implementation with the production hardening sketched above, read the TradingView Webhook Receiver code walkthrough. When you're ready to connect the front door to actual order placement — entirely at your own discretion and risk — the Build a TradingView-to-Broker Webhook Bridge project ties this together with the broker REST and WebSocket patterns from the broker-api track.