Skip to main content

Template Messages

When you need to write to a customer first — recovering an abandoned cart, telling someone their order is ready, delivering a pickup PIN — the message has to go out as a Meta-approved template. These endpoints are the public door for requesting that send and for checking where it got to.
Your account has to be enabled before your first call. The contract is published ahead of general availability so you can build against it; enablement is per account and we arrange it with you along with the template and the sender number.Until your account is enabled, every call answers 422 with "type": "account_not_eligible" — always, not intermittently. It is not a problem with your request and no retry changes it: it is configuration on our side, and the response says so in those words instead of hiding it behind a generic error. Write to us to get it enabled. Any change to the contract is announced in the Changelog.

What it does and what it does not

Prerequisites

To find out which templates you can send, read the catalogue. GET /v1/templates lists the ones you have approved, with the exact name and language this call expects. An empty list means none is approved yet, and until one is every send is rejected. Each template’s sendable field says whether this API can send it: it is false when the text mixes positional and named variables, numbers them with gaps, or carries a variable in the header (this API fills the body only). A template with sendable: false answers 422 on send, and the fix lives in Meta, not in your request.

Send a message

Request fields

Your company identifier does not travel in the request: it is derived from your API key (Authentication). The template does travel, by name and language, and is validated against the ones you have approved.

Responses

Send accepted — 202

We validate, reserve the event_id and hand it to the sending service. The response arrives as soon as that happens and does not carry the outcome:
accepted means “on its way”, not “it worked”. The send carries on after the response. Do not read it as success — check the status endpoint, where it resolves against the real message. It is the only route: the webhooks at /v1/webhooks emit order events only (order.created, order.status_changed), none for delivery.
Set your client timeout to 35 seconds or more. The POST awaits the call to the sending service, which has its own 30-second ceiling. On the normal path it answers in under a second, but that is the typical case and not a guarantee: a client with a 5 or 10-second timeout abandons healthy requests that are still in flight, and then reports failures that never happened.

No new send — 200

A 200 means this request produced no send. Two cases:
  • A repeated event_id: it returns the stored state of the first request (Idempotency).
  • A send that is unviable from the start — the event is stale, the customer’s conversation is paused, or the merchant switched sending off from the panel: it answers "status": "discarded" with the reason in status_detail (stale, paused or disabled). The request worked; the send is off by design.

Idempotency

Same event_id = same send. Repeating it returns the state of the first request and never produces a second WhatsApp. That is why retrying on a network timeout is safe. Idempotency runs before the phone, template and freshness checks: a replay returns the stored state without judging those fields again. What does run first is the shape of the body — the event_id charset, the template.name and template.language lengths, occurred_at parsing. A malformed replay gets a 422 and never reaches idempotency. There is one case where an event_id stops working: if the sending side has already bound it to a different message, or it aged out of the 30-day window, the answer is 409 with "type": "idempotency_conflict". Send that event under a new event_id — repeating the same one returns the same 409. We deliberately store no state under the refused key: if we did, the next attempt would get a quiet replay instead of the 409 that tells you what to fix.
A 422 on a replay deserves a look before you drop the event. If the first attempt may already have gone out, the event is not lost: fix the shape of the body and resend with the same event_id, which stays safe and returns the real state. When the 422 was an account refusal, that resend answers 200 with "status": "failed" — it is the stored state of the first attempt, not a new send.
The event_id is not a trace identifier. If your system generates a new id per HTTP attempt, that value is not usable as an event_id: each retry would look like a distinct event and the customer would receive the WhatsApp more than once. To trace attempts use the X-Request-ID header — see Request IDs.

Check the status

“Sent” is not “delivered”: Meta reports delivery to the phone minutes or hours later — or never, if the phone is off. That cycle is checked by your event_id:
An event_id that was never sent answers 404; one that does not match the charset answers 422.
status_detail on a failed is not a closed set, and may be absent. It is either an account refusal (account_not_eligible, no_sender_number), a send that did not complete (send_failed) or was cancelled before it was attempted (send_cancelled), or, when the provider refuses delivery with a code, provider_error_<code> — for example provider_error_131049. The code is Meta’s and their catalogue changes without telling us, so do not switch exhaustively on this field: branch on status, log status_detail, and treat any unknown or missing value as a generic failure.An accepted may also carry delivery_not_observable: that merchant’s sends live in an instance of their own that this deployment cannot read, so the status will not resolve on each poll — it is not a failure.
Two details worth knowing when you integrate:
  • updated_at is when the record last changed, not when you asked. Polling does not move it, but a delivery write that leaves status unchanged does, so do not use it as a watermark: reconcile on event_id.
  • Only failed and discarded are final. sent, delivered and read are re-resolved on every check, because a later receipt can still arrive — including a failure after a read.

Time windows

Send the offset in occurred_at. A value with no timezone is read as UTC. If your system sends Peru local time (UTC-5), the event is aged an extra 5 hours against a 6-hour window: a cart abandoned two hours ago computes as seven hours old and comes back discarded with reason stale — answered 200, no error, and no message sent.

Errors

Errors follow the standard API format:
The two account 422s are told apart by detail[0].type, not by a field: they carry "loc": ["body"] because there is no value to correct. It is the same array format as any other 422 — see Error Handling. The 409, by contrast, uses the standard format and carries its type at the root.
A 502 no longer means “it could be anything”. The refusals that depend on your account or your event have their own answer: an account not enabled to send and a missing sender number are 422s, a paused conversation is a 200 with "status": "discarded" and "status_detail": "paused", and a recycled key is a 409. What is left in 502 is a real failure of the internal sending service. Retry with backoff a few times; if it persists, stop retrying and contact us.
Neither a stale event, nor a paused conversation, nor sending switched off is an error: they all answer 200 with "status": "discarded" and "status_detail" set to "stale", "paused" or "disabled". A pause is put there by a human agent who took over the conversation, or by a billing freeze; the switch is thrown by the merchant from the cart abandonment panel. Neither is lifted from the request.

Recipe: Shopify (Flow)

Shopify Flow makes this same POST with no code and no server of your own in the middle: it is the guided example of the contract above, with the abandonment trigger already solved. On Shopify? Follow the guided recipe →

Testing the integration

There is no sandbox environment. The dashboard always creates live keys and there is no isolated environment behind them — see Authentication. This integration’s test environment is distinguished by the destination phone, not by the credential.
  • Test phase — send to your own team’s phones, in an agreed window. The message really arrives, with the real template: that is exactly the test.
  • Production phase — same key, same contract; the only change is that the phones belong to real customers.
The list of destination phones is therefore the only real control between one phase and the other: agree it in writing before the first test and treat it as part of the integration’s configuration.