Webhook Integration Done Right: HMAC-SHA256, At-Least-Once Delivery, and Idempotency Keys
Webhook Integration Done Right
💡 Target Audience: Backend engineers wiring HyperBabel events into their billing, audit log, analytics, or moderation pipelines.
Why webhooks beat polling
If you've been polling /chat/messages every minute to feed your analytics or your moderation queue, you already know the problems: missed events when polling intervals slip, wasted DB queries when nothing changed, and an architecture that doesn't scale past a few thousand rooms.
Webhooks invert the flow. HyperBabel POSTs an event to your URL the moment something happens — no polling, no missed updates, no wasted queries.
The 14 event types
Register a webhook for any subset:
- Chat (4):
chat.message.created,chat.message.updated,chat.message.deleted,chat.channel.created - Video (4):
video.session.created,video.session.started,video.session.ended,video.participant.joined - Live Streaming (2):
stream.session.started,stream.session.ended - Billing (4):
billing.quota.warning,billing.quota.exceeded,billing.payment.succeeded,billing.payment.failed
(Catalogue current as of this article's date — the live list is in the API reference.)
Step 1: Register your endpoint
Register in the Console — Webhooks — with three fields:
| Field | Value |
|---|---|
url | your HTTPS endpoint, e.g. https://your-app.com/hooks/hyperbabel |
events | one or more event types from the list above |
description | optional label so you can tell endpoints apart |
Two things to know before you save:
- The signing secret is shown exactly once at creation. Store it in your secret manager immediately — you cannot read it back, only rotate it.
- The URL must be public HTTPS. Private IPs, localhost and our own domains are rejected at registration to prevent request loops and internal-network probing. For local development, use a tunnel that gives you a public HTTPS hostname.
Step 2: Verify the HMAC-SHA256 signature
Every webhook delivery includes an X-HyperBabel-Signature header containing sha256=<hex>. Verify it before trusting the payload — otherwise an attacker can spoof events.
Express (Node.js):
import crypto from 'crypto';
import express from 'express';
const app = express();
const SIGNING_SECRET = process.env.HB_WEBHOOK_SECRET;
// CRITICAL: capture the raw body (not parsed JSON) for signature verification.
app.post('/hooks/hyperbabel', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-HyperBabel-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', SIGNING_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// Process event…
res.status(200).send('ok');
});Web-standard fetch handler (any edge or serverless runtime):
// No framework required — this is the Web Fetch API, so the same handler runs
// on any modern edge/serverless runtime.
export default {
async fetch(request: Request, env: { HB_WEBHOOK_SECRET: string }) {
const signature = request.headers.get('X-HyperBabel-Signature') ?? '';
const raw = await request.text();
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw', enc.encode(env.HB_WEBHOOK_SECRET),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(raw));
const expected = 'sha256=' + [...new Uint8Array(sig)]
.map(b => b.toString(16).padStart(2, '0')).join('');
if (signature !== expected) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(raw);
// Process event…
return new Response('ok');
},
};Two non-negotiables:
- Use the raw body, not parsed JSON. Parsing changes whitespace and breaks the signature.
- Use timing-safe comparison (
timingSafeEqualin Node; on runtimes without it, compare fixed-length hex strings — constant-time at this length).
Step 3: Handle at-least-once delivery (idempotency)
HyperBabel guarantees at-least-once delivery, not exactly-once. If your endpoint times out, returns 5xx, or drops the connection, the event is retried with exponential backoff — 3 attempts total, with roughly 1s / 2s / 4s between them, and a 5-second timeout per attempt. Sometimes a successful delivery is also retried because our acknowledgment was lost in flight.
This means your handler must be idempotent. Each event has stable identifiers you can dedup against:
| Event family | Idempotency key |
|---|---|
chat.message.* | data.message_id |
video.session.* · video.participant.* | data.session_id |
stream.session.* | data.session_id |
chat.channel.* | data.channel_id + event |
billing.* | event + timestamp |
billing.* | data.transaction_id or data.subscription_id + event |
Concrete pattern:
async function handleEvent(event) {
const idempotencyKey = `${event.event}:${event.data.message_id ?? event.data.session_id ?? event.id}`;
const inserted = await db.dedup.insertIfAbsent(idempotencyKey, { ttl: '7 days' });
if (!inserted) return; // already processed
await processEvent(event);
}The retry window itself is only seconds, but keep the dedup key for at least 7 days anyway — it also protects you from replaying the same event during an incident backfill. Redis SET NX, Postgres unique constraint, or DynamoDB conditional put — pick whichever your stack already uses.
Step 4: Always return 200 fast
Webhook handlers should:
- Return 200 OK in under 5 seconds — anything slower triggers a retry and your queue starts oscillating.
- Do as little as possible inline — verify signature, dedup, push to a queue, return.
- Long work (image processing, ML inference, downstream API calls) goes through your own job runner.
Inspecting deliveries
Every webhook attempt — successful or failed — is logged with status, response code, and latency. Pull them anytime:
Open Console — Webhooks — Delivery history. Useful when debugging signature mismatches or a 5xx storm during a deploy: you can see which attempt failed, what your endpoint returned, and how long it took.
What this unlocks
Once webhooks are wired:
- Audit log: every chat/video event timestamped in your warehouse for compliance review
- Real-time moderation: AI moderation pipeline reacts to
chat.message.createdin milliseconds - Billing visibility:
billing.quota.warningnotifies your finance team before overage hits the invoice - Custom analytics: dashboards backed by your data, not the vendor's
Postman collection ships with all webhook endpoints pre-configured — import, drop in your API key, and test the full lifecycle in 5 minutes.
Frequently asked questions
Why must I use the raw request body to verify the signature?
Parsing JSON normalises whitespace and key order, which changes the bytes the HMAC was computed over. Verify against the exact raw body first, then parse. This is the single most common cause of "signature mismatch" reports.
What happens if my endpoint is down during a deploy?
Delivery is at-least-once with exponential backoff — 3 attempts spaced roughly 1s / 2s / 4s apart, 5-second timeout each. The whole retry window is therefore seconds, not minutes: a rolling deploy that keeps one healthy instance is fine, but an outage longer than a few seconds will drop events, and you will need to backfill from the delivery history in the Console.
Can the same event arrive twice even though my endpoint returned 200?
Yes. If our acknowledgment is lost in flight, a successful delivery can still be retried. That is why the handler must be idempotent — dedup on the stable identifier for each event family and store the key for at least seven days.
Should I do the work inside the webhook handler?
No. Acknowledge fast and queue. Long processing inside the handler causes timeouts, which trigger retries, which cause duplicate processing — the failure loop feeds itself.
Related articles
One API Key, Six SDKs: Cross-Platform Chat & Video Without the Glue Code
Same API key drives chat, video, live, and translation across React, React Native, Swift, Kotlin, Flutter, and JavaScript. See the same Create Room call written in all six SDKs side-by-side — pick a stack and ship.
Real-time AI Translation, Built Into the Wire: How HyperBabel Translates 100+ Languages Without an Add-On
Most chat APIs treat translation as a third-party plugin you bolt on after the fact. HyperBabel runs translation in the message pipeline itself — same channel, same SDK, same API key. Here's how it works and why it matters for global apps.
Customer Auth + Firebase OAuth: A Practical Integration Guide
Stop embedding HyperBabel API keys in your mobile and web apps. Use the Customer Auth feature to issue per-end-user JWTs from your backend, with Firebase OAuth (Google + Apple) handling the sign-in flow. Step-by-step integration with code samples for React Native, Web, and iOS.
