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.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:
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 instatus_detail(stale,pausedordisabled). The request worked; the send is off by design.
Idempotency
Sameevent_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.
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 yourevent_id:
event_id that was never sent answers 404; one that does not match the charset
answers 422.
Two details worth knowing when you integrate:
updated_atis when the record last changed, not when you asked. Polling does not move it, but a delivery write that leavesstatusunchanged does, so do not use it as a watermark: reconcile onevent_id.- Only
failedanddiscardedare final.sent,deliveredandreadare re-resolved on every check, because a later receipt can still arrive — including a failure after aread.
Time windows
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.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 samePOST 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
- 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.

