Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Webhook Security

Tracktile signs all webhook payloads with HMAC-SHA256, allowing you to verify that events genuinely came from Tracktile and haven’t been tampered with.

Every webhook request includes a signature in the X-Tracktile-Signature header:

X-Tracktile-Signature: t=1699900000,v1=5a3c8e9f...
  • t= Unix timestamp (seconds) when the webhook was sent
  • v1= HMAC-SHA256 signature (hex-encoded)
  1. Extract the timestamp (t) and signature (v1) from the header
  2. Read the raw request body (before JSON parsing)
  3. Compute: HMAC-SHA256(secret, "{timestamp}.{rawBody}")
  4. Compare your computed signature to v1 using constant-time comparison
  5. Verify the timestamp is within your tolerance (recommended: 5 minutes)
const crypto = require('crypto');
function verifyWebhook(req, secret, toleranceSec = 300) {
const sig = req.headers['x-tracktile-signature'];
const [tPart, v1Part] = sig.split(',');
const timestamp = parseInt(tPart.replace('t=', ''), 10);
const signature = v1Part.replace('v1=', '');
// Check timestamp freshness (prevents replay attacks)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSec) {
throw new Error('Webhook timestamp too old');
}
// Verify signature
const payload = `${timestamp}.${req.rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(req.rawBody);
}
// Express middleware to capture raw body
app.use('/webhook', express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
app.post('/webhook', (req, res) => {
try {
const event = verifyWebhook(req, process.env.WEBHOOK_SECRET);
// Process verified event
res.status(200).send('OK');
} catch (err) {
res.status(401).send('Invalid signature');
}
});

Each webhook has a unique secret used for signature verification. Secrets use the format:

whsec_dGhpcyBpcyBhIHNlY3JldCBrZXkgZm9yIHRlc3Q=

The webhook secret is returned only once when you create a webhook:

curl -X POST "https://api.tracktile.io/hooks" \
-H "Authorization: Bearer api-YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"name": "My Webhook",
"url": "https://your-server.com/webhook",
"on": "order.status.changed"
}'

Response:

{
"id": "whk_abc123",
"name": "My Webhook",
"url": "https://your-server.com/webhook",
"on": "order.status.changed",
"status": "active",
"secret": "whsec_dGhpcyBpcyBhIHNlY3JldC4uLg==",
"secretLastRotatedAt": "2026-02-03T12:00:00Z"
}

If your secret is compromised, rotate it immediately:

curl -X POST "https://api.tracktile.io/hooks/{id}/rotate" \
-H "Authorization: Bearer api-YOUR_TOKEN_HERE"

Response:

{
"secret": "whsec_bmV3IHNlY3JldCBnZW5lcmF0ZWQ=",
"secretLastRotatedAt": "2026-02-03T14:30:00Z"
}

The old secret is immediately invalidated. Update your server with the new secret right away.

Tracktile includes additional headers with each webhook request:

HeaderDescription
X-Tracktile-SignatureSignature for verification (t=...,v1=...)
X-Tracktile-Webhook-IdThe webhook configuration ID
Content-TypeAlways application/json
  • Return a 2xx response within 30 seconds
  • Process events asynchronously if your logic is complex
app.post('/webhook', (req, res) => {
const event = verifyWebhook(req, secret);
// Acknowledge immediately
res.status(200).send('OK');
// Process asynchronously
processEventAsync(event);
});

Webhooks may be delivered more than once. Use the event id to deduplicate:

app.post('/webhook', async (req, res) => {
const event = verifyWebhook(req, secret);
if (await isEventProcessed(event.id)) {
return res.status(200).send('Already processed');
}
await processEvent(event);
await markEventProcessed(event.id);
res.status(200).send('OK');
});

Related events share a transactionId. Use it to group events that occurred as part of the same operation.

View recent webhook deliveries:

curl "https://api.tracktile.io/hooks/{id}/executions" \
-H "Authorization: Bearer api-YOUR_TOKEN_HERE"

Each webhook tracks delivery metrics:

{
"id": "whk_abc123",
"name": "My Webhook",
"status": "active",
"successCount": 150,
"errorCount": 3,
"lastRunAt": "2026-02-03T10:30:00Z"
}