Skip to main content
Webhooks let your code react to events from your WhatsApp numbers in real time. Every delivery is signed, retried, and recorded so you can debug from the dashboard.
The Webhooks dashboard lives at wassist.app/developers/webhooks. Create one there, copy the signing secret, and you’re done.

How a delivery works

Behind the scenes:
  1. The event is committed to our outbox as a WebhookDelivery with a stable UUID.
  2. A worker POSTs the JSON body to your URL.
  3. We record an attempt row with status code, duration, and response body.
  4. On 5xx or network errors we retry up to 3 times with exponential backoff (10s, 30s, 90s).
  5. After 20 consecutive failures the webhook is auto-disabled and you get a banner in the dashboard.

Headers

Verifying signatures

Compute HMAC-SHA256 over the string <timestamp>.<raw body> using the webhook’s signing secret and compare it in constant time to the v1 component.

Event catalog

message.received

Fired when a customer sends a message to one of your numbers.
Phone numbers are digits only, with no +. All IDs are Wassist IDs.
  • message.body holds the text, the media caption, or, for voice notes, a transcription.
  • message.replyTo is the Wassist ID of the message the customer quoted, or null. To quote the customer’s message in your reply, pass message.id as replyTo when you send a message.
  • message.media is a list of { "url", "mimeType", "filename" }. filename is set for documents and null otherwise.
  • Audio items also carry transcription and voice. transcription matches body: the transcript, "[unintelligible audio]" when nothing could be heard, or null if transcription failed. voice is true for a recorded voice note and false for an uploaded audio file.
Media is sent for images, voice notes and audio (converted to MP3, audio/mpeg), video, and documents. Voice note, video and document events are sent once the file has downloaded (and, for voice notes, been transcribed), so they can arrive a few seconds after the message.
The url is a pre-signed link that needs no API key and expires one hour after the message arrived. Download the file when you receive the event, or fetch a fresh link from GET /conversations/{id}/messages/.

Ad attribution (Click-to-WhatsApp)

When the customer starts the chat by tapping a Click-to-WhatsApp ad (or a boosted post), message.referral carries the ad details WhatsApp attached to that message. latestReferral has the same shape and holds the most recent referral in the conversation. That means later messages in an ad-driven chat can still be attributed to the ad.
ctwaClid is the Click-to-WhatsApp click ID. Send it back to Meta via the Conversions API for Business Messaging to attribute leads and purchases to the ad. sourceId is the Meta ad (or post) ID. You can also look these up later: they’re on referral for each message returned by GET /conversations/{id}/messages/, and on latestReferral for each conversation. To filter conversations, use hasReferral, referralSourceId or ctwaClid. See Ad Attribution for the full field reference and a Conversions API example.

message.status.updated

Fired once each time the delivery status of a message you sent changes. WhatsApp sometimes reports statuses twice or out of order. Those callbacks are dropped, so a status never goes backwards. Deduplicate on message.id plus message.status.
  • status is one of sent, delivered, read or failed.
  • failure is only present when status is failed. code is Meta’s error code.

message.reaction.updated

Fired when the customer adds, changes or removes a reaction. reaction.messageId is the Wassist ID of the message they reacted to, and emoji is null when the reaction was removed. Reactions don’t trigger message.received, subscription.message.received, or the agent.
Both events go to every active webhook in the organization that has the event ticked, whatever the conversation’s routing. To react from your own code, send a message with type: "reaction":
Send "emoji": null (or leave it out) to remove the reaction.

test.ping

Triggered by the dashboard Send test event button. Same envelope as a real event with a stub payload — use it to validate signature verification in CI.

Best practices

  • Idempotency. Store the X-Wassist-Delivery ID and reject duplicates. We use the same ID across all retries.
  • Respond fast. Send a 2xx within 10 seconds and do real work asynchronously. We treat slow responses as failures and retry.
  • Replay protection. Reject events where X-Wassist-Timestamp is more than 5 minutes from now.
  • Rotate secrets. From the dashboard, Rotate secret generates a new secret. The old one stops working immediately.
  • Local development. Use the CLI: wassist listen tunnels real events to localhost without exposing a public URL.

Replaying deliveries

Every delivery is stored with its full payload. From the dashboard you can:
  • Filter deliveries by status (all / failed / succeeded).
  • Open a delivery to see request headers, response body, and the timeline of every retry attempt.
  • Click Replay to send a fresh copy of the same payload. It gets a new X-Wassist-Delivery ID and is re-signed with a fresh timestamp, but the JSON body is identical, including its timestamp and any media URLs (which may have expired). Deduplicate replays on message.id.
You can also replay programmatically via POST /api/v1/webhook-deliveries/{id}/replay/.