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, yGET /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.statusde producto en las respuestas:unpublished. Crear o editar un producto sigue aceptando sólopublished.
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.
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_variableslista posiciones (["1", "2"]) o nombres (["name"]) según cómo se creó la plantilla, y un nuevo camposendablemarca la plantilla que mezcla ambos estilos, que no se puede enviar por esta vía. variablesacepta 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 un202vacío; el mensaje nombra las claves que la plantilla espera.
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 consultarevent_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_idbuscar. - 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í.
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, no200. Si tu integración trata sólo el200como éxito, empezaría a leer envíos buenos como fallos. Acepta cualquier2xx. El200queda para las respuestas que no producen un envío nuevo: unevent_idrepetido y un envío descartado (stale,paused). - El estado de un envío nuevo es
accepted, noin_progress. Significan lo mismo — “va en camino, todavía no sabemos cómo terminó” — y el desenlace se sigue consultando enGET /v1/template-messages/{event_id}, que es el único camino: ningún webhook reporta el resultado de un envío.in_progresssigue 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.
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_progressera el final del camino, ahora vas a versent,delivered,readyfailed. Ramifica sobrestatus. status_detailen unfailedno es una lista cerrada y puede faltar. Además deaccount_not_eligibleyno_sender_number, ahora puede traersend_failedcuando el envío no llegó a completarse,send_cancelledcuando se canceló antes de intentarse, oprovider_error_<código>con el código de Meta cuando el proveedor rechazó la entrega con uno — por ejemploprovider_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_ates 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 porevent_id.
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:languageya 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 es422, sin excepción. Si dependías de ese escape, manda el código que devuelve la lista.categoryahora llega en minúsculas —marketing, noMARKETING. 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 →
422con"type": "account_not_eligible"endetail[0]. No es transitorio: escríbenos para habilitarla. - Sin número remitente conectado →
422con"type": "no_sender_number". Conecta el número de WhatsApp de tu cuenta y reintenta. - Conversación en pausa →
200con"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_idya atado a otro mensaje, o fuera de la ventana de idempotencia →409con"type": "idempotency_conflict". Manda el evento con unevent_idnuevo: repetir el mismo devuelve el mismo409.
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 endpointGET /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 entemplate.name.language— el valor que va entemplate.language. Puede venir vacío: la plantilla no tiene idioma registrado, pero el envío igual exige un código, así que no mandes elnulltal cual — el request lo rechaza con422. Solo en ese caso cualquier código pasa la validación; si la plantilla sí registra idioma y no coincide, también es422. 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 enin_progress.required_variables— las claves que tienen que estar envariables."1"llena{{1}}. Una lista vacía significa que la plantilla no lleva variables.categoryydisplay_name— este último esnullsalvo que difiera delname, para reconocerla desde tu lado. La clave siempre viene: comprueba el valor, no su presencia.
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 documentabaPOST /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 enGET /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_variablesahora se llamavariables.first_name,last_nameysecond_last_nameno 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_ates opcional. Si lo omites, el aviso nunca se considera vencido.- Nuevo campo opcional
metadata.use_casepara atribuir el envío a un caso de uso. - Alcances requeridos:
outbound:sendyoutbound:read.
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 filtrosale_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).
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_urlsen 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_conversationen 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
titleya 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. HeaderX-Timezoneincluido en cada respuesta. - SKU: Nuevo campo
skuen variantes de productos y en items de pedidos. - Variant ID: Nuevo campo
variant_iden 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

