Enviar una plantilla desde tu propio sistema

Una llamada a la API envía una plantilla de WhatsApp aprobada a un número — desde un webhook, un cron o un script. La clave, el id de la plantilla, la petición, cada rechazo y las dos cosas que hay que tener en cuenta al diseñar.

Actualizado

Algunas plantillas no las envía ninguna persona. Lavandería Tidewater quiere que la confirmación de recogida salga en cuanto su sistema de reservas confirma una franja: nadie debería tener que abrir la bandeja para eso. Para esto hay un endpoint:

POST https://api.omniocto.com/api/wa-templates/{id}/send

Envía una plantilla aprobada a un número de teléfono. Llámalo desde un manejador de webhook o un script: «cuando se envíe un pedido, manda la plantilla de recogida». Como es una plantilla, la ventana de 24 horas no aplica (por qué).

Consigue una clave de API

Las claves están en /settings/api-keys — en el menú del engranaje, bajo Administración y sistema → Claves de API.

  1. Haz clic en Crear clave. El diálogo, Crear clave de API, pide un Nombre (opcional) — algo como «p. ej. canalización de CI»; ponle el nombre del sistema que la va a usar.
  2. Clave de API creada muestra la clave una sola vez, con «Copie esta clave ahora; no podrá verla de nuevo.» Guárdala donde tu sistema guarda los secretos.

La lista muestra la fecha de Último uso de cada clave, y Revocar clave la detiene al instante: «Cualquier integración que la use fallará.» Una clave por integración hace que eso sea seguro.

Envía la clave en cada llamada como Authorization: Bearer <key>.

Encuentra el id de la plantilla

Abre /templates/whatsapp y haz clic en la plantilla. Se abre el panel y la dirección de la página gana &template=wt-…: ese id corto wt- es el que va en la ruta. (El UUID de la plantilla también sirve.)

El mismo panel te dice qué tiene que rellenar la llamada:

  • Variables — las variables del cuerpo: o bien «2 variables posicionales» ({{1}}, {{2}}), o bien Nombre: seguido de los nombres.
  • Encabezado — su texto, o [IMAGE], [VIDEO] o [DOCUMENT] para un encabezado multimedia.
  • Botones — listados en orden, cada uno con su tipo ([URL], [COPY_CODE], [QUICK_REPLY]). El primero es el índice 0.

Desde un script, GET /api/wa-templates?status=approved lista las plantillas aprobadas con shortId, name, language y category.

Solo se puede enviar una plantilla aprobada; cualquier otro estado se rechaza.

La petición

El cuerpo tiene dos campos:

CampoQué es
toEl número de teléfono, en E.164 — +14255550123
componentsEl propio array template.components de Meta, pasado tal cual. Omítelo en una plantilla sin variables

components es el mismo array que enviarías a la Cloud API de Meta, así que se aplica la documentación de Meta sobre él. En resumen:

  • Encabezado — un parámetro: text para un encabezado de texto con una variable, o image, video o document con un link https://.
  • Cuerpo — parámetros text. Para una plantilla posicional ({{1}}, {{2}}), envíalos en orden y sin nombres. Para una plantilla con nombres ({{customer_name}}), da a cada parámetro su parameter_name. Nunca mezcles los dos. Una plantilla creada por Octo es posicional aquí, sean cuales sean los nombres que le diste.
  • Botones — un componente por cada botón que recibe un valor, con sub_type e index (su posición en la lista de botones de la plantilla):
    • url — un parámetro text: la parte del enlace que varía.
    • copy_code — el código, como el { "type": "coupon_code", "coupon_code": "FRESH20" } de Meta o como { "type": "text" }. Hasta 15 caracteres.
    • quick_reply — opcional; un parámetro { "type": "payload" } si quieres recibir un payload cuando se pulse.

Un ejemplo completo

La plantilla de recogida de Lavandería Tidewater, pickup_confirmation, tiene como cuerpo Hi {{1}}, your pickup is confirmed for {{2}}. Reply here to change it. y un botón Track pickup cuyo enlace termina en el número de pedido:

curl -X POST "https://api.omniocto.com/api/wa-templates/wt-a1b2c3d4e5/send" \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14255550123",
    "components": [
      { "type": "body", "parameters": [
        { "type": "text", "text": "Ana" },
        { "type": "text", "text": "Tuesday, 9–11am" }
      ] },
      { "type": "button", "sub_type": "url", "index": "0",
        "parameters": [{ "type": "text", "text": "TW-20931" }] }
    ]
  }'

Si la plantilla usara variables con nombre, el cuerpo nombraría cada valor:

{ "type": "body", "parameters": [
  { "type": "text", "parameter_name": "customer_name", "text": "Ana" },
  { "type": "text", "parameter_name": "pickup_slot", "text": "Tuesday, 9–11am" }
] }

Un éxito es 201:

{
  "messageId": "6f1c…",
  "conversationId": "3a9e…",
  "conversationShortId": "cv-9f8e7d6c5b",
  "externalId": "wamid.HBgL…",
  "isNewConversation": false,
  "template": { "id": "…", "shortId": "wt-a1b2c3d4e5", "name": "pickup_confirmation", "language": "en_US" }
}

externalId es el id contra el que Meta informará de la entrega y la lectura; esos avisos llegan a la conversación más tarde. 201 significa que Meta aceptó el envío, no que el teléfono lo recibió. Guarda conversationShortId: lo necesitarás más abajo.

Dónde aparece el mensaje

En /conversations, en la conversación con ese número en el número de WhatsApp de la plantilla — la misma conversación a la que llegan los mensajes del cliente:

  • Si ya existe una conversación con ese número, la plantilla se suma a ella — y si estaba cerrada, se reabre. isNewConversation es false.
  • Si no, se abre una conversación nueva (y se crea un contacto para un número desconocido). isNewConversation es true.
  • La burbuja muestra el cuerpo con tus valores, etiquetado Plantilla · y el nombre de la plantilla.

Cuando el cliente responde, la respuesta llega a esa misma conversación — y un envío por API no le pasa la conversación a una persona. Una conversación que ya existía la sigue atendiendo quien la atendía; una cerrada o nueva queda activa bajo el agente de IA que responde en ese número de WhatsApp. Así que ese agente, si el número tiene uno, responde a la respuesta, salvo que una persona haya tomado el control de la conversación en /conversations.

Cuando se rechaza un envío

EstadocodeSignificado
400—to no está en E.164, o components no tiene una forma que Meta acepte; el cuerpo lista los problemas
402billing_not_eligibleEl espacio de trabajo todavía no puede enviar mensajes salientes
404template_not_foundNo hay ninguna plantilla con ese id en este espacio de trabajo
409blocked_dncEl número se dio de baja. El mensaje se guarda en la conversación como bloqueado; nada llega a Meta
422template_not_approvedMeta no ha aprobado la plantilla; status dice en qué estado está
422template_components_mismatchcomponents no encaja con la plantilla. error nombra el problema
422channel_unavailableEl número de WhatsApp de la plantilla se eliminó o se deshabilitó
422channel_pending_verification, workspace_suspended, route_blockedUna comprobación de envío sobre el número o el espacio de trabajo lo rechazó. El mensaje se guarda en la conversación como bloqueado o fallido
429workspace_rate_limitedTu espacio de trabajo supera su tasa en este endpoint. No se escribe nada
429rate_limitedLa ventana de ráfaga del número de WhatsApp está llena: espera de segundos a minutos
429daily_limit_reachedEl tope diario de plantillas del número está agotado: la espera puede ser de horas
502send_failedMeta rechazó el envío; error lleva el mensaje de Meta. El mensaje queda en la conversación marcado como fallido

Todo 429 lleva una cabecera Retry-After y un retryAfterSec equivalente en el cuerpo. Espera ese tiempo y repite la misma llamada: la primera vez no se envió, guardó ni facturó ningún mensaje. (Un primer envío a un número nuevo que el presupuesto del número rechaza sí ha abierto la conversación, vacía; el cuerpo lleva su conversationShortId.)

Las dos familias de 429 son límites distintos. Tu espacio de trabajo puede llamar a este endpoint 300 veces por minuto por defecto, en ráfagas de hasta 10 por segundo; esa tasa cuenta solo llamadas a la API, y el soporte de Omni Octo la fija por espacio de trabajo. El número de WhatsApp tiene su propio presupuesto de envío, compartido con la bandeja, Octo y las campañas — qué tan rápido puede enviar un número lo explica.

Los desajustes no envían nada

La llamada compara components con la plantilla antes de escribir nada. Un valor que falta, uno que sobra, un nombre desconocido, un tipo de encabezado equivocado, un parámetro en un botón que no lo admite: cada uno es un 422 template_components_mismatch, sin mensaje guardado ni nada enviado. error dice exactamente qué está mal:

body expects 2 parameters, got 1
body is missing variable "pickup_slot"
button 0 is a QUICK_REPLY button, got url
button 0 (a URL button) is missing its url parameter

Así que un desajuste se puede corregir y reenviar sin riesgo.

La llamada no es idempotente

Cada llamada correcta envía — y factura — una plantilla. No hay clave de idempotencia. Si una llamada excede el tiempo de espera o se corta la conexión, no sabes si salió, y un reintento a ciegas puede enviarla dos veces.

Antes de reintentar tras un tiempo de espera agotado, revisa la conversación —

GET /api/conversations/{conversationShortId}

con el id de un envío anterior a ese número, o abre la conversación en /conversations. Reenvía solo si la plantilla no está.

Los rechazos de arriba son distintos: un 4xx te dice que el envío no ocurrió.

Lo que este endpoint no hace

  • Enviar texto libre. Solo envía plantillas aprobadas; para responder dentro de la ventana, usa la bandeja.
  • Enviar a una lista. Una llamada es un número; las campañas se ocupan de las listas.
  • Imponerse a Meta. Meta todavía puede rechazar una plantilla de marketing después de un 201 — ver por qué.

Adónde ir después