> ## 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

> Historial de cambios y actualizaciones de la YaVendio Business API.

<Update label="2026-10-13" tags={["Mejora"]}>
  ## Cambio de contrato: un pedido puede llegar sin dirección de entrega

  Las tiendas pronto podrán registrar ventas hechas fuera de WhatsApp: en el local o por
  teléfono. Cuando el producto se entregó en mano, ese pedido no tiene dirección, y
  `GET /v1/orders` y `GET /v1/orders/{order_id}` lo devuelven con **`address: null`**. Si tu
  integración lee `address.full_address`, revisa primero que `address` no sea nulo.

  Un pedido también puede llegar **sin líneas** (`items: []`): se registró sólo a partir de
  un comprobante de pago o de una marca de la tienda, y gana sus líneas después con el mismo
  `id`. Si sincronizas pedidos, vuelve a leerlo antes de darlo por cerrado.

  Dos valores nuevos que ya pueden aparecer:

  * `payment_terms`: **`paid_pending_review`** — el cliente envió el comprobante y la tienda
    todavía no lo revisó. **Trátalo como no pagado** hasta que cambie.
  * `status` de producto en las respuestas: **`unpublished`**. Crear o editar un producto
    sigue aceptando sólo `published`.
</Update>

<Update label="2026-09-07" tags={["Nueva funcionalidad"]}>
  ## Un envío que la cuenta no puede pagar ahora responde `402`

  Los envíos salientes de WhatsApp se pagan desde un saldo prepago. Hasta ahora, una cuenta
  sin saldo podía seguir enviando por la API y la deuda aparecía después, sin que nadie te
  hubiera avisado. Desde hoy, `POST /v1/template-messages` responde **`402` y no envía nada**
  cuando la cuenta no puede pagar el mensaje.

  El `type` dice cuál de los tres casos es:

  * **`insufficient_funds`** — la cuenta se quedó sin saldo para mensajes salientes.
  * **`wallet_sends_paused`** — los envíos se cortaron cuando el saldo se acabó.
  * **`wallet_currency_mismatch`** — el saldo está en una moneda que esta API no puede
    cotizar. Reintentar no cambia nada; escríbenos.

  Los dos primeros se resuelven **recargando desde la página de pagos de WhatsApp**, y el
  envío se reanuda solo. No hace falta un `event_id` nuevo: el que recibió el `402` sigue
  libre, así que puedes reenviar el mismo evento después de recargar y no se duplica nada.

  **Reintentar sin recargar devuelve el mismo `402`.** Trátalo como los otros rechazos de
  cuenta: registra el `type` y no reintentes en bucle.

  <Note>
    Esto **no** cambia nada mientras tu cuenta no esté incluida en el cobro por saldo. Si no
    lo está, la API responde exactamente como antes.
  </Note>
</Update>

<Update label="2026-09-03" tags={["Corrección"]}>
  ## Las plantillas con variables por nombre ya se pueden enviar

  Una plantilla creada con marcadores nombrados — `Hola {{name}}, …`, que es lo que arma el
  editor de campañas al vincular una variable a un campo del contacto — aparecía en
  `GET /v1/templates` con `required_variables: []`, como si no llevara variables, y cualquier
  envío se aceptaba con `202` para que el proveedor lo rechazara después. Ahora:

  * **El catálogo publica las claves reales.** `required_variables` lista posiciones
    (`["1", "2"]`) o nombres (`["name"]`) según cómo se creó la plantilla, y un nuevo campo
    `sendable` marca la plantilla que mezcla ambos estilos, que no se puede enviar por esta vía.
  * **`variables` acepta las mismas claves.** `{"name": "Ronald"}` sale como parámetro nombrado.
  * **Un envío que el proveedor rechazaría se rechaza antes con `422`.** Una clave que falta,
    una que sobra o una posición sobre una plantilla nombrada ya no producen un `202` vacío; el
    mensaje nombra las claves que la plantilla espera.

  **Si tu integración ya envía posiciones sobre plantillas posicionales, no cambia nada.** Si
  enviabas claves de más, ahora recibirás un `422` que antes se perdía en el proveedor.
</Update>

<Update label="2026-08-28" tags={["Nueva funcionalidad"]}>
  ## El carrito abandonado ahora se ve y se apaga desde el panel

  Saber si los envíos de un comercio estaban saliendo obligaba a consultar `event_id` por
  `event_id`, y apagarlos no tenía más vía que dejar de llamar a la API desde el ecommerce.
  Las dos cosas se resuelven ahora desde el panel de carrito abandonado:

  * **Historial de envíos.** Lista los envíos del comercio, del más reciente al más antiguo,
    con el estado de cada uno, dentro de la ventana de retención de 30 días. Ya no hace
    falta saber de antemano qué `event_id` buscar.
  * **Interruptor de encendido y apagado.** El envío se puede apagar y volver a encender sin
    tocar la integración. Apagarlo no borra nada: el historial sigue ahí.

  **Con el envío apagado, `POST /v1/template-messages` responde `200` con
  `"status": "discarded"` y `"status_detail": "disabled"`.** No es un error: la petición
  funcionó y el envío está apagado a propósito, igual que en una conversación en pausa. El
  estado queda guardado bajo tu `event_id`, así que `GET /v1/template-messages/{event_id}`
  cuenta la misma historia.

  **Tu ecommerce no necesita ningún cambio.** Si ya aceptas cualquier `2xx` y ramificas
  sobre `status`, un envío apagado entra por donde ya entran `stale` y `paused`. Ese
  catálogo de motivos puede crecer: registra el valor y no ramifiques de forma exhaustiva
  sobre él.
</Update>

<Update label="2026-08-28" tags={["Mejora"]}>
  ## Enviar ya no espera 10 segundos

  `POST /v1/template-messages` mantenía el request abierto 10 segundos esperando una
  confirmación de entrega **que nunca llegaba por ese camino**: agotaba la espera completa
  y respondía `in_progress`. Los diez segundos no compraban ninguna información. Ahora la
  respuesta llega apenas el servicio de envío toma el mensaje. Tres cosas cambian para ti:

  * **El envío aceptado responde `202`, no `200`.** Si tu integración trata sólo el `200`
    como éxito, **empezaría a leer envíos buenos como fallos**. Acepta cualquier `2xx`. El
    `200` queda para las respuestas que no producen un envío nuevo: un `event_id` repetido
    y un envío descartado (`stale`, `paused`).
  * **El estado de un envío nuevo es `accepted`, no `in_progress`.** Significan lo mismo —
    "va en camino, todavía no sabemos cómo terminó" — y el desenlace se sigue consultando
    en `GET /v1/template-messages/{event_id}`, que es el único camino: ningún webhook
    reporta el resultado de un envío. `in_progress` sigue siendo un
    valor válido del contrato: los envíos hechos antes de este cambio lo devuelven mientras
    sigan dentro de la ventana de retención de 30 días.
  * **La modalidad de acuse rápido por cuenta desaparece**, porque ahora es el
    comportamiento de todos. No hay nada que solicitar ni que configurar.

  El timeout recomendado de tu cliente baja de 45 a **35 segundos**: la llamada al servicio
  de envío conserva su tope de 30 segundos, y ese es ahora el único tramo que puede tardar.
</Update>

<Update label="2026-08-26" tags={["Corrección"]}>
  ## El estado de un envío ahora dice cómo terminó

  `GET /v1/template-messages/{event_id}` respondía `in_progress` para siempre. `sent`,
  `delivered` y `read` estaban en el contrato pero nunca llegaban, y `failed` solo aparecía
  por un rechazo de cuenta. **Un mensaje que Meta rechazaba se veía igual que uno
  entregado.** Ahora el estado se resuelve contra el mensaje real. Tres cosas cambian para
  ti:

  * **Los cuatro estados ya llegan.** Si tu integración asumía que `in_progress` era el
    final del camino, ahora vas a ver `sent`, `delivered`, `read` y `failed`. Ramifica
    sobre `status`.
  * **`status_detail` en un `failed` no es una lista cerrada y puede faltar.** Además de
    `account_not_eligible` y `no_sender_number`, ahora puede traer `send_failed` cuando el
    envío no llegó a completarse, `send_cancelled` cuando se canceló antes de intentarse, o
    `provider_error_<código>` con el código de Meta cuando el proveedor rechazó la entrega
    con uno — por ejemplo
    `provider_error_131049`, el tope de frecuencia de plantillas de marketing. Ese catálogo
    cambia sin que nos avisen: **no lo trates de forma exhaustiva**, registra el valor y cae
    a un fallo genérico si no lo reconoces o si viene vacío.
  * **`updated_at` es cuándo cambió por última vez el registro del envío**, no cuándo
    consultaste. Consultar ya no lo mueve, pero sí puede moverse sin que el estado cambie,
    así que **no sirve como marca de agua**: reconcilia por `event_id`.

  Los envíos que hiciste antes de este cambio también empiezan a responder bien, mientras
  sigan dentro de la ventana de retención.
</Update>

<Update label="2026-08-24" tags={["Corrección"]}>
  ## `GET /v1/templates` lee el catálogo del que realmente se envía

  La lista salía de una copia que el riel de envío de esta API no escribe. Podía mostrarte
  plantillas que el envío después rechazaba, y ocultarte las que sí podías mandar. Ahora
  ambos leen el mismo catálogo. Tres cosas cambian para ti:

  * **`language` ya no puede venir vacío.** Antes te decíamos que, si la plantilla no tenía
    idioma registrado, cualquier código pasaba la validación. Ese caso ya no existe: el
    catálogo registra el idioma siempre, así que **un idioma que no coincide es `422`,
    sin excepción**. Si dependías de ese escape, manda el código que devuelve la lista.
  * **`category` ahora llega en minúsculas** — `marketing`, no `MARKETING`. Si comparas el
    string tal cual, ajústalo.
  * **Tu lista puede cambiar de tamaño, incluso a vacía.** Las plantillas viven en el
    catálogo del riel de envío, y se llenan cuando tu cuenta sincroniza con WhatsApp. Una
    lista vacía significa que hoy todo envío se rechaza: escríbenos y la sincronizamos.
</Update>

<Update label="2026-08-24" tags={["Mejora"]}>
  ## Los rechazos de un envío ahora dicen qué pasó

  `POST /v1/template-messages` respondía `502` a situaciones muy distintas: una cuenta no
  habilitada, una conversación en pausa, un `event_id` reciclado. Desde afuera se veían
  idénticas, y la documentación te pedía reintentar incluso cuando ningún reintento podía
  cambiar el resultado. Ahora cada una tiene su propia respuesta:

  * **Cuenta no habilitada para enviar** → `422` con `"type": "account_not_eligible"` en
    `detail[0]`. No es transitorio: escríbenos para habilitarla.
  * **Sin número remitente conectado** → `422` con `"type": "no_sender_number"`. Conecta el
    número de WhatsApp de tu cuenta y reintenta.
  * **Conversación en pausa** → `200` con `"status": "discarded"` y
    `"status_detail": "paused"`. La petición funcionó; el envío está apagado a propósito,
    porque un agente humano tomó la conversación o hay un corte por facturación.
  * **`event_id` ya atado a otro mensaje, o fuera de la ventana de idempotencia** → `409`
    con `"type": "idempotency_conflict"`. Manda el evento con un `event_id` nuevo: repetir
    el mismo devuelve el mismo `409`.

  El `502` queda solo para una falla real del servicio interno de envío, que sí vale la pena
  reintentar con backoff acotado.

  Con esto **`failed` pasa a ser alcanzable**: los dos rechazos de cuenta se guardan bajo tu
  `event_id`, así que `GET /v1/template-messages/{event_id}` cuenta la misma historia que el
  envío. Un rechazo de entrega todavía no lo produce. Y un efecto de la idempotencia que
  conviene tener presente: reenviar el mismo `event_id` después de un rechazo de cuenta
  responde `200` con `"status": "failed"` — es el estado guardado, no un envío nuevo.
</Update>

<Update label="2026-08-24" tags={["Corrección"]}>
  ## El envío outbound ya se dirige por entorno

  `POST /v1/template-messages` devuelve `502` para cualquier envío que pase la validación:
  la llamada interna sale hacia un servicio que no atiende esa ruta. Esta versión le da a la
  escritura su propia dirección, configurable por entorno. **El envío empieza a funcionar
  cuando esa dirección se apunta**, no con esta publicación.

  **Cuando se active, algo no va a cambiar**: la plantilla enviada **no aparecerá** en
  `GET /v1/conversations/{id}/messages`. El envío y esa lista se sirven desde almacenes
  distintos, así que reconcilia contra tu propio `event_id`, no contra la lista.

  Además, la conexión a la base de datos ya no queda tomada durante la espera del envío. Si
  hacías ráfagas de envíos, podías ver lentitud o errores en otros endpoints de la API
  durante esas ráfagas; eso se corrige acá.
</Update>

<Update label="2026-08-22" tags={["Nueva funcionalidad"]}>
  ## Lista de plantillas aprobadas

  Nuevo endpoint `GET /v1/templates`: devuelve las plantillas que tu cuenta puede enviar
  ahora mismo, con las variables que cada una espera. Es lo que faltaba para armar un
  `POST /v1/template-messages` sin adivinar los valores.

  Cada plantilla trae:

  * **`name`** — el valor que va en `template.name`.
  * **`language`** — el valor que va en `template.language`. Puede venir vacío: la
    plantilla no tiene idioma registrado, pero el envío **igual exige un código**, así que no
    mandes el `null` tal cual — el request lo rechaza con `422`. Solo en ese caso cualquier
    código pasa la validación; si la plantilla sí registra idioma y no coincide, también es
    `422`. El código viaja tal cual a WhatsApp, así que si te equivocas de idioma el envío
    falla allá — y hoy eso no se ve desde aquí: el estado se queda en `in_progress`.
  * **`required_variables`** — las claves que tienen que estar en `variables`. `"1"` llena
    `{{1}}`. Una lista vacía significa que la plantilla no lleva variables.
  * **`category`** y **`display_name`** — este último es `null` salvo que difiera del
    `name`, para reconocerla desde tu lado. La clave siempre viene: comprueba el valor, no
    su presencia.

  Solo aparecen las plantillas **aprobadas**, porque son exactamente las que un envío
  acepta: listar un borrador te daría una plantilla que responde `422`. Y si la lista
  vuelve vacía, tu cuenta todavía no tiene ninguna plantilla aprobada — cada envío va a
  responder `422` hasta que la tenga.

  Alcance requerido: `outbound:read`.
</Update>

<Update label="2026-08-21" tags={["Corrección"]}>
  ## Corrección del contrato de Mensajes de Plantilla

  La página de carrito abandonado documentaba `POST /v1/cart-abandonments`, un contrato que
  fue reemplazado durante el diseño y que **nunca llegó a existir**. La reemplazamos por el
  contrato real, en la misma URL.

  Si empezaste a construir contra la versión anterior, estos son los cambios que te afectan:

  * **La ruta es `POST /v1/template-messages`**, y el estado se consulta en
    `GET /v1/template-messages/{event_id}`.
  * **La plantilla viaja en el request**, como `template: {name, language}`. La versión
    anterior decía lo contrario: que quedaba fijada de nuestro lado.
  * **`template_variables` ahora se llama `variables`.**
  * **`first_name`, `last_name` y `second_last_name` no existen.** Este endpoint no crea ni
    actualiza el contacto, y hoy ninguna llamada pública lo hace: la API de Contactos es de
    solo lectura.
  * **`occurred_at` es opcional.** Si lo omites, el aviso nunca se considera vencido.
  * **Nuevo campo opcional `metadata.use_case`** para atribuir el envío a un caso de uso.
  * **Alcances requeridos**: `outbound:send` y `outbound:read`.

  **Tu cuenta tiene que estar habilitada para enviar**: mientras no lo esté, cada llamada
  responde `502`. Y los estados `sent`, `delivered`, `read` y `failed` todavía no son
  alcanzables — están en el contrato para que tu integración los contemple desde ahora.
</Update>

<Update label="2026-08-12" tags={["Nueva funcionalidad"]}>
  ## Conversaciones, contactos, perfil de empresa y conteo de pedidos

  Cuatro nuevos grupos de endpoints de solo lectura:

  * **Conversaciones**: Lista las conversaciones del comercio (`GET /v1/conversations`, con filtro `sale_status`) y los mensajes de una conversación (`GET /v1/conversations/{conversation_id}/messages`). Leer mensajes por la API nunca los marca como leídos.
  * **Contactos**: Lista los contactos del comercio con sus tags, estado de opt-in y metadata (`GET /v1/contacts`).
  * **Empresa**: Perfil público de la empresa (`GET /v1/company`).
  * **Conteo de pedidos**: Total de pedidos y desglose por estado (`GET /v1/orders/counts`).

  Nuevos scopes: `conversations:read`, `contacts:read`, `company:read` (el conteo usa `orders:read`).
</Update>

<Update label="2026-04-14" tags={["Mejora", "Corrección"]}>
  ## Vouchers de pago, resumen de conversación y fix de media

  * **Vouchers de pago**: Nuevo campo `payment_receipt_urls` en las respuestas de pedidos. Contiene la lista de URLs de las imágenes de comprobantes de pago subidos por el cliente (Yape, Plin, transferencia, etc.). Si hay múltiples vouchers, todos se incluyen.
  * **Resumen de conversación**: Nuevo campo `summary_conversation` en las respuestas de pedidos. Contiene un resumen generado por IA con los detalles del pedido y preferencias del cliente, evitando tener que entrar a la conversación.
  * **Fix media de variante**: Crear una imagen de variante sin el campo `title` ya no retorna error. El campo es opcional tal como está documentado.
</Update>

<Update label="2026-03-22" tags={["Nueva funcionalidad"]}>
  ## Webhooks, zonas horarias y SKU

  * **Webhooks**: Recibe eventos de pedidos en tiempo real (`order.created`, `order.status_changed`). CRUD completo en `/v1/webhooks`. Firma HMAC-SHA256 en cada entrega.
  * **Zonas horarias**: Todas las fechas (`order_date`, `created_at`) ahora se devuelven en la zona horaria local del comercio. Header `X-Timezone` incluido en cada respuesta.
  * **SKU**: Nuevo campo `sku` en variantes de productos y en items de pedidos.
  * **Variant ID**: Nuevo campo `variant_id` en items de pedidos para identificar la variante interna.
</Update>

<Update label="2026-03-20" tags={["Nueva funcionalidad"]}>
  ## Gestión completa de productos

  * **Productos**: Crear, actualizar y eliminar productos (`POST`, `PATCH`, `DELETE /v1/products`)
  * **Variantes**: CRUD completo de variantes por producto
  * **Opciones y valores**: Gestión de opciones y sus valores por producto
  * **Media**: Subida, actualización y eliminación de media por variante
  * **Categorías**: Crear, actualizar, eliminar categorías y asignarlas a productos
  * **Catálogos**: Listado de catálogos disponibles (`GET /v1/catalogs`)
</Update>

<Update label="2026-03-10" tags={["Lanzamiento"]}>
  ## Lanzamiento de la Business API

  Primera versión pública de la YaVendio Business API.

  * **Pedidos**: Listado y detalle de pedidos (`GET /v1/orders`, `GET /v1/orders/{order_id}`)
  * **Productos**: Consulta de productos (`GET /v1/products`, `GET /v1/products/{product_id}`)
  * **Autenticación**: API Keys con header `X-API-Key`
  * **Rate Limiting**: Control de tasa de peticiones por API Key
  * **Paginación**: Paginación basada en cursor para todos los listados
</Update>


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