Webhook
Set your Webhook, read the events we send, check their signature, and handle retries.
We send your events by POST to one URL on your server: your Webhook. You can set it in the Dashboard (see Webhook) or with the API.
Set the Webhook
PUT /v1/webhooks| Body field | Required | Meaning |
|---|---|---|
url | yes | Your http or https address that receives events. |
secret | yes | Your signing secret: a long random text. |
Setting it again replaces the old one.
curl -X PUT https://api.wa.goupdev.net/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-site.com/whatsapp/events", "secret": "a-long-random-secret"}'Answer 200: the Webhook, with id, url, secret and active.
| Error | Why |
|---|---|
400 {"error": "url and secret are required"} | url or secret is missing. |
Read the Webhook
GET /v1/webhooksAnswer 200: { "id", "url", "secret", "active" }, or null when no Webhook is set.
Events
Every event is a JSON object with an event field that names it.
message.received
Someone sent a Message to one of your Devices.
{
"event": "message.received",
"deviceId": 7,
"from": "9665XXXXXXXX@s.whatsapp.net",
"text": "Hello, is my order ready?",
"providerMessageId": "3EB0C767D26A1D8B4F21"
}| Field | Meaning |
|---|---|
deviceId | The Device that received it. |
from | The sender's WhatsApp address. The digits before @ are their number. |
text | The text, or null for Messages without text (images, audio, stickers...). |
providerMessageId | WhatsApp's id for this Message. Use it to ignore an event you already handled. |
message.status_changed
A Message you sent was delivered or read.
{
"event": "message.status_changed",
"deviceId": 7,
"messageId": 1042,
"status": "delivered"
}messageId is the id you got when you sent the Message. status is delivered or read.
device.status_changed
A Device was logged out of WhatsApp. It needs a new QR scan before it can send again.
{
"event": "device.status_changed",
"deviceId": 7,
"status": "logged_out"
}Check the signature
Every event has an X-Signature header: the HMAC-SHA256 of the raw request body, using your signing secret, written in hex. Compute the same value on your side and compare. If they differ, ignore the request: it didn't come from us.
Use the raw body exactly as received, before parsing the JSON.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/whatsapp/events', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(req.body).digest('hex');
const received = req.get('X-Signature') ?? '';
const valid =
received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// Handle the event (quickly), then answer 200.
res.sendStatus(200);
});Retries
- Answer with any
2xxstatus to confirm you received the event. - If your server answers anything else, or can't be reached, we try again: up to 5 attempts in total, waiting longer each time (about 10 seconds, then 20, 40 and 80).
- After the last attempt, that event is dropped.
So:
- Answer fast. Save the event and answer
200at once. Do slow work afterwards. - Expect duplicates. A retry can deliver an event you already handled. Use
providerMessageId, ormessageIdwithstatus, to skip it. - Don't rely on order. Events can arrive in a different order from the one they happened in.