developers
EN Crear cuenta

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
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
curl https://app.heysteff.com/api/v1/media \
  -H "Authorization: Bearer $HEYSTEFF_API_KEY" \
  -F "[email protected]"
Respuesta
{
  "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
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_typeArchivos
imageJPEG, PNG, WebP, GIF
audioMP3, M4A, OGG, WAV, WebM
videoMP4, MOV
filePDF, 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
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
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
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:

json
{
  "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:

CanalBotonesLista
WhatsApp CloudNativosNativa hasta 10 filas (en una sola sección); con más, texto numerado
Instagram, MessengerQuick repliesQuick replies hasta 13 opciones; con más, texto numerado
SMS, email, WhatsApp por QRTexto con opciones numeradasTexto 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.

json
{
  "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).

json
{
  "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.

json
{
  "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.

json
{
  "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
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
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.

json
{
  "name": "pedido_despachado",
  "language": "es",
  "category": "UTILITY",
  "body_text": "Hola {{1}}, tu pedido {{2}} va en camino.",
  "body_examples": ["Ana", "#1042"]
}

Errores frecuentes#

CódigoQué pasó
invalid_mediamedia.url es de otro espacio de trabajo.
media_not_foundEl archivo ya no existe: vuelve a subirlo.
template_not_foundNo hay una plantilla con ese nombre e idioma en ese número.
template_param_mismatchLa cantidad de body_params no coincide con la plantilla.
wa_window_closedBotones, lista, ubicación, Flow o botón de llamar por WhatsApp Cloud fuera de las 24 horas: manda una plantilla.
meta_window_closedLo mismo en Instagram o Messenger.
reactions_unsupportedEl canal no permite reaccionar por API (Messenger, SMS, email). Las reacciones existen en WhatsApp e Instagram.
reaction_emoji_unsupportedInstagram solo acepta ❤️ como reacción.
whatsapp_cloud_onlyUn Flow, un botón de llamar o un pedido de permiso por un canal que no es WhatsApp Cloud.
not_inboundSolo se marca como leído un mensaje del cliente.