Skip to main content
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.
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.
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.
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.
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.
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.
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.
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.
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.
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á.
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.
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.
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).
MejoraCorrecció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.
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.
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)
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