wav1
DashboardOpenAPI document
WEBHOOKS

Events and payloads

wa pushes events to the URLs configured per device — three URLs, set on the device page. The two OTP events ride the status URL, since they are outcomes of a message you sent. Every request is a signed JSON POST.

Answer 2xx within ten secondsAnything else counts as a failure. wa then retries on a widening backoff — 1, 2, 4, 8, 16 and 32 minutes, then hourly — 29 retries after the first attempt, spanning a little over a day, after which the delivery is dead-lettered and stops. You can replay a dead delivery from the dashboard.

The events

FieldTypeDescription
message.incoming
Incoming URL

The device received a message. Carries the sender, the text, and media metadata — never the media bytes. Its inboxid is what you quote to reply.

message.status
Status URL

A message you sent moved to sent, delivered, read, played or failed. One delivery per transition — WhatsApp only reports a read receipt if the recipient shares them.

device.status
Device URL

The device changed connection state: connecting, qr, connected, disconnected, logged_out. Sent on a real change only, never on a QR refresh.

otp.verified
Status URL

A one-time password was verified. Carries the otp_id, the masked destination and the attempt count — never the code. You already know this synchronously from POST /v1/otp/verify; the event is for an audit consumer that is not the caller.

otp.expired
Status URL

A one-time password reached its expiry without being verified. Sent once, by the gateway rather than by anything you did, which is what makes it worth subscribing to. Note it does NOT fire when a code is spent by wrong guesses — you already learn that synchronously, as otp exhausted on the verify call.

Ignore events you do not handleNew event kinds are added over time, and the two OTP events share the status URL with message.status. Answer 2xx to anything you do not recognise and move on — refusing an unknown event fills your delivery log with dead rows for a day and changes nothing.

The envelope

Every delivery has the same four outer fields plus its data, which is what changes with the event.

json
{
  "id": "01JQ8T4Z2KX9V6WQ0N3B7C5D2E",
  "event": "message.status",
  "timestamp": 1772373732,
  "device": "628123456789",
  "data": {
    "device": "628123456789",
    "id": "01JQ8T4Z2K",
    "requestid": "01JQ8T4Z2KREQ",
    "status": "delivered",
    "sent_at": "2026-09-04T09:12:03.000Z",
    "delivered_at": "2026-09-04T09:12:07.000Z",
    "read_at": null
  }
}
FieldTypeDescription
id
string

The delivery id. Delivery is at-least-once, so this is your dedupe key — store it and ignore a repeat.

event
string

Which kind this is. Also sent as the X-Wa-Event header. One of message.incoming · message.status · device.status · otp.verified · otp.expired.

timestamp
integer

The event time in unix seconds — for an incoming message, WhatsApp's own message timestamp. It is part of the signature and identical on every retry, so never reject a delivery for being old.

device
string

The device's own phone number, or its id if it has never paired.

data
object

The payload for this event kind.

Fields are additive: new keys may appear without a version bump, and existing keys never change meaning. Verify the signature before you trust any of it — how to do that.