Securing TradingView Webhooks
Harden a TradingView webhook receiver with shared-secret auth, payload validation, idempotency keys, rate limiting, and replay protection — with a Node/Express example.
Engineering and education only. This article covers webhook security mechanics. It is not investment advice, a trading signal, or a recommendation. Connecting a webhook receiver to live execution is your responsibility and risk.
A webhook endpoint is a public door into your system. Anyone who learns the URL can POST to it, so the receiver — not the sender — must enforce every guarantee. TradingView alerts send a plain HTTP POST with a body you define in the alert message; there is no built-in signature, so authentication and integrity are entirely on your side. This article layers the defenses you need on top of a basic receiver.
Shared-secret authentication
The simplest workable scheme is a secret string you embed in the alert payload and verify on arrival. The critical detail: compare it in constant time. A naive === short-circuits on the first differing byte, leaking timing information that lets an attacker recover the secret byte by byte.
import crypto from "node:crypto";
function safeEqual(a, b) {
const ba = Buffer.from(a ?? "", "utf8");
const bb = Buffer.from(b ?? "", "utf8");
if (ba.length !== bb.length) return false; // length is not secret
return crypto.timingSafeEqual(ba, bb);
}Store the expected secret in an environment variable, never in code. If you control the proxy, prefer sending it in a header rather than the JSON body so it stays out of request logs that capture payloads.
Payload validation
Treat the body as hostile until proven well-formed. Reject anything that does not match a strict schema — wrong types, missing fields, or unexpected keys.
function validate(p) {
return p
&& typeof p.symbol === "string" && p.symbol.length <= 20
&& (p.action === "buy" || p.action === "sell")
&& Number.isFinite(p.qty) && p.qty > 0;
}A schema check here stops malformed alerts from ever reaching your downstream logic, and it converts a class of bugs into clean 400 responses.
Idempotency and replay protection
Networks retry. An alert may be delivered twice, and a captured request can be replayed by an attacker. Two mechanisms defend against this:
- Idempotency key — a unique
idper alert. Record processed ids; a repeat is acknowledged but not re-executed. - Freshness window — a timestamp in the payload. Reject anything older than a short window (say 60 seconds) so a captured request cannot be replayed later.
const seen = new Set(); // back this with Redis + TTL in production
function accept(p) {
const age = Date.now() - Date.parse(p.timestamp);
if (!(age >= 0 && age < 60_000)) return false; // stale or future-dated
if (seen.has(p.id)) return false; // already handled
seen.add(p.id);
return true;
}In a multi-instance deployment, the seen set and rate-limit counters must live in shared storage like Redis, or two instances will disagree.
Transport, rate limiting, and IP allowlisting
- HTTPS only. Terminate TLS at a reverse proxy (nginx, Caddy) and never expose the app server directly. A shared secret over plain HTTP is readable by anyone on the path.
- Rate limiting. Cap requests per source to blunt floods and brute-force attempts against the secret. A simple token bucket per IP is enough.
- IP allowlisting — with caveats. TradingView publishes the IP ranges its alerts originate from, and allowlisting them adds a layer. But those ranges change, a misconfigured proxy can mask the real client IP behind
X-Forwarded-For, and IPs can be spoofed at some layers. Treat allowlisting as defense-in-depth, never as your primary auth.
Putting it together
import express from "express";
const app = express();
app.use(express.json({ limit: "8kb" })); // bound the body size
app.post("/webhook", (req, res) => {
if (!safeEqual(req.get("X-Webhook-Secret"), process.env.WEBHOOK_SECRET))
return res.sendStatus(401);
if (!validate(req.body)) return res.status(400).json({ error: "bad payload" });
if (!accept(req.body)) return res.sendStatus(409); // stale or duplicate
enqueue(req.body); // hand off; never block the response
return res.sendStatus(202);
});Acknowledge fast and process asynchronously — a slow handler causes TradingView to retry, amplifying load exactly when you are struggling.
Where to go next
Start from the basics in From TradingView Alert to Webhook, then apply these defenses to the reference implementation in TradingView Webhook Receiver. Pair this with monitoring so rejected and dead-lettered requests surface as alerts rather than silent 4xx responses.