Skip to main content

Configure webhooks

Manage the full webhook lifecycle via the public API — or use the dashboard Webhooks page (/webhooks). A webhook is configured per number; once configured, events are delivered automatically (no API-key linking is required).
Pilot Status Webhooks page

The Webhooks page — create, edit, deactivate, or delete a webhook and inspect its delivery events.

For payloads and the full event list, see the event reference.

Endpoints

Number scoping

A webhook belongs to a single WhatsApp number.
  • With a number-scoped key the number comes from the key itself; a whatsappNumberId in the body must match it (else 400 NUMBER_MISMATCH).
  • With a tenant-scoped key, pass the target number via the whatsappNumberId body field or the x-whatsapp-number-id header — omitting both returns 400 NUMBER_REQUIRED; a number outside your tenant returns 404 NUMBER_NOT_FOUND.
  • GET /v1/webhooks lists only the webhooks visible to the key’s scope (a number-scoped key sees only its own number’s webhooks). A webhook outside the key’s scope returns 404 WEBHOOK_NOT_FOUND.

Examples

Reading delivery logs

GET /v1/webhooks/{id}/logs returns the recent attempts, newest first. Two fields together tell you where an attempt died: A statusCode of null is the case worth reading closely: nothing reached your server, so no status exists. response then carries the chain of causes, outermost first, joined by <- and tagged with the system error code:
The codes that show up most often:
Treat response as a diagnostic string, not a contract. The wording comes from the runtime and can change between releases; branch on status and statusCode, and read response when you need to know why.
A failed attempt is retried with exponential backoff before it is given up on, so a single FAILED row does not mean the event was lost — look for a later SUCCESS on the same event.

Event gating

An empty (or omitted) events list dispatches NOTHING — there is no implicit “subscribe to all”. Use "*" to subscribe to every event.
  • On PATCH, events replaces the whole subscription list (it is not a merge).
  • Events the number’s provider cannot emit are silently dropped.

Rules and errors

The signing secret is never returned by any of these endpoints.

How many webhooks per number

A number can carry more than one webhook. Scoping is many-to-many: every active webhook subscribed to the number receives the events it subscribed to, and a second one does not replace the first. Use that to split concerns — one endpoint for conversations, another for health and monitoring. The single refusal is an exact duplicate: the same url on the same number — 409 WEBHOOK_DUPLICATE. There is no legitimate reason for two, and the integration that created instead of updating used to pile up rows in silence; it now gets a conflict. A workspace-wide cap of 50 webhooks also applies (409 WEBHOOK_LIMIT_REACHED) — deliberately loose, since a tenant with many numbers may want one per number.
Two endpoints on the same number

Verify the signature

Every webhook created from 2026-09-15 onward is signed by birth. Creation mints a secret and returns it in clear on that 201 — the only response in the life of a secret that carries it. Nothing reads it back.
Webhooks created before that date have no secret, and stay that way. Their deliveries still go out unsigned. That is deliberate: switching signature verification on underneath a live integration is its own outage. POST /v1/webhooks/{id}/rotate-secret is how you turn it on for one of them — and the only way to recover a secret that was lost, since no read returns it.Read hasSecret on any webhook to tell the two apart before you decide whether your receiver can reject unsigned deliveries.

Rotate — or mint one for an older webhook

Rotation is immediate and has no overlap window. The previous secret stops verifying the moment the call returns; deliveries already in flight sign with whichever value their dispatch loaded. Accept either secret on your side for a minute, or rotate while the number is quiet.
When a secret is set, the header x-pilot-status-signature carries the hex-encoded HMAC-SHA256 of the raw request body, keyed with the secret. Compute the HMAC over the raw bytes exactly as received (before any JSON parsing) and compare in constant time:
Node

Deduplicate retries: Idempotency-Key

When it is present, Idempotency-Key is stable per event: a retry, a backoff replay and a failover replay of the same event all carry the same value, so it is the right key to dedupe on. The same value is also merged into the delivered JSON body as a top-level idempotencyKey field, so you can read it from either place.
The header is not on every delivery. It is derived from a message id found in the payload (messageId / internalMessageId, at the top level or under data). A payload with no such id gets no header and no body field — that includes:
  • every Meta Cloud API native envelope (messages, smb_message_echoes, message_echoes, the template and account fields …), whose top level is { object, entry },
  • the number.* lifecycle and number-health events.
So a Meta number’s conversation traffic is not covered by this header today. Dedupe those on Meta’s own ids instead — and read the wamid warning in the event reference before keying on the wamid alone.

Dashboard

Everything above is also available in the dashboard at /webhooks: create/edit webhooks per number, pick events, pause/resume, and inspect delivery logs. The New/Edit Webhook UI includes the notice that message.read only fires when the recipient has WhatsApp read receipts enabled.