Skip to main content
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, and GET /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 status in responses: unpublished. Creating or updating a product still accepts only published.
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.
The first two clear by adding funds from the WhatsApp payments page, and sending resumes on its own. No new 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_variables lists positions (["1", "2"]) or names (["name"]) depending on how the template was authored, and a new sendable field flags a template that mixes both styles, which cannot be sent this way.
  • variables takes 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 empty 202; the message names the keys the template expects.
If your integration already sends positions on positional templates, nothing changes. If it was sending extra keys, you will now get a 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 checking event_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_id to 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.
With sending off, 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, not 200. If your integration treats only 200 as success, it would start reading good sends as failures. Accept any 2xx. 200 is now for responses that produce no new send: a repeated event_id, and a discarded send (stale, paused).
  • A new send’s status is accepted, not in_progress. They mean the same thing — “on its way, we do not know yet how it ended” — and the outcome is still checked with GET /v1/template-messages/{event_id}, which is the only route: no webhook reports a send outcome. in_progress remains 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.
The recommended client timeout drops from 45 to 35 seconds: the call to the sending service keeps its 30-second ceiling, and that is now the only leg that can take time.
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_progress was the end of the road, you will start seeing sent, delivered, read and failed. Branch on status.
  • status_detail on a failed is not a closed set, and may be absent. Alongside account_not_eligible and no_sender_number, it can now carry send_failed when the send did not complete, send_cancelled when it was cancelled before it was attempted, or provider_error_<code> with Meta’s own code when the provider refused delivery with one — for example provider_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_at is 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 on event_id.
Sends you made before this change also start answering correctly, as long as they are still inside the retention window.
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:
  • language can 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 a 422, with no exception. If you relied on that escape, send the code the list returns.
  • category now arrives lowercase — marketing, not MARKETING. 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 → 422 with "type": "account_not_eligible" in detail[0]. It is not transient: write to us to get it enabled.
  • No connected sender number → 422 with "type": "no_sender_number". Connect your account’s WhatsApp number and retry.
  • Paused conversation → 200 with "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_id already bound to a different message, or outside the idempotency window → 409 with "type": "idempotency_conflict". Send the event under a new event_id: repeating the same one returns the same 409.
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 endpoint GET /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 as template.name.
  • language — the value to send as template.language. It can come back empty: the template records no language, but the send still requires a code, so do not pass the null through — the request answers 422. Only in that case does any code clear validation; if the template does record a language and yours does not match, that is a 422 too. The code travels verbatim to WhatsApp, so a wrong guess fails there — and you cannot see that from here today: the status stays in_progress.
  • required_variables — the keys that must be present in variables. "1" fills {{1}}. An empty list means the template takes no variables.
  • category and display_name — the latter is null unless it differs from name, to recognise it on your side. The key is always there: test the value, not its presence.
Only approved templates are listed, because those are exactly what a send accepts: listing a draft would hand you a template that answers 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 documented POST /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 at GET /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_variables is now variables.
  • first_name, last_name and second_last_name do not exist. This endpoint does not create or update the contact, and no public call does today: the Contacts API is read-only.
  • occurred_at is optional. Omit it and the event is never considered stale.
  • New optional metadata.use_case field to attribute the send to a use case.
  • Required scopes: outbound:send and outbound:read.
Your account has to be enabled to send: until it is, every call answers 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, with sale_status filter) 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).
New scopes: conversations:read, contacts:read, company:read (counts uses orders:read).
ImprovementFix

Payment vouchers, conversation summary, and media fix

  • Payment vouchers: New payment_receipt_urls field 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_conversation field 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 title field 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-Timezone header included in every response.
  • SKU: New sku field on product variants and order items.
  • Variant ID: New variant_id field 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-Key header
  • Rate Limiting: Request rate control per API Key
  • Pagination: Cursor-based pagination for all list endpoints