Sending a WhatsApp message from code is, at the core, a single HTTP POST. The complexity isn't the request — it's the rules around it. This guide covers both: the call itself, and the things that make it actually deliver in production. The examples use walpio's API; the full reference is in the API docs.

The core request

Every WhatsApp send is a POST with a bearer key and a JSON body naming the recipient and the content. walpio's send body follows the Cloud API's text and template shapes:

curl -X POST https://api.walpio.com/v1/messages \
  -H "Authorization: Bearer wp_live_····" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-42-out-for-delivery" \
  -d '{
    "to": "+9715XXXXXXXX",
    "type": "text",
    "text": { "body": "Your order #42 is out for delivery today." }
  }'

A 200 means Meta accepted the message; the answer carries Meta's message id and walpio's own record:

{
  "messaging_product": "whatsapp",
  "messages": [{ "id": "wamid.HBgM…" }],
  "walpio": { "id": "7b1c…", "via": "text", "status": "accepted" }
}

If Meta is slow or briefly unavailable, walpio may answer 202 and keep trying. Until Meta accepts the message, messages[0].id holds walpio's own id, so match later statuses on walpio.id, which every answer and callback carries.

The part that bites: the 24-hour window

That plain-text send only works if the customer messaged you in the last 24 hours. Outside that window, WhatsApp requires an approved template. If you build directly against Meta, your code has to track each customer's last-inbound time, decide per send whether to go free-form or template, and build the template payload. Get it wrong and messages fail.

With walpio you can send a template by name — or keep sending your own wording and add a text rule for it. A text rule maps a message like “Your order {{order}} is out for delivery today.” to an approved template. Outside the window, walpio sends that template with the values filled in and tells you so with "via": "template" in the response. If no rule matches, you get 422 no_matching_template and nothing is sent — never a silent drop.

Retry without double-sending

Networks fail, and a send you retry must not reach the customer twice. Send an Idempotency-Key header with every message: repeating a request with the same key replays the first answer instead of sending again.

Handle errors by what they say

Every walpio error carries a machine code, a plain message and a retryable flag. Retry only when retryable is true — for example a 429 rate_limited (wait for its Retry-After) or a 502 when Meta is unavailable. A 409 means the same key is still in flight; a 422 means fix the request, or the recipient can't get this message (opted out, not on WhatsApp, no matching template).

Know what happened: delivery status

A 200 means “accepted,” not “read.” WhatsApp reports the real journey — sent, delivered, read, failed — asynchronously via webhooks. See delivery statuses and webhooks for how that works. walpio records every status per message: read it with GET /v1/messages/{id}, watch it in the dashboard, or receive it as a signed message.status callback on your own URL.

Build vs buy

You can build all of this — the window tracking, template routing, webhook signature verification, status history, rate limiting, safe retries — against Meta directly. Or you can POST to one endpoint and get it handled. That's the trade walpio offers: your own Meta number, a send body in the Cloud API's text and template shapes, and the plumbing done.