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.
- 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.
- 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 índice0.
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:
| Campo | Qué es |
|---|---|
to | El número de teléfono, en E.164 — +14255550123 |
components | El 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:
textpara un encabezado de texto con una variable, oimage,videoodocumentcon unlinkhttps://. - 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 suparameter_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_typeeindex(su posición en la lista de botones de la plantilla):url— un parámetrotext: 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.
isNewConversationesfalse. - Si no, se abre una conversación nueva (y se crea un contacto para un número
desconocido).
isNewConversationestrue. - 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
| Estado | code | Significado |
|---|---|---|
| 400 | — | to no está en E.164, o components no tiene una forma que Meta acepte; el cuerpo lista los problemas |
| 402 | billing_not_eligible | El espacio de trabajo todavía no puede enviar mensajes salientes |
| 404 | template_not_found | No hay ninguna plantilla con ese id en este espacio de trabajo |
| 409 | blocked_dnc | El número se dio de baja. El mensaje se guarda en la conversación como bloqueado; nada llega a Meta |
| 422 | template_not_approved | Meta no ha aprobado la plantilla; status dice en qué estado está |
| 422 | template_components_mismatch | components no encaja con la plantilla. error nombra el problema |
| 422 | channel_unavailable | El número de WhatsApp de la plantilla se eliminó o se deshabilitó |
| 422 | channel_pending_verification, workspace_suspended, route_blocked | Una 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 |
| 429 | workspace_rate_limited | Tu espacio de trabajo supera su tasa en este endpoint. No se escribe nada |
| 429 | rate_limited | La ventana de ráfaga del número de WhatsApp está llena: espera de segundos a minutos |
| 429 | daily_limit_reached | El tope diario de plantillas del número está agotado: la espera puede ser de horas |
| 502 | send_failed | Meta 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
- Trae tus plantillas a Omni Octo — la plantilla tiene que estar aprobada y sincronizada primero.
- Qué tan rápido puede enviar un número
— el presupuesto por número detrás de
rate_limitedydaily_limit_reached.