Guías
Tipos de mensaje
Todo sale por POST /v1/messages. Lo que cambia es content_type y lo que lo acompaña: texto, un archivo, una plantilla, botones, una ubicación o, en WhatsApp Cloud, un Flow o un botón de llamar.
Texto#
Es el tipo por defecto. Por WhatsApp Cloud solo sale dentro de las 24 horas siguientes al último mensaje del cliente; fuera de esa ventana usa una plantilla.
curl https://app.heysteff.com/api/v1/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp_cloud",
"to": "+56912345678",
"content": "Hola Ana, tu pedido ya va en camino"
}'Si mandas por cuenta de uno de tus clientes, agrega customer_id: el mensaje sale por el canal que ese cliente conectó con su link (ver Clientes y links de conexión). Responde 404 customer_not_found si el cliente no existe y 422 customer_channel_not_connected si no tiene un canal activo de ese tipo.
Imágenes, audio, video y documentos#
Primero sube el archivo con POST /v1/media (multipart, campo file, hasta 15 MB):
curl https://app.heysteff.com/api/v1/media \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-F "[email protected]"{
"data": {
"url": "/media/2c7e9b1a-3d4f-4a5b-8c6d-7e8f9a0b1c2d/9f2c41d0a7b3e815.pdf",
"mime_type": "application/pdf",
"filename": "catalogo.pdf",
"size": 184213
}
}Después manda su url en media.url, con el content_type que corresponde. content es opcional y va como pie.
curl https://app.heysteff.com/api/v1/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp_cloud",
"to": "+56912345678",
"content_type": "file",
"media": {
"url": "/media/2c7e9b1a-3d4f-4a5b-8c6d-7e8f9a0b1c2d/9f2c41d0a7b3e815.pdf",
"filename": "catalogo.pdf"
},
"content": "Te dejo el catálogo de noviembre"
}'| content_type | Archivos |
|---|---|
image | JPEG, PNG, WebP, GIF |
audio | MP3, M4A, OGG, WAV, WebM |
video | MP4, MOV |
file | PDF, Word, Excel, PowerPoint |
También sirve una URL https pública en media.url: heysteff la descarga al recibir el pedido (hasta 15 MB, sin seguir redirecciones) y la guarda como si la hubieras subido. Si no se puede descargar, la respuesta es 422 media_fetch_failed y no se envía nada. SVG, HTML y texto plano nunca se aceptan. Puedes subir o descargar hasta 30 archivos por minuto y 1.000 por día.
Plantillas de WhatsApp#
Para escribirle a alguien fuera de la ventana de 24 horas, WhatsApp exige una plantilla aprobada. GET /v1/templates lista las aprobadas de tus números, con cuántas variables lleva el cuerpo (body_param_count):
curl https://app.heysteff.com/api/v1/templates \
-H "Authorization: Bearer $HEYSTEFF_API_KEY"Para mandarla, pasa el nombre, el idioma y las variables del cuerpo en orden:
curl https://app.heysteff.com/api/v1/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1042-despacho" \
-d '{
"channel": "whatsapp_cloud",
"to": "+56912345678",
"content_type": "template",
"template": {
"name": "pedido_despachado",
"language": "es",
"body_params": ["Ana", "#1042"]
}
}'Si la plantilla lleva encabezado con imagen o botones con variables, manda template.components con la forma de la Graph API de Meta; gana sobre body_params. Una plantilla con otro nombre, otro idioma o un número distinto de variables responde 422 antes de enviar nada.
Botones y listas#
Con content_type: "interactive" mandas una pregunta con hasta 3 botones (type: "buttons") o una lista de opciones en secciones (type: "list", con button como el texto que abre la lista). El id de cada opción es lo que vuelve cuando el cliente la toca.
curl https://app.heysteff.com/api/v1/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp_cloud",
"to": "+56912345678",
"content_type": "interactive",
"interactive": {
"type": "buttons",
"body": "¿Confirmas tu hora del jueves a las 10:00?",
"buttons": [
{ "id": "confirmar", "title": "Confirmo" },
{ "id": "cambiar", "title": "Cambiar hora" }
]
}
}'Una lista por Instagram:
{
"channel": "instagram",
"to": "17841400000000000",
"content_type": "interactive",
"interactive": {
"type": "list",
"body": "¿Desde qué sucursal lo retiras?",
"button": "Ver sucursales",
"sections": [
{ "title": "Santiago", "rows": [
{ "id": "providencia", "title": "Providencia", "description": "Av. Providencia 123" },
{ "id": "nunoa", "title": "Ñuñoa" }
] }
]
}
}Cada canal lo muestra como puede. La respuesta trae delivery y degraded: true cuando el canal no tiene el formato:
| Canal | Botones | Lista |
|---|---|---|
| WhatsApp Cloud | Nativos | Nativa hasta 10 filas (en una sola sección); con más, texto numerado |
| Instagram, Messenger | Quick replies | Quick replies hasta 13 opciones; con más, texto numerado |
| SMS, email, WhatsApp por QR | Texto con opciones numeradas | Texto con opciones numeradas |
Cuando el cliente toca una opción, el webhook message:received trae interactive_reply con el id y el título. Si responde escribiendo, llega como texto normal y interactive_reply es null.
{
"event": "message:received",
"data": {
"message_id": "4e3d2c1b-0a9f-4e8d-7c6b-5a4f3e2d1c0b",
"content": "Confirmo",
"interactive_reply": { "id": "confirmar", "title": "Confirmo" }
}
}Botones, listas y ubicaciones son mensajes de sesión: en WhatsApp Cloud, Instagram y Messenger solo salen dentro de las 24 horas desde el último mensaje del cliente. Fuera de la ventana responde 409 wa_window_closed o meta_window_closed sin enviar nada.
Ubicación#
Con content_type: "location" mandas un punto en el mapa. En WhatsApp Cloud llega como ubicación nativa; en el resto de los canales, como texto con el nombre, la dirección y un enlace de Google Maps (degraded: true).
{
"channel": "whatsapp_cloud",
"to": "+56912345678",
"content_type": "location",
"location": {
"latitude": -33.4489,
"longitude": -70.6693,
"name": "Tienda Centro",
"address": "Alameda 123, Santiago"
}
}Reacciones#
Con content_type: "reaction" reaccionas con un emoji a un mensaje que ya existe (no hace falta to). emoji: "" quita la reacción. Funciona en WhatsApp (Cloud y por QR; en Cloud, dentro de las 24 horas) y en Instagram, conectado por página o sin página de Facebook (Instagram Login), dentro de las 24 horas. Instagram solo tiene una reacción, el corazón: acepta ❤️ (también ❤, ♥️ y ♥, que se guardan como ❤️) y cualquier otro emoji responde 422 reaction_emoji_unsupported. Messenger no permite reaccionar por API. Responde 202: el envío lo hace la cola. Una key de prueba (sk_test_) no reacciona: responde 403 test_key_not_allowed.
{
"channel": "whatsapp_cloud",
"content_type": "reaction",
"reaction": { "message_id": "4e3d2c1b-0a9f-4e8d-7c6b-5a4f3e2d1c0b", "emoji": "👍" }
}Flows, botón de llamar y permiso de llamada#
Tres tipos que solo existen en WhatsApp Cloud: por otro canal (o si el remitente no es un número de WhatsApp Cloud) responden 422 whatsapp_cloud_only, sin degradarse a texto ni a un link, porque el cliente no podría abrir el formulario ni llamar. Necesitan una key sk_live_ (una de prueba responde 403 test_key_not_allowed) y la ventana de 24 horas abierta (si no, 409 wa_window_closed; fuera de la ventana usa una plantilla con botón FLOW o de llamar). Si Meta rechaza uno, el mensaje queda failed.
content_type: "flow"manda un WhatsApp Flow (un formulario dentro del chat) conflow: { flow_id, cta, body, header?, footer?, flow_token?, mode?, action?, screen?, data? }. La respuesta llega enmessage:receivedconflow_response: { flow_token, data }. Ver WhatsApp Flows.content_type: "voice_call"manda un botón para que el cliente te llame por WhatsApp:voice_call: { body, display_text?, ttl_minutes?, payload? }.content_type: "call_permission_request"le pide permiso al cliente para llamarlo:call_permission: { body }. Ver Llamadas de WhatsApp.
{
"channel": "whatsapp_cloud",
"to": "+56912345678",
"content_type": "flow",
"flow": {
"flow_id": "1234567890123456",
"cta": "Agendar",
"body": "Elige el día de tu visita.",
"flow_token": "pedido-1042"
}
}Estado y leído#
GET /v1/messages/{id} (scope conversations:read) devuelve el estado de un mensaje: queued, sent, delivered, read o failed (con error), con sent_at, delivered_at y read_at. Los cambios también llegan por webhook: message.delivered, message.read y message.failed.
curl https://app.heysteff.com/api/v1/messages/1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f \
-H "Authorization: Bearer $HEYSTEFF_API_KEY"POST /v1/messages/{id}/read marca como leído un mensaje del cliente: la conversación queda leída en heysteff y el cliente ve las tildes azules en WhatsApp Cloud o el "visto" en Instagram y Messenger (Meta marca la conversación entera). SMS, email y WhatsApp por QR no tienen recibo: responde supported: false.
curl -X POST https://app.heysteff.com/api/v1/messages/4e3d2c1b-0a9f-4e8d-7c6b-5a4f3e2d1c0b/read \
-H "Authorization: Bearer $HEYSTEFF_API_KEY"Un texto más largo que el canal (Instagram 1.000 caracteres, Messenger 2.000, WhatsApp 4.096) sale en varias piezas en orden; la respuesta trae parts con el id de cada una.
Crear y borrar plantillas#
Con el scope templates:write, POST /v1/templates crea una plantilla de WhatsApp y la manda a revisión de Meta (queda PENDING; mira el estado con GET /v1/templates?status=all). DELETE /v1/templates/{name} la borra en Meta y en heysteff (todas sus traducciones, o solo ?language=es). Los encabezados con imagen, video o documento se crean desde el panel.
{
"name": "pedido_despachado",
"language": "es",
"category": "UTILITY",
"body_text": "Hola {{1}}, tu pedido {{2}} va en camino.",
"body_examples": ["Ana", "#1042"]
}Errores frecuentes#
| Código | Qué pasó |
|---|---|
invalid_media | media.url es de otro espacio de trabajo. |
media_not_found | El archivo ya no existe: vuelve a subirlo. |
template_not_found | No hay una plantilla con ese nombre e idioma en ese número. |
template_param_mismatch | La cantidad de body_params no coincide con la plantilla. |
wa_window_closed | Botones, lista, ubicación, Flow o botón de llamar por WhatsApp Cloud fuera de las 24 horas: manda una plantilla. |
meta_window_closed | Lo mismo en Instagram o Messenger. |
reactions_unsupported | El canal no permite reaccionar por API (Messenger, SMS, email). Las reacciones existen en WhatsApp e Instagram. |
reaction_emoji_unsupported | Instagram solo acepta ❤️ como reacción. |
whatsapp_cloud_only | Un Flow, un botón de llamar o un pedido de permiso por un canal que no es WhatsApp Cloud. |
not_inbound | Solo se marca como leído un mensaje del cliente. |