Skip to main content
Webhooks are inbound HTTP requests from Robase to your server. Without verification, an attacker who knows your endpoint URL can post fake events — “this OTP was delivered” — and walk past your auth. This guide covers the three layers you need.

1. Verify the signature

Every webhook request includes:
Robase-Signature: t=1718637005,v1=3c2a9f...
Robase-Event: sms.delivered
Content-Type: application/json
v1 is the hex of HMAC-SHA256 over {t}.{raw_body} with your webhook’s secret. Always verify before reading the body.
import { verifyWebhook } from '@robase/node';
import express from 'express';

const app = express();

// Reserve express.raw for the webhook route — we need the unparsed body.
app.post('/webhooks/robase', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['robase-signature'] as string;
  const raw = (req.body as Buffer).toString('utf8');

  const ok = await verifyWebhook(raw, sig, process.env.ROBASE_WEBHOOK_SECRET!);
  if (!ok) return res.status(401).end();

  const event = JSON.parse(raw);
  await processEvent(event);
  res.status(200).end();
});
Verify against the raw body bytes, not a re-serialized version. JSON parsers often reorder keys or normalize whitespace — that breaks HMAC. In Express you need express.raw; in Flask get_data(); in Go io.ReadAll(r.Body) before any decode.

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:
await verifyWebhook(raw, sig, secret, 60);  // 60 seconds tolerance
If your server’s clock drifts (containers without NTP can drift minutes), 300 seconds is a reasonable default. Monitor clock skew — your SRE checklist should include NTP sync.

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 the id field (guaranteed unique, ULID-like) to deduplicate:
async function processEvent(event: { id: string; type: string; data: any }) {
  const seen = await redis.set(`evt:${event.id}`, '1', { NX: true, EX: 86400 });
  if (!seen) {
    // We've processed this event already — ack and skip.
    return;
  }

  // Your real logic.
  await handle(event);
}
Or, if you persist events to a table with 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 return 200 in under a few hundred milliseconds. If downstream work (sending email, updating a CRM, running a background job) takes longer, enqueue it:
app.post('/webhooks/robase', async (req, res) => {
  const event = verifyAndParse(req);
  if (!event) return res.status(401).end();

  await queue.enqueue('robase-event', event);  // fire and forget
  res.status(200).end();
});
If you block the handler waiting on downstream work, you risk:
  • Timeouts — Robase treats >10s as 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

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.
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>.
Dashboard → Webhooks → your webhook → Rotate secret. The old secret is invalidated immediately; you’ll need to redeploy with the new one.

Common bugs

SymptomLikely cause
Verification always fails, even on fresh eventsYou parsed JSON before verifying, or your framework strips headers. Use raw body.
Works locally, fails in prodClock skew — container doesn’t sync NTP. Run chronyd / systemd-timesyncd.
Some events verified, some notLoad balancer modifying body (CRLF → LF, or adding BOM). Disable body transformations for the webhook path.
Works for 5 minutes then breaksYou’re caching the secret; a rotation happened. Fetch the secret on each request or pub/sub rotations.