Webhooks and verifying signatures
Wagent posts events to your HTTPS endpoint as they happen, signed so you can prove they came from Wagent.
Endpoints#
Add a webhook on the Developers screen. Each endpoint belongs to one agent, so message events follow that agent's rights: an event about a chat the agent cannot see is never sent. Adding one needs the owner's WhatsApp code.
The URL must be https on a public address. Wagent follows no redirects and reads at most a few kilobytes of your answer. The signing secret (whsec_…) is shown once; Rotate signs with both the old and new secret for 24 hours.
What a delivery looks like#
A POST with a JSON body { id, type, created_at, workspace_id, env, data } and three Standard Webhooks headers:
| Header | Value |
|---|---|
webhook-id | The event id, evt_ then a UUID. The same on every retry and replay: use it to skip duplicates. |
webhook-timestamp | Unix seconds when this attempt was signed. |
webhook-signature | Space-separated v1,<base64> HMAC-SHA256 signatures of {webhook-id}.{webhook-timestamp}.{raw body}. |
Verify each event#
Check the signature over the raw body before parsing it, refuse timestamps more than 5 minutes from your clock, answer 2xx quickly and skip ids you have already handled. This is the function Wagent tests its own signatures with:
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
export function verify(secret: string, h: Record<string, string>, raw: string) {
const id = h['webhook-id'], ts = Number(h['webhook-timestamp']);
if (!id || !Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const want = createHmac('sha256', Buffer.from(secret.slice(6), 'base64')).update(`${id}.${ts}.${raw}`).digest();
return (h['webhook-signature'] ?? '').split(' ').some((p) => {
const [v, sig] = p.split(',');
const got = Buffer.from(sig ?? '', 'base64');
return v === 'v1' && got.length === want.length && timingSafeEqual(got, want);
});
}
const seen = new Set(); // keep handled event ids in your database in production
createServer((req, res) => {
let raw = '';
req.setEncoding('utf8');
req.on('data', (chunk) => (raw += chunk));
req.on('end', () => {
if (!verify(process.env.WAGENT_WEBHOOK_SECRET, req.headers, raw)) return void res.writeHead(401).end();
res.writeHead(200).end(); // answer within 15 seconds, then do the work
const id = req.headers['webhook-id'];
if (seen.has(id)) return; // a retry or replay of an event you already handled
seen.add(id);
const event = JSON.parse(raw); // { id, type, created_at, workspace_id, env, data }
});
}).listen(3000);Retries#
- Any 2xx within 15 seconds marks the event delivered. Anything else, a redirect or a timeout is a failure.
- Wagent tries 6 times over 24 hours with the same event id: at once, then about 1 minute, 10 minutes, 1 hour, 6 hours and 24 hours later.
- Keep events until delivered (on by default): after the sixth failure the event stays queued and is tried every 6 hours until it is delivered or 30 days old. Turned off, it is marked failed.
- A 410 answer turns the endpoint off. A 429 pauses the whole endpoint for its Retry-After (at most an hour).
- You can replay any event from the Developers screen or with
POST /v1/events/{id}/replay. A replay delivers the same event id again; it never repeats the action behind it.
List your endpoint's deliveries with GET /v1/events. The list has ids, types, statuses and tries, never message text.