Skip to content

Signature verification

When signing ships, every outbound webhook will include two headers:

X-Webhook-Signature: t=1746440100,v1=9f8e7d6c5b4a3928176054321aabbccdd11ee2f3a4c5b6d7e8f9a0b1c2d3e4f5a
X-Webhook-Timestamp: 1746440100

The signature is base64- or hex-encoded HMAC-SHA256 of:

HMAC-SHA256(webhook_secret, f"{timestamp}.{raw_body_bytes}")

The webhook_secret is shown exactly once when generated or rotated. Argon2id and Fernet ensure the database-stored copy is not reversible. To rotate, generate a new one, deploy it to your verifier, and revoke the old one — the dashboard supports overlapping active secrets.

Reject webhooks whose X-Webhook-Timestamp differs from server time by more than 5 minutes. This bounds the window in which a replayed payload can be accepted.

const ageSeconds = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (ageSeconds > 300) throw new Error("webhook too old");

When signing ships, expect to deal with:

  • Missing headers. Reject — the gateway will always set them once enabled.
  • Bad signature. Reject — possibly forged, possibly a webhook_secret rotation that didn’t propagate.
  • Stale timestamp. Reject — replay attempt or clock skew. Log for investigation; don’t silently drop.
  • Body re-parsing. The signature is computed against the raw body bytes before any framework parses JSON. Use middleware that exposes the raw body (express.raw({type: 'application/json'}), FastAPI’s await request.body()).