Every webhook delivery from Run a Call includes an HMAC signature header. Verify it before processing — otherwise anyone with your URL could spoof events.
How signatures work
When you create a webhook subscription, Run a Call generates a signing secret unique to that subscription. The secret is shown once at creation — store it where your receiving code can read it.
For every event delivery, Run a Call:
- Builds the JSON payload.
- Computes
HMAC-SHA256(secret, payload). - Sends the hex digest in the
X-Signatureheader. - POSTs the payload to your URL.
Your endpoint:
- Recomputes
HMAC-SHA256(secret, raw_request_body)with the secret you stored. - Compares against the
X-Signatureheader. - If they match, the request is authentic.
- If they don't match, reject with HTTP 400.
Pseudocode
import crypto from 'crypto';
function verifyWebhook(req, secret) {
const signature = req.headers['x-signature'];
const computed = crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed),
);
}
Use a constant-time comparison (timingSafeEqual) to prevent timing-attack leaks of the signature.
Use the raw body, not the parsed JSON
The signature is computed over the raw byte sequence of the payload. If your framework parses JSON before you can see the bytes, the re-serialized JSON may have different formatting and the signature won't match.
| Framework | How to capture raw body |
|---|---|
| Express | app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } })) |
| Next.js Route Handlers | Read await req.text() before parsing |
Replay protection
The payload includes a created_at timestamp. Reject events older than a few minutes to prevent an attacker from replaying captured events later.
const ageMinutes = (Date.now() - new Date(event.created_at).getTime()) / 60000;
if (ageMinutes > 5) return reject();
Rotating the secret
If you suspect a secret leaked:
- Settings → Integrations → Webhooks.
- Open the subscription.
- Rotate secret (generates a new one; old one stops working immediately).
- Update your endpoint to use the new secret.
There's no overlap window today — the old secret invalidates the moment the new one is issued. Plan the rotation accordingly (deploy first, then rotate).