Skip to content

Webhook Security

Every webhook delivery is signed so you can verify it came from TokenBot. Each request includes these headers:

HeaderDescription
X-TokenBot-Signaturesha256=<hex_digest> HMAC of the payload
X-TokenBot-TimestampUnix seconds when the signature was created
X-TokenBot-EventThe event type (e.g. trade.executed)
X-TokenBot-Delivery-IdUnique delivery ID (use for idempotency)
  1. TokenBot builds the string ${timestamp}.${raw_body}.
  2. It computes HMAC-SHA256(secret, that_string) and sends it as X-TokenBot-Signature: sha256=<hex>, with the timestamp in X-TokenBot-Timestamp.
  3. You recompute the same HMAC and compare in constant time.
  4. Reject deliveries whose timestamp is older than your tolerance (TokenBot uses a 5-minute replay window).
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, timestamp, secret, toleranceSeconds = 300) {
// Reject stale deliveries
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > toleranceSeconds) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Express middleware (note: raw body is required)
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyWebhook(
req.body,
req.headers['x-tokenbot-signature'],
req.headers['x-tokenbot-timestamp'],
process.env.WEBHOOK_SECRET
);
if (!ok) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body);
// Process event...
res.status(200).send('OK');
});
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str, tolerance=300) -> bool:
if abs(int(time.time()) - int(timestamp)) > tolerance:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = 'sha256=' + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)

Webhook signing secrets are prefixed whsec_ and are shown only once, when the webhook is created. Rotate a secret with POST /v1/webhooks/:id/rotate-secret (the previous secret keeps working for a short grace window).

  • Always verify signatures and the timestamp before processing events.
  • Use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python) to prevent timing attacks.
  • Deduplicate using X-TokenBot-Delivery-Id (or the payload’s event_id).
  • Respond with 2xx quickly; process events asynchronously.