message.status
An outgoing message moved to sent, delivered, read, failed or unknown. Carries
your reference. Statuses only move forward - ignore updates that would move one back.
unknown means Meta's answer to the send was lost (timeout or server error): it may or
may not have reached the customer, and it is never sent again automatically. If Meta
did send it, a later sent/delivered/read arrives for the same message.
pricing is what Meta reports it charges for the message; Meta bills the business's
WhatsApp Business Account directly, never PaalChat.
Verify every callback
Check X-PaalChat-Signature on the raw body, reject timestamps older than 5 minutes, and de-duplicate on X-PaalChat-Delivery. Answer 2xx within 10 seconds. See Webhooks and callbacks.
Headers
X-PaalChat-Event
string
X-PaalChat-Delivery
string
Unique per event (equals body id). De-duplicate on it.
Format uuid.
X-PaalChat-Signature
string
t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. Reject if invalid or older than 300 seconds.
Pattern ^t=\d+,v1=[a-f0-9]{64}$.
Body
id
string
Equals X-PaalChat-Delivery.
Format uuid.
sandbox
boolean
true for sandbox businesses (test keys) - nothing reached WhatsApp.
event
any
Always message.status.
occurred_at
string
Format date-time.
business
object
external_id
string
business.external_id
data
object
message_id
integer
data.message_id
channel
string
data.channel
One of: whatsapp, sms, email.
reference
string | null
data.reference
wamid
string | null
data.wamid
The provider's message id (Meta's wamid; Arkesel's, Hubtel's or Resend's id for SMS and email).
contact_id
integer | null
data.contact_id
conversation_id
integer | null
data.conversation_id
to
string
data.to
phone_number_id
string | null
data.phone_number_id
status
string
data.status
One of: sent, delivered, read, failed, unknown.
previous_status
string
data.previous_status
Outgoing moves forward only (queued < sent < delivered < read); failed is final and never follows read. cancelled: a scheduled message withdrawn before it went. unknown follows queued when Meta's answer to the send was lost - it may still move to sent, delivered, read or failed if Meta reports the message.
One of: queued, unknown, sent, delivered, read, failed, received, cancelled.
error
object | null
data.error
code
string
data.error.code
Meta's error code (e.g. 131026) or a platform code (connection_unavailable, no_message_id, circuit_open when Meta was failing for longer than the send's retries; for unknown: http_5xx or no_response).
message
string
data.error.message
pricing
object | null
data.pricing
Meta's pricing for the message, from its status webhooks (null until Meta reports it, and for incoming messages). Meta bills the business's WhatsApp Business Account directly; PaalChat never charges for Meta messages. From 2026-10-01 replies inside the 24-hour window are billable too.
billable
boolean | null
data.pricing.billable
Whether Meta charges for this message.
category
string | null
data.pricing.category
Meta's pricing category.
Example marketing.
type
string | null
data.pricing.type
regular (charged), free_customer_service or free_entry_point.
Example regular.
model
string | null
data.pricing.model
Meta's pricing model: PMP (per message).
Example PMP.
sent_at
string | null
data.sent_at
Format date-time.
delivered_at
string | null
data.delivered_at
Format date-time.
read_at
string | null
data.read_at
Format date-time.
failed_at
string | null
data.failed_at
Format date-time.