> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ya.works/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> History of changes and updates to the YaVendio Business API.

<Update label="2026-10-13" tags={["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`.
</Update>

<Update label="2026-09-07" tags={["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.

  <Note>
    This changes **nothing** until your account is included in balance-based charging. Until
    then the API answers exactly as it did before.
  </Note>
</Update>

<Update label="2026-09-03" tags={["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.
</Update>

<Update label="2026-08-28" tags={["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.
</Update>

<Update label="2026-08-28" tags={["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.
</Update>

<Update label="2026-08-26" tags={["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.
</Update>

<Update label="2026-08-24" tags={["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.
</Update>

<Update label="2026-08-24" tags={["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.
</Update>

<Update label="2026-08-24" tags={["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.
</Update>

<Update label="2026-08-22" tags={["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`.
</Update>

<Update label="2026-08-21" tags={["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.
</Update>

<Update label="2026-08-12" tags={["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`).
</Update>

<Update label="2026-04-14" tags={["Improvement", "Fix"]}>
  ## 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.
</Update>

<Update label="2026-03-22" tags={["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.
</Update>

<Update label="2026-03-20" tags={["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`)
</Update>

<Update label="2026-03-10" tags={["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
</Update>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.