Improvement
Contract change: an order can arrive without a delivery address
Merchants will soon be able to record sales made outside WhatsApp: in a store or by phone. When the goods were handed over in person the order has no address, andGET /v1/orders
and GET /v1/orders/{order_id} return it with address: null. If your integration
reads address.full_address, check that address is not null first.An order can also arrive with no lines (items: []): it was recorded only from a
payment receipt or a merchant’s mark, and gains its lines later under the same id. If you
sync orders, read it again before treating it as final.Two new values may now appear:payment_terms:paid_pending_review— the customer sent a receipt the merchant has not reviewed yet. Treat it as not paid until it changes.- product
statusin responses:unpublished. Creating or updating a product still accepts onlypublished.
New feature
A send the account cannot pay for now answers 402
Outbound WhatsApp messages are paid from a prepaid balance. Until now an account with no
funds could keep sending through the API and the debt surfaced afterwards, with nobody
told. From today, POST /v1/template-messages answers 402 and sends nothing when
the account cannot pay for the message.type says which of the three it is:insufficient_funds— the account has no funds left for outbound messages.wallet_sends_paused— sending stopped when the funds ran out.wallet_currency_mismatch— the funds are held in a currency this API cannot price against. Retrying will not change that; contact us.
event_id is needed: the one that got the 402 is still free,
so you can send the same event again after adding funds and nothing is duplicated.Retrying without adding funds returns the same 402. Treat it like the other
account-level refusals: log the type and do not retry in a loop.This changes nothing until your account is included in balance-based charging. Until
then the API answers exactly as it did before.
Fix
Templates with named variables can now be sent
A template authored with named placeholders —Hola {{name}}, …, which is what the campaign
editor writes when it binds a variable to a contact field — showed up in GET /v1/templates
with required_variables: [], as if it took no variables, and every send was accepted with
202 only for the provider to reject it later. Now:- The catalog publishes the real keys.
required_variableslists positions (["1", "2"]) or names (["name"]) depending on how the template was authored, and a newsendablefield flags a template that mixes both styles, which cannot be sent this way. variablestakes the same keys.{"name": "Ronald"}goes out as a named parameter.- A send the provider would reject is rejected first, with
422. A missing key, an extra one, or a position on a named template no longer produce an empty202; the message names the keys the template expects.
422 that used to get lost at the provider.New feature
Cart abandonment is now visible and switchable from the panel
Knowing whether a merchant’s sends were going out meant checkingevent_id by event_id,
and turning them off had no route other than stopping the calls from the ecommerce. Both
are now handled from the cart abandonment panel:- Send history. It lists the merchant’s sends, newest first, with each one’s status,
within the 30-day retention window. You no longer need to know up front which
event_idto look for. - An on/off switch. Sending can be turned off and back on without touching the integration. Turning it off erases nothing: the history stays.
POST /v1/template-messages answers 200 with
"status": "discarded" and "status_detail": "disabled". It is not an error: the
request worked and the send is off by design, just like on a paused conversation. The
state is stored under your event_id, so GET /v1/template-messages/{event_id} tells the
same story.Your ecommerce needs no changes. If you already accept any 2xx and branch on
status, a switched-off send comes in where stale and paused already do. That
catalogue of reasons can grow: log the value and do not branch exhaustively on it.Improvement
Sending no longer waits 10 seconds
POST /v1/template-messages held the request open for 10 seconds waiting for a delivery
confirmation that never arrived down that path: it ran the wait out in full and
answered in_progress. Those ten seconds bought no information. The response now arrives
as soon as the sending service takes the message. Three things change for you:- An accepted send answers
202, not200. If your integration treats only200as success, it would start reading good sends as failures. Accept any2xx.200is now for responses that produce no new send: a repeatedevent_id, and a discarded send (stale,paused). - A new send’s status is
accepted, notin_progress. They mean the same thing — “on its way, we do not know yet how it ended” — and the outcome is still checked withGET /v1/template-messages/{event_id}, which is the only route: no webhook reports a send outcome.in_progressremains a valid contract value: sends made before this change still return it while they are inside the 30-day retention window. - The per-account fast acknowledgement mode is gone, because it is now everyone’s behaviour. There is nothing to request and nothing to configure.
Fix
A send’s status now tells you how it ended
GET /v1/template-messages/{event_id} answered in_progress forever. sent,
delivered and read were in the contract but never arrived, and failed only showed up
through an account refusal. A message Meta rejected looked exactly like a delivered
one. The status is now resolved against the real message. Three things change for you:- All four statuses now arrive. If your integration assumed
in_progresswas the end of the road, you will start seeingsent,delivered,readandfailed. Branch onstatus. status_detailon afailedis not a closed set, and may be absent. Alongsideaccount_not_eligibleandno_sender_number, it can now carrysend_failedwhen the send did not complete,send_cancelledwhen it was cancelled before it was attempted, orprovider_error_<code>with Meta’s own code when the provider refused delivery with one — for exampleprovider_error_131049, the marketing-template frequency cap. That catalogue changes without telling us, so do not handle it exhaustively: log the value and fall back to a generic failure when you do not recognise it, or when it is missing.updated_atis when the send’s record last changed, not when you asked. Polling no longer moves it, but it can move without the status changing, so it is not a watermark: reconcile onevent_id.
Fix
GET /v1/templates now reads the catalog sends actually come from
The list came from a copy this API’s sending rail does not write. It could show you
templates the send then refused, and hide ones you could actually send. Both now read the
same catalog. Three things change for you:languagecan no longer come back empty. We used to tell you that when a template had no recorded language, any code passed validation. That case is gone: the catalog always records the language, so a language that does not match is a422, with no exception. If you relied on that escape, send the code the list returns.categorynow arrives lowercase —marketing, notMARKETING. Adjust if you compare the string as-is.- Your list can change size, including to empty. Templates live in the sending rail’s catalog, filled when your account syncs with WhatsApp. An empty list means every send is refused today — write to us and we will sync it.
Improvement
A refused send now tells you what happened
POST /v1/template-messages answered 502 to very different situations: an account not
enabled, a paused conversation, a recycled event_id. From the outside they looked
identical, and the documentation told you to retry even when no retry could change the
outcome. Each one now has its own answer:- Account not enabled to send →
422with"type": "account_not_eligible"indetail[0]. It is not transient: write to us to get it enabled. - No connected sender number →
422with"type": "no_sender_number". Connect your account’s WhatsApp number and retry. - Paused conversation →
200with"status": "discarded"and"status_detail": "paused". The request worked; the send is off by design, because a human agent took over the conversation or there is a billing freeze. event_idalready bound to a different message, or outside the idempotency window →409with"type": "idempotency_conflict". Send the event under a newevent_id: repeating the same one returns the same409.
502 is now left for a real failure of the internal sending service, which is worth
retrying with capped backoff.With this, failed becomes reachable: both account refusals are stored under your
event_id, so GET /v1/template-messages/{event_id} tells the same story as the send. A
delivery refusal still does not produce it. And one effect of idempotency worth keeping in
mind: resending the same event_id after an account refusal answers 200 with
"status": "failed" — that is the stored state, not a new send.Fix
The outbound send is now routed per environment
POST /v1/template-messages returns 502 for every send that passes validation: the
internal call goes to a service that does not serve that route. This version gives the
write its own address, configurable per environment. The send starts working when that
address is pointed, not with this release.One thing will not change once it is on: the sent template will not appear in
GET /v1/conversations/{id}/messages. The send and that list are served from different
stores, so reconcile against your own event_id, not against the list.The database connection is also no longer held during the send’s wait. If you ran bursts
of sends, you could see slowness or errors on other API endpoints during those bursts;
that is fixed here.New feature
List your approved templates
New endpointGET /v1/templates: it returns the templates your account can send right
now, with the variables each one expects. It is what was missing to build a
POST /v1/template-messages without guessing the values.Every template carries:name— the value to send astemplate.name.language— the value to send astemplate.language. It can come back empty: the template records no language, but the send still requires a code, so do not pass thenullthrough — the request answers422. Only in that case does any code clear validation; if the template does record a language and yours does not match, that is a422too. The code travels verbatim to WhatsApp, so a wrong guess fails there — and you cannot see that from here today: the status staysin_progress.required_variables— the keys that must be present invariables."1"fills{{1}}. An empty list means the template takes no variables.categoryanddisplay_name— the latter isnullunless it differs fromname, to recognise it on your side. The key is always there: test the value, not its presence.
422. And if the list comes back
empty, your account has no approved template yet — every send will answer 422 until it
does.Required scope: outbound:read.Fix
Template Messages contract corrected
The cart abandonment page documentedPOST /v1/cart-abandonments, a contract that was
replaced during design and never existed. It has been replaced with the real
contract, at the same URL.If you started building against the previous version, these are the changes that affect
you:- The path is
POST /v1/template-messages, and status is read atGET /v1/template-messages/{event_id}. - The template travels in the request, as
template: {name, language}. The previous version said the opposite: that it was pinned on our side. template_variablesis nowvariables.first_name,last_nameandsecond_last_namedo not exist. This endpoint does not create or update the contact, and no public call does today: the Contacts API is read-only.occurred_atis optional. Omit it and the event is never considered stale.- New optional
metadata.use_casefield to attribute the send to a use case. - Required scopes:
outbound:sendandoutbound:read.
502. And
the sent, delivered, read and failed statuses are not reachable yet — they are in
the contract so your integration can account for them now.New feature
Conversations, contacts, company profile and order counts
Four new read-only endpoint groups:- Conversations: List the merchant’s conversations (
GET /v1/conversations, withsale_statusfilter) and the messages of a conversation (GET /v1/conversations/{conversation_id}/messages). Reading messages through the API never marks them as read. - Contacts: List the merchant’s contacts with their tags, opt-in status and metadata (
GET /v1/contacts). - Company: Public company profile (
GET /v1/company). - Order counts: Total number of orders and breakdown by status (
GET /v1/orders/counts).
conversations:read, contacts:read, company:read (counts uses orders:read).ImprovementFix
Payment vouchers, conversation summary, and media fix
- Payment vouchers: New
payment_receipt_urlsfield in order responses. Contains the list of URLs for payment receipt images uploaded by the customer (Yape, Plin, bank transfer, etc.). If multiple vouchers exist, all are included. - Conversation summary: New
summary_conversationfield in order responses. Contains an AI-generated summary with order details and customer preferences, avoiding the need to open the conversation. - Variant media fix: Creating a variant image without the
titlefield no longer returns an error. The field is optional as documented.
New feature
Webhooks, timezones, and SKU
- Webhooks: Receive real-time order events (
order.created,order.status_changed). Full CRUD at/v1/webhooks. HMAC-SHA256 signature on every delivery. - Timezones: All dates (
order_date,created_at) are now returned in the merchant’s local timezone.X-Timezoneheader included in every response. - SKU: New
skufield on product variants and order items. - Variant ID: New
variant_idfield on order items to identify the internal variant.
New feature
Full product management
- Products: Create, update, and delete products (
POST,PATCH,DELETE /v1/products) - Variants: Full CRUD for product variants
- Options and values: Manage options and their values per product
- Media: Upload, update, and delete media per variant
- Categories: Create, update, delete categories and assign them to products
- Catalogs: List available catalogs (
GET /v1/catalogs)
Release
Business API Launch
First public release of the YaVendio Business API.- Orders: List and detail orders (
GET /v1/orders,GET /v1/orders/{order_id}) - Products: Query products (
GET /v1/products,GET /v1/products/{product_id}) - Authentication: API Keys via
X-API-Keyheader - Rate Limiting: Request rate control per API Key
- Pagination: Cursor-based pagination for all list endpoints

