Skip to content
FactorQX
intermediatetradingviewwebhookssecurity

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.

3 min read

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.

auth.js
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.

validate.js
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 id per 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.
replay.js
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

receiver.js
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.

Educational content. This article covers software development and research methods only. It is not investment advice, a trading signal, or a recommendation. See our disclaimer.

More in TradingView Automation

Securing TradingView Webhooks · FactorQX