Skip to content

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.

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 (sentdeliveredread, or failed).

Voxa’s flow:

  1. Receive POST, read the raw body.
  2. Compute HMAC-SHA256(body, app_secret) and compare against X-Hub-Signature-256 using timingSafeEqual. Mismatch ⇒ 403.
  3. Record the event in webhook_events (signature status, raw JSON).
  4. Enqueue a webhook-inbound job with the payload.
  5. Return 200 in under 50 ms. Meta’s webhook timeouts are tight — we never process inline.

The webhook-inbound worker then:

  1. Extracts wa_id and the message.
  2. Creates or updates the contact in contacts. Sets last_seen_at = now.
  3. Writes the message to messages with direction: inbound.
  4. Finds the contact’s active conversation (if any) and, if it’s awaiting input on this channel, enqueues a flow-execution job.
  5. Updates delivery-status rows for any statuses in the payload.

Total inbound-to-first-outbound latency in normal conditions is ~200–500 ms end-to-end.

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 body up 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 are the only way to send outbound WhatsApp messages outside the 24-hour customer-care window. Voxa has:

  • A whatsapp_templates table with meta_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.

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.

Two layers of rate limiting apply:

  1. 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.
  2. 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.

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.

HTTP status (from Meta)What it usually meansVoxa’s behaviour
200Send accepted, message_id returned.Message row written.
401Access token invalid or revoked.Message marked failed, owner-level notification fires.
403Permission denied (often missing asset access).Same as 401.
400, code 131047Outside 24h window.failed + policy note.
400, code 132012Template send outside an approved template.failed.
429Rate-limit hit.Job retried with backoff by BullMQ.
5xxMeta side failure.Job retried with backoff.

All failures land in messages.failure_reason and the audit log.