Proud winners of the 2026 CFIN Foodtech Frontier Awards. Learn more →

Webhook Security

Verify webhook signatures and secure your endpoints

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

#Signature Verification

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)

#Verification Steps

  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)

#Code Examples

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');
  }
});
 

#Webhook Secrets

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


whsec_dGhpcyBpcyBhIHNlY3JldCBrZXkgZm9yIHRlc3Q=

#Getting Your Secret

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"
}

#Rotating Secrets

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.

#Additional Headers

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

#Best Practices

#Respond Quickly

  • 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);
});

#Handle Duplicates

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");
});

#Use the Transaction ID

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

#Monitoring

#Execution History

View recent webhook deliveries:

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

#Webhook Statistics

Each webhook tracks delivery metrics:

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