Mensajes

Todo lo que podés mandar y recibir en una conversación 1:1, qué responde cada envío y cómo saber qué puede hacer cada número.

Todo lo que podés mandar y recibir en una conversación 1:1, con la misma llamada para un número oficial de Meta y para uno por QR.

PiezaEstado
Recibir mensajes (message.received) y leer el historialDisponible
Enviar texto, archivos, botones, listas, plantillas, formularios de WhatsApp, tarjetas de contacto, reacciones y reenvíosDisponible, con el permiso conversations:send y las reglas de WhatsApp
Saber qué pasó con cada envío (message.status_changed y la consulta del mensaje)Disponible: Estados de mensaje
Ubicación, nota de voz, encuestas e invitaciones a eventosTodavía no hay una ruta para mandarlos

Qué podés mandar

TipoCómoEn capabilities.messages
TextoEnviar un mensaje a un contacto, con texttext
Imagen, video, documento, audio o stickerLa misma ruta, con file: la URL pública del archivo (abajo)media
Botones o listaLa misma ruta, con options (de 1 a 10, cada una con id y label)buttons, lists
Plantilla aprobadaEnviar una plantillatemplates
Formulario de WhatsAppEnviar un formulario, solo por un número de WhatsApp Cloud—
Tarjeta de contactoEnviar una tarjeta de contactocontact_card
ReacciónReaccionar a un mensajereactions
ReenvíoReenviar un mensaje—

Qué puede cada número lo dice su ficha. Listar números trae en cada uno capabilities.messages, con un valor verdadero o falso por tipo: miralo antes de mandar en lugar de suponerlo por el tipo de número (Números). La ficha también tiene location, voice_note, polls y events, pero hoy la API no tiene una ruta para mandarlos.

  • Todo envío pide conversations:send y pasa por las reglas de WhatsApp: la ventana de 24 horas en los números de WhatsApp Cloud, los frenos contra spam y el ritmo por número (hasta 60 mensajes por segundo en un número de WhatsApp Cloud, 15 en coexistencia y sin límite por QR).
  • Por la API solo se les escribe a personas: si el contacto es un grupo, una lista de difusión o un canal, responde 422 recipient_unsupported.
  • Recomendamos el número oficial (por qué).

Un texto

curl -X POST "https://api.funnelchat.com/api/v1/contacts/2001/message" \
  -H "Authorization: Bearer <id>|fc_live_..." \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": 3,
    "contact_id": 2001,
    "text": "¡Hola! Te paso la info…",
    "client_reference": "pedido-1001"
  }'
  • device_id es el número desde el que sale (Listar números). contact_id va también en el cuerpo: la especificación lo pide ahí, además de en la ruta.
  • Responde 200 con message: el mensaje ya registrado, con su id y status: "queued". La llamada no espera a WhatsApp: el mensaje sale enseguida, desde la cola. En cola no es enviado, y un mensaje queued no se reintenta: reenviarlo lo duplicaría. Qué pasó después te lo cuenta Estados de mensaje.
  • client_reference (opcional, de 1 a 64 caracteres visibles, sin espacios) es tu propia referencia del envío: vuelve en el historial del contacto y en message.status_changed. No es única ni secreta (la ven todas las claves y todas las suscripciones de la cuenta): relacionala siempre junto con message_id y contact_id, y no pongas datos personales.
  • Si una regla de WhatsApp frena el envío, responde 422 o 429 con su código y no se registra nada (Reglas de WhatsApp).
  • Un rechazo por el límite de contactos activos (MAC) es distinto: queda en el historial del contacto un mensaje con estado de error que lleva tu client_reference. Si tu cuenta está en bloqueo (Ver el consumo de MACs responde state = enforcing) y el contacto es inactivo (is_mac_inactive en true), el envío responde 429 con mac_lockout_24h. No trae Retry-After: vuelve a funcionar cuando el contacto deja de ser inactivo, al sumar capacidad o al empezar un ciclo nuevo con cupo. A los contactos que ya son activos en el ciclo les seguís escribiendo con normalidad.

Un archivo por URL

No subís el archivo a Funnelchat: en file mandás la URL pública del archivo y WhatsApp o el proveedor lo descarga desde ahí.

curl -X POST "https://api.funnelchat.com/api/v1/contacts/2001/message" \
  -H "Authorization: Bearer <id>|fc_live_..." \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": 3,
    "contact_id": 2001,
    "file": "https://cdn.example.com/facturas/2026-10.pdf",
    "text": "Te paso la factura"
  }'
  • El tipo sale de la extensión de la URL: .jpg, .jpeg y .png son imagen; .webp, sticker; .ogg, .oga, .mp3, .m4a, .aac, .opus y .wav, audio; .gif y .mp4, video; .doc, .docx y .pdf, documento. Si la URL no termina en una de esas (por ejemplo una URL firmada sin extensión), mandá file_type (image, video, audio, document o sticker); si no, responde 422. Una extensión de otro formato, como .xlsx, sin file_type responde 415.
  • file_name es el nombre con el que el contacto ve un documento, con su extensión (factura.pdf). Es obligatorio en un documento cuya URL no termina en una extensión de la lista.
  • La URL tiene que durar al menos una hora: un envío con clave puede esperar en cola hasta 54 minutos, y Funnelchat no la abre al recibir tu llamada. Un archivo inaccesible, vencido o fuera de los límites termina en un mensaje failed con el error del proveedor.
  • No pongas secretos en la URL. La URL completa, con su consulta, queda en el historial del contacto, la ven los usuarios de tu cuenta y se copia al reenviar. Usá URLs firmadas de corta duración (cerca de una hora) y para un solo archivo.
  • Tamaños máximos de WhatsApp: imagen 5 MB, video y audio 16 MB, documento 100 MB, sticker 100 KB (500 KB si es animado). Los formatos aceptados de cada tipo están en la página del endpoint.

Una plantilla

Fuera de la ventana de 24 horas, un número de WhatsApp Cloud solo puede mandar plantillas aprobadas por Meta. Elegí una en Listar plantillas (su status dice si está aprobada) y mandala con su id en la ruta:

curl -X POST "https://api.funnelchat.com/api/v1/devices/3/templates/45/send-template" \
  -H "Authorization: Bearer <id>|fc_live_..." \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "5491100000000",
    "contact_id": 2001,
    "template_variables": { "body": ["Lucía"] },
    "client_reference": "recordatorio-1001"
  }'
  • Responde 200 con message, en cola, igual que un texto.
  • Si una variable se completa con un campo personalizado que no existe en tu cuenta, responde 422 template_unresolved_placeholder con placeholders y count, y no manda nada. Si la plantilla no existe en tu cuenta, 404 template_not_found.
  • Cuando Meta aprueba, rechaza o pausa una plantilla, te llega template.status_changed (catálogo).

Un formulario de WhatsApp

Enviar un formulario le manda al contacto un formulario ya publicado, con form_id y el número de WhatsApp Cloud por el que sale (device_id); text y button_text son opcionales. Solo funciona dentro de la ventana de 24 horas. Responde 200 con el mensaje en cola, y lo que el contacto contestó te llega en el evento whatsapp_form.completed, con answers. Sus errores propios (form_not_found, form_not_published, form_not_on_number, number_not_eligible…) están en la página del endpoint.

Crear y publicar formularios todavía no se puede por la URL base: se hace desde Funnelchat.

Qué recibís

  • Cada mensaje entrante llega a tu servidor como message.received, firmado: el mensaje (id, type, text, media), el contacto (id, name, phone), el teléfono del número que lo recibió (receiver.phone) y su device_id. Paso a paso: Recibir mensajes.
  • El historial de un contacto está en Listar los mensajes de un contacto, con paginación por cursor y updated_since para traer solo lo nuevo. Paso a paso: Leer conversaciones.
  • Un mensaje por su id, con su estado y tu client_reference: Ver un mensaje.
  • Qué pasó con cada mensaje que mandaste (enviado, entregado, leído, falló): Estados de mensaje.

Probalo acá

Esto manda un mensaje real

Sale por WhatsApp desde tu número y pasa por las mismas reglas que tu código.

Enviar un mensaje a un contacto

POST
/contacts/{contact}/message

Permiso requerido: conversations:send

Autorización

http
headerAuthorizationBearer <clave>

Parámetros de la ruta

contact*integer

Cuerpo del pedido

application/json
  1. body
phone?string
Largo1 <= length
whatsapp_id?string
Largo1 <= length
device_id*integer
text?string
contact_id*integer
file?string

Archivo del mensaje: el nombre de un archivo ya subido a tu cuenta o la URL pública de un archivo (te recomendamos https://), que WhatsApp o el proveedor descarga; admite hasta 4096 caracteres. Si empieza con http, en mayúsculas o minúsculas, tiene que ser una URL http:// o https://; si no, la respuesta es 422. Consulta la sección «Enviar un archivo por URL».

Patrón^(?:[Hh][Tt][Tt][Pp][Ss]?:\/\/.+|(?!\.\.?$)(?![Hh][Tt][Tt][Pp])[^\/\\\x00]+)$
Largolength <= 4096
file_type?|

Tipo del archivo. Es obligatorio cuando file es una URL que no termina en una extensión reconocida; si lo envías, manda sobre la extensión. Valores: image, video, audio, document, sticker. Consulta la sección «Enviar un archivo por URL».

Valor en"image""video""audio""document""sticker"null
file_name?|

Nombre con el que el contacto ve un documento, con su extensión (por ejemplo factura.pdf), de hasta 255 caracteres. Es obligatorio al enviar un documento por URL que no termina en una extensión de la lista. Consulta la sección «Enviar un archivo por URL».

Largo1 <= length <= 255
temporal_uuid?string
note?boolean
options?array<>
Elementos1 <= items <= 10
reference_message_uid?|
Largolength <= 255
client_reference?|
Patrón^[\x21-\x7E]{1,64}$
Largolength <= 64
link_preview?boolean
link_preview_meta?

Cuerpo de la respuesta

OK

application/json
  1. response
message*|null
curl -X POST "https://example.com/contacts/0/message" \  -H "Content-Type: application/json" \  -d '{    "device_id": 0,    "contact_id": 0  }'
{  "message": {    "id": 0,    "contact_id": 0,    "device_id": 0,    "direction": "outbound",    "type": "text",    "status": "sent",    "error": {      "code": 0,      "title": "string",      "reason": "string"    },    "client_reference": "string",    "created_at": "string",    "updated_at": "string"  }}

En esta página