Meta webhook handling: X-Hub-Signature-256 payload verification (HMAC-SHA256 over the raw body with your App Secret), the GET subscription challenge, and helpers that pull messages and delivery statuses out of a notification payload.

Functions

change-values

fn (payload: Any): Vec

The value objects of every messages-field change in a notification payload (entry[].changes[].value). Exposed for testing.

extract-messages

fn (payload: Any): Vec

The inbound messages in a webhook notification payload, flattened across entries and changes. Each message is Meta's message object (from, id, timestamp, type, and the type's object such as text.body, image.id, interactive.button_reply, location) plus phone_number_id (the business number that received it) and contact (the sender's {profile: {name}, wa_id} entry, when present). Returns [] for payloads without messages, such as status updates.

Example

messages ::whatsapp::webhooks/extract-messages(request.body)
map(messages, (m) {
  if(eq(m.type, "text"), println(`${m.from}: ${m.text.body}`), null)
})

extract-statuses

fn (payload: Any): Vec

The outbound message status updates in a webhook notification payload (sent, delivered, read, failed), flattened across entries and changes, each with phone_number_id added. Each status carries id (the wamid you sent), status, timestamp, recipient_id, and errors on failure.

verify-challenge

fn (query: Map): Str
fn (query: Map, verify-token: Str): Str

Verify a webhook subscription challenge. When you configure a webhook URL in Meta's App Dashboard, Meta sends a GET request with hub.mode, hub.verify_token, and hub.challenge query parameters. Returns the hub.challenge value when the mode is subscribe and the token matches (constant-time comparison), otherwise an Err.

Uses the whatsapp.verify.token context variable by default (failing when it is unset), or accepts an explicit token; an empty explicit token never matches.

Example

::webhooks ::whatsapp::webhooks

// In a webhook handler for GET requests:
result ::webhooks/verify-challenge(request.query)
// Returns the challenge string on success, or an Err on failure:
// reply 200 with the challenge as the plain-text body, else 403.
Context Vars: whatsapp.verify.token

verify-request

fn (request: Map): Bool
fn (request: Map, app-secret: Str): Bool

Verify a WhatsApp webhook request using its HMAC-SHA256 signature. Meta signs the raw payload with your App Secret and sends X-Hub-Signature-256: sha256={hex}. The digest is computed over ::hot::http/raw-body(request) and compared in constant time.

Uses the whatsapp.app.secret context variable by default (failing when it is unset), or accepts an explicit secret; an empty explicit secret never verifies. Meta does not sign a timestamp, so there is no replay window: deduplicate on message id.

Example

::webhooks ::whatsapp::webhooks

// Using context variable (1-arity)
is-valid ::webhooks/verify-request(request)

// With explicit secret (2-arity)
is-valid ::webhooks/verify-request(request, "my-app-secret")
Context Vars: whatsapp.app.secret