Event webhooks, signed
Seven events, one endpoint per account, and every delivery is signed like Stripe's. Each payload names the number it happened on, so one URL serves however many numbers you run.
Register once
import { Whatsapi } from "@whatsapi.sh/sdk";
const wa = new Whatsapi({ apiKey: "wa_your_key" });
const data = await wa.setWebhook({ url: "https://api.yourapp.com/webhooks/whatsapp" });{
"data": {
"secret": "whsec_…", // shown ONCE, store it
"record": { "url": "…", "events": ["message.created", "…"], "enabled": true }
}
}Events: message.created, message.sent, message.failed, message.status, number.connected, number.disconnected, number.logged_out, number.history_synced. The two middle ones are the outcome of a message that waited in the send queue. The last one fires once per pairing, when the phone has sent all its old chats or has been quiet for 10 minutes. To rotate the secret, save the endpoint with "rotateSecret": true.
Verify the signature
Every delivery carries x-whatsapi-signature: t=<unix>,v1=<hmac>, an HMAC-SHA256 of <t>.<body> with your secret. Check it before you trust anything:
import { verifyWebhook, WebhookSignatureError } from "@whatsapi.sh/sdk";
// Signature, replay window and constant-time compare, in one call. Anything
// that does not check out throws WebhookSignatureError. Answer 400.
const event = await verifyWebhook({
payload: rawBody, // the raw bytes, not the parsed body
signature: req.header("x-whatsapi-signature"),
secret: process.env.WHATSAPI_WEBHOOK_SECRET,
});Expect retries and dedupe
Each event gets up to 5 delivery attempts. We retry a lost answer, a 5xx, or a 408, 425 or 429, waiting 30, 60, 120 and 240 seconds between attempts. A longer Retry-After extends the wait, up to 1 hour per retry. Other 4xx responses and redirects fail the delivery without a retry. Register the final URL, not a redirect. Retries keep the same x-whatsapi-delivery id, so use it to avoid processing an event twice. After 10 failed deliveries in a row, we disable the endpoint. Attempts still being retried do not count toward that limit. Save the fixed endpoint or send a successful test to enable it again. POST /v1/webhooks/test sends one test and tells you what your endpoint answered.
Read a chat's history
If your endpoint was down, or you want context before you reply, read the chat back. You get its latest messages, newest first, each in the same shape as message.created. A number keeps the last 100 messages of every chat unless you set historyPerChat with PATCH /v1/numbers/:id. 0 keeps none.
import { Whatsapi } from "@whatsapi.sh/sdk";
const wa = new Whatsapi({ apiKey: "wa_your_key" });
const data = await wa.getChatHistory({ chat: "+5511987654321" });Don't know which chats there are yet? GET /v1/chats lists the chats a number kept, most recent first. Then read each one as above.
import { Whatsapi } from "@whatsapi.sh/sdk";
const wa = new Whatsapi({ apiKey: "wa_your_key" });
const data = await wa.listChats();