Handle webhooks
Verify the HMAC signature, respect the replay window, and respond before Hatched retries.
Webhooks are how your backend reacts to buddy events. Hatched signs every request so you can trust the payload came from us and hasn't been tampered with.
Subscribe
- Dashboard → Settings → Webhooks → Add endpoint.
- Pick the event types you care about (catalogue).
- Copy the signing secret once — we don't show it again.
Programmatically:
await hatched.webhooks.create({
url: 'https://your-app.com/api/webhooks/hatched',
events: ['buddy.leveled_up', 'badge.awarded', 'streak.milestone'],
});Verify with the SDK helper
@hatched/sdk-js ships a static helper for signature verification:
import { WebhooksResource } from '@hatched/sdk-js';
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get('hatched-signature') ?? '';
const valid = WebhooksResource.verifySignature(
rawBody,
signature,
process.env.HATCHED_WEBHOOK_SECRET!,
);
if (!valid) return new Response('invalid signature', { status: 400 });
const event = JSON.parse(rawBody);
await handle(event);
return new Response(null, { status: 202 });
}The header format is t=<unix_ts>,v1=<hmac_sha256_hex>. The helper
verifies the HMAC, rejects timestamps older than toleranceSeconds
(default 300), and uses timingSafeEqual under the hood.
Sign over raw body bytes. A JSON
parse→stringifyround-trip reorders keys and breaks the signature. Read the body asBufferorstringbefore any framework middleware parses it as JSON.
Manual verification (without the SDK)
import crypto from 'node:crypto';
export function verifyHatchedSignature(header: string, rawBody: Buffer, secret: string) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const ts = parts.t;
const sig = parts.v1;
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody.toString('utf8')}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(sig, 'hex'),
Buffer.from(expected, 'hex'),
);
}Respond quickly
- Return a 2xx within 10 seconds, or Hatched retries the delivery.
- Retry schedule: +5s, +30s, +5min. After the third failure the
delivery is marked
failedin the delivery log but the state in Hatched is already correct. - If your handler is expensive, ack fast and push to a queue.
Idempotency
Every webhook carries a unique deliveryId. Dedupe against it before
side-effects:
if (await alreadyHandled(event.deliveryId)) return ack();
await recordHandled(event.deliveryId);
await doTheWork(event);Replay from the delivery log
Dashboard → Developers → Webhook deliveries shows every attempt with payload, headers, and response. Replay failed deliveries once your endpoint is healthy:
await hatched.webhooks.replay(deliveryId);Framework examples
- Next.js route handler — App Router, raw body, signature verify.
- Express middleware —
express.raw- signature verify before JSON parsing.
- Edge runtimes — Workers/Vercel Edge notes.