whathooks documentation
Connect a WhatsApp number, receive inbound messages on your webhook, and send replies through our REST API.
🤖 Building with an AI coding agent? Give it the whole API in one markdown file: whathooks.app/llms.txt
Introduction
whathooks lets your clients connect their own WhatsApp number by scanning a QR code. Once linked, the integration works in two directions:
- Inbound → webhook. Every message your number receives is POSTed as JSON to a webhook URL you configure.
- Outbound → API. You send replies by calling our REST API at
/v1.
Your number is connected through a WhatsApp Web link, the same mechanism as WhatsApp's “linked devices”. Reconnection after a dropped link is handled automatically, and each number supports one linked device.
Quickstart
Get from zero to a sent message in four steps.
1. Create an account & organization
Sign up and create an organization. All sessions, webhooks, and API keys live under your organization.
2. Create a WhatsApp session & scan the QR
Create a session in the dashboard, then open WhatsApp on your phone →
Linked Devices→Link a Deviceand scan the QR.3. Add a webhook URL
Register an HTTPS endpoint to receive
message.receivedand session events.4. Create an API key
Generate an API key to send messages programmatically via
POST /v1/messages.
Connecting a number
A session moves through a series of statuses as it links and stays online. You can poll a session or subscribe to session.status webhooks to track it.
- PENDINGSession created, not yet initialized.
- QRA QR code is ready to be scanned with WhatsApp.
- CONNECTINGPairing accepted, establishing the connection.
- CONNECTEDOnline and ready to send and receive messages.
- DISCONNECTEDConnection dropped; reconnection is attempted automatically.
- LOGGED_OUTThe device was unlinked; a new QR scan is required.
Embed the QR in your product
Sessions are fully manageable with an API key, so your product can onboard users without ever showing them the whathooks dashboard: create a session, then render the pairing QR in your own UI. GET /v1/sessions/:id returns qrDataUrl. A ready-to-embed PNG data URI (<img src={qrDataUrl} />). QR codes rotate, so poll the session while it's in the QR status or subscribe to session.qr webhooks for fresh codes, and watch for CONNECTED to dismiss the QR.
# 1. Create a session (starts pairing, a QR is generated)
curl -X POST https://api.whathooks.com/v1/sessions \
-H "X-API-Key: wh_live_..." \
-H "Content-Type: application/json" \
-d '{ "label": "customer-42" }'
# → { "id": "sess_8f2k19a7", "status": "PENDING", ... }
# 2. Poll until status is "QR", then embed the code in your UI
curl https://api.whathooks.com/v1/sessions/sess_8f2k19a7 \
-H "X-API-Key: wh_live_..."
# → { "status": "QR", "qrDataUrl": "data:image/png;base64,...", ... }
# 3. Your user scans it with WhatsApp → status becomes "CONNECTED"Webhooks
When something happens on a session, we POST a JSON body to your configured webhook URL.
Events
| Event | Description |
|---|---|
message.received | An inbound WhatsApp message arrived on a connected session. |
session.status | A session changed status (e.g. CONNECTED, DISCONNECTED, LOGGED_OUT). |
session.qr | A new QR code was generated and is waiting to be scanned. |
contact.created | A new contact was saved. Via the session's auto-save setting, a flow's "Save contact" node, or manual creation. |
contact.updated | An existing contact changed: fields edited, name backfilled from WhatsApp, or a new session linked. |
Delivery headers
Every delivery includes these request headers:
| Header | Description |
|---|---|
X-Whathooks-Event | The event type, e.g. message.received. |
X-Whathooks-Signature | HMAC-SHA256 of the raw body, formatted as sha256=<hex>. |
X-Whathooks-Delivery | A unique ID for this delivery attempt. |
Payload envelope
Every delivery body shares the same envelope: { event, sessionId, data, timestamp }. Below is a full message.received delivery:
{
"event": "message.received",
"sessionId": "sess_8f2a...",
"data": {
"id": "msg_3c91...",
"conversationId": "conv_9d41...",
"sessionId": "sess_8f2a...",
"from": "15551234567",
"isGroup": false,
"participant": null,
"mentionedMe": false,
"pushName": "Jane Doe",
"type": "TEXT",
"text": "Hi there!",
"media": null,
"waMessageId": "3EB0A1B2C3D4E5F6",
"timestamp": 1784892795
},
"timestamp": "2026-07-24T11:33:15.401Z"
}type is uppercase (TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT, STICKER, LOCATION, CONTACT, UNKNOWN). data.timestamp is unix seconds (the WhatsApp message time); the envelope timestamp is the ISO delivery time.
For media messages, media carries a signed download link (expires after 1 hour. Download promptly, or re-fetch the message via GET /v1/messages for a fresh link). text holds the caption, if any:
"type": "IMAGE",
"text": "the caption, if any",
"media": {
"url": "https://api.whathooks.app/v1/media/raw?key=...&exp=...&sig=...",
"mimeType": "image/jpeg",
"fileName": "photo.jpg"
}Customize the payload
Optionally give a webhook mapping rules to reshape data to match your system: rename fields, format dates, and inject fixed values. When rules are set, data contains only the fields you map. The envelope stays the same. Each rule has a target (output name) and either a source (dot path like data.from, also reaching event, sessionId, timestamp) or a fixed value. Add dateFormat to a source rule to format it as a date: iso, unix, unix_ms, or a UTC pattern using yyyy MM dd HH mm ss. Configure it on the webhook in the dashboard, or pass payloadMapping when creating one via API.
// Mapping rules on the webhook:
[
{ "target": "phone", "source": "data.from" },
{ "target": "message", "source": "data.text" },
{ "target": "receivedAt", "source": "data.timestamp",
"dateFormat": "yyyy-MM-dd HH:mm" },
{ "target": "origin", "value": "whathooks" }
]
// What your endpoint receives:
{
"event": "message.received",
"sessionId": "sess_8f2k19a7",
"data": {
"phone": "15551234567",
"message": "Hi! Is my order shipped yet?",
"receivedAt": "2026-07-14 19:30",
"origin": "whathooks"
},
"timestamp": "2026-07-14T22:30:00.000Z"
}Signature verification
Verify each delivery by computing an HMAC-SHA256 of the raw request body using your webhook secret, then comparing it to the X-Whathooks-Signature header. Always compare with a constant-time function.
import crypto from "crypto";
// Mount with the raw body, e.g. express.raw({ type: "application/json" })
function verifySignature(rawBody, header, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Header arrives as "sha256=<hex>" — strip the prefix first
const received = (header || "").replace(/^sha256=/, "");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Sending messages
Send a message with POST /v1/messages, authenticated with an API key via the X-API-Key: <token> header. An Authorization: Bearer <token> header also works.
The to field accepts a bare phone number (digits only, with country code, no +) or a full WhatsApp JID. The target session must be CONNECTED or the request returns 400.
curl -X POST https://api.whathooks.com/v1/messages \
-H "X-API-Key: wh_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "sess_8f2a...",
"to": "15551234567",
"text": "Hello"
}'A successful request responds with the queued message:
{
"id": "msg_5d72...",
"waMessageId": "3EB0F1E2D3C4B5A6",
"sessionId": "sess_8f2a...",
"to": "15551234567",
"status": "sent"
}API reference
Base URL in production: https://api.whathooks.com/v1. Dashboard requests use a JWT (from /v1/auth/login), while programmatic requests use an API key.
| Method | Path | Auth |
|---|---|---|
| POST | /v1/auth/register | Public |
| POST | /v1/auth/login | Public |
| GET | /v1/sessions | JWT / API key |
| POST | /v1/sessions | JWT / API key |
| GET | /v1/sessions/:id | JWT / API key |
| POST | /v1/sessions/:id/logout | JWT / API key |
| GET | /v1/webhooks | JWT / API key |
| POST | /v1/webhooks | JWT / API key |
| POST | /v1/messages | API key |
| GET | /v1/messages | JWT / API key |
Errors & limits
- 401Missing or invalid credentials (JWT or API key).
- 400The session is not
CONNECTED, or the request body is invalid. - WebhooksDelivery requests time out after 10 seconds. For now we make a single delivery attempt per event. Automatic retries are coming.