1. Verify the signature
Every webhook request includes:v1 is the hex of HMAC-SHA256 over {t}.{raw_body} with your webhook’s secret. Always verify before reading the body.
2. Reject stale timestamps
Signatures expire. The SDK helpers reject any signature whose timestamp is more than 5 minutes old by default. This blocks replay attacks where an attacker captures a signed request and re-posts it later. To loosen or tighten the window:3. Idempotent processing
Robase retries failed webhooks with exponential backoff (1s → 5s → 30s → 2m → 15m → 1h → 6h, 7 attempts). Your handler may receive the same event more than once. Use theid field (guaranteed unique, ULID-like) to deduplicate:
id as the primary key, a duplicate insert + ON CONFLICT DO NOTHING gets you the same guarantee with one SQL round trip.
4. Respond fast, process later
Your webhook handler should return200 in under a few hundred milliseconds. If downstream work (sending email, updating a CRM, running a background job) takes longer, enqueue it:
- Timeouts — Robase treats
>10sas a failure and retries. Repeated retries pile up on your queue. - Chain failures — your slow handler ties up worker slots, slowing unrelated traffic.
5. Don’t leak the secret
Never log the secret, or the full signature
Never log the secret, or the full signature
A developer debugging a signature failure might
console.log(secret) — and suddenly it’s in every SaaS log aggregator you send to. Log only the first 6 chars + … if you need a hint.Never expose the webhook URL publicly
Never expose the webhook URL publicly
An attacker who knows the URL can send crafted payloads (they’ll fail signature verification, but they cost you CPU). Put your webhook behind a path only you know, e.g.
/webhooks/robase/<random-16-char-token>.Rotate secrets after suspected exposure
Rotate secrets after suspected exposure
Dashboard → Webhooks → your webhook → Rotate secret. The old secret is invalidated immediately; you’ll need to redeploy with the new one.
Common bugs
| Symptom | Likely cause |
|---|---|
| Verification always fails, even on fresh events | You parsed JSON before verifying, or your framework strips headers. Use raw body. |
| Works locally, fails in prod | Clock skew — container doesn’t sync NTP. Run chronyd / systemd-timesyncd. |
| Some events verified, some not | Load balancer modifying body (CRLF → LF, or adding BOM). Disable body transformations for the webhook path. |
| Works for 5 minutes then breaks | You’re caching the secret; a rotation happened. Fetch the secret on each request or pub/sub rotations. |