Verify and Process Events

Every delivery includes:

  • X-Webhook-Signature — HMAC-SHA256 hex of the raw request body, keyed with your webhook secret.
  • X-Timestamp — Unix time (seconds) when the event was sent.

Verification requirements:

  • Compute the HMAC over the raw bytes, not a re-serialized object.
  • Compare with a constant-time equality check.
  • Reject requests with missing/invalid signatures (401), stale timestamps, and secrets that fail to load.
  • Store secrets in environment variables; rotate via API Settings on a schedule.

JavaScript

const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Ruby

require 'openssl'
def verify_webhook_signature(raw_body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
ActiveSupport::SecurityUtils.secure_compare(signature.to_s, expected)
end

Python

import hmac
import hashlib
def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)

Example handler (Node)

const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
switch (req.body.event) {
case 'parcel.created':
break;
default:
if (String(req.body.event).startsWith('tracking.')) {
// handle tracking
}
}
res.status(200).json({ received: true });
});

Example handler (Rails)

class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :verify_webhook_signature
def receive
case params[:event]
when 'parcel.created'
handle_parcel_created(params[:data])
when /^tracking\./
handle_tracking_update(params[:event], params[:data])
end
head :ok
end
private
def verify_webhook_signature
signature = request.headers['X-Webhook-Signature']
secret = ENV['WEBHOOK_SECRET']
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post)
unless ActiveSupport::SecurityUtils.secure_compare(signature.to_s, expected)
render json: { error: 'Invalid signature' }, status: :unauthorized
end
end
end