Messaging — WhatsApp
Voxa integrates with Meta’s WhatsApp Business Cloud API (Graph API v21.0). If you haven’t connected a number yet, start with Connecting channels.
This page is the reference for what actually happens once the connection is live.
Inbound
Section titled “Inbound”Every inbound event from Meta is a POST to
https://voxa.software/webhooks/whatsapp/<token> containing one or
more of:
- Messages — text, image, video, audio, document, interactive (button clicks, list selections), location, or contacts.
- Statuses — delivery receipts for outbound messages you
previously sent (
sent→delivered→read, orfailed).
Voxa’s flow:
- Receive POST, read the raw body.
- Compute
HMAC-SHA256(body, app_secret)and compare againstX-Hub-Signature-256usingtimingSafeEqual. Mismatch ⇒ 403. - Record the event in
webhook_events(signature status, raw JSON). - Enqueue a
webhook-inboundjob with the payload. - Return 200 in under 50 ms. Meta’s webhook timeouts are tight — we never process inline.
The webhook-inbound worker then:
- Extracts
wa_idand the message. - Creates or updates the contact in
contacts. Setslast_seen_at = now. - Writes the message to
messageswithdirection: inbound. - Finds the contact’s active conversation (if any) and, if it’s
awaiting input on this channel, enqueues a
flow-executionjob. - Updates delivery-status rows for any
statusesin the payload.
Total inbound-to-first-outbound latency in normal conditions is ~200–500 ms end-to-end.
Outbound
Section titled “Outbound”Voxa sends outbound messages by calling
POST https://graph.facebook.com/v21.0/<phone_number_id>/messages
with the tenant’s access_token. The wrapper lives in
packages/whatsapp/ and is called sendTextMessage().
Supported message types today:
- Text — free-form
bodyup to 4096 chars. - Interactive buttons and lists — via the interactive message shape defined by Meta.
Meta returns a message_id on success; Voxa stores it and tracks
statuses as they come back through the webhook.
Templates
Section titled “Templates”Templates are the only way to send outbound WhatsApp messages outside the 24-hour customer-care window. Voxa has:
- A
whatsapp_templatestable withmeta_template_id,name,language,category,status,components,synced_at. - A template list view in the dashboard (read-only).
Voxa does not yet:
- Submit templates to Meta for approval on your behalf.
- Send template messages from a flow node.
When template sending ships, this page will cover component interpolation, language fallbacks, and approval workflow.
The 24-hour customer-care window
Section titled “The 24-hour customer-care window”Meta’s policy:
- When a contact sends you a message, a 24-hour window opens.
- Inside the window, you can reply with free-form text, media, or interactive content.
- Outside the window, Meta requires an approved template.
A contact’s reply resets the window. If the window closes and you try
to send free-form, Meta responds with error 131047 (message outside
window) and the message status is failed.
Voxa surfaces this failure in the messages table (status=failed,
failure_reason=...) and in the conversation timeline, but does not
currently block the send client-side.
Rate limits
Section titled “Rate limits”Two layers of rate limiting apply:
- Meta’s per-number limit — tied to your number’s quality rating and messaging tier. Starts at ~250 business-initiated conversations per 24 hours and grows based on delivery success. See Meta’s docs for the current tier thresholds.
- Voxa’s queue concurrency — the flow-execution worker runs 10 concurrent jobs by default. A heavy burst queues behind this; it’s invisible to the contact but keeps us inside Meta’s throughput limits.
If you’re bumping against either limit, ask us to tune it —
hello@voxa.software.
Media upload / download
Section titled “Media upload / download”Inbound media — Meta sends a media_id in the webhook. Voxa
downloads the media on demand (not eagerly), uploads to Cloudflare
R2, and stores the R2 path in the message + the entry’s media_refs.
Outbound media — not yet exposed in flow builder. The API-level
wrapper exists in packages/whatsapp/ but there’s no UI node type
to attach a file. This is on the roadmap.
Error handling
Section titled “Error handling”| HTTP status (from Meta) | What it usually means | Voxa’s behaviour |
|---|---|---|
| 200 | Send accepted, message_id returned. | Message row written. |
| 401 | Access token invalid or revoked. | Message marked failed, owner-level notification fires. |
| 403 | Permission denied (often missing asset access). | Same as 401. |
| 400, code 131047 | Outside 24h window. | failed + policy note. |
| 400, code 132012 | Template send outside an approved template. | failed. |
| 429 | Rate-limit hit. | Job retried with backoff by BullMQ. |
| 5xx | Meta side failure. | Job retried with backoff. |
All failures land in messages.failure_reason and the audit log.