Every webhook Run a Call sends is signed with your subscription's signing secret. Your receiving code should check that signature before trusting the event — otherwise anyone who learns your URL could send fake events. This article is for the developer writing that code.

Tip

Using Zapier, Make or n8n without custom code? Those tools can't easily verify signatures. Keep the webhook URL private, and don't let a webhook alone trigger anything sensitive (like moving money) without a check back against the API.

The signature header

Each delivery includes:

Run-A-Call-Signature: t=1758035045,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
PartMeaning
tWhen Run a Call signed the request, in Unix seconds
v1Hex-encoded HMAC-SHA256 of the string <t>.<raw request body>, keyed with your signing secret

The signing secret is the full whsec_... value you saved when you created (or last rotated) the subscription.

How to verify

  1. Read the raw request body exactly as received — before any JSON parsing.
  2. Split the header on , and take t and v1.
  3. Reject the request if t is more than 5 minutes from your server's current time. This stops old captured requests from being replayed.
  4. Compute HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex.
  5. Compare it to v1 with a constant-time comparison. If they don't match, reject the request (for example with HTTP 400) and don't process it.
  6. If it matches, respond with a 2xx quickly and do any slow work afterwards. Deliveries that take longer than 10 seconds count as failures.

Node.js example

import crypto from "node:crypto";

export function verifyRunACall(rawBody, header, secret, toleranceSec = 300) {
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=", 2)),
  );
  const t = Number.parseInt(parts.t, 10);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || !v1) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - t) > toleranceSec) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  if (expected.length !== v1.length) return false;
  return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
}

Getting the raw body

The signature covers the exact bytes Run a Call sent. If your framework parses the JSON first and you re-serialize it, spacing can change and the check fails.

FrameworkHow to get the raw body
Expressapp.post("/hook", express.raw({ type: "application/json" }), handler)req.body is a Buffer; use req.body.toString("utf8")
Next.js route handlerconst rawBody = await request.text() before parsing

Duplicates

Retries mean the same event can occasionally arrive more than once. Every delivery has a unique id in the body (also in the Run-A-Call-Event-Id header) — store the ids you've handled and skip repeats.

Rotating the secret

If a secret may have leaked:

  1. Go to Settings → Integrations → Webhooks and open the subscription.
  2. Click Rotate secret and confirm.
  3. Copy the new secret from Save your new signing secret.
  4. Update your receiving code with it.
Warning

The old secret stops working immediately — there's no grace period. Deliveries sent before you update your code will fail the check. To avoid missing events, click Pause first, rotate, update your code, then Resume. Events that happen while paused aren't sent later, so keep the pause short.

Testing

Click Send test event on an active subscription. It sends a signed webhook.test event you can use to confirm your verification works, and the result shows under Recent deliveries.