WhatsApp Cloud API
Todo lo necesario para enviar y recibir mensajes a través del WABA del negocio: mensajes simples y con plantilla, multimedia, botones y listas interactivas, marcar como leído, gestionar plantillas, perfil del negocio, diagnostics del número y estadísticas.
Para conceptos comunes (autenticación, scopes, errores, idempotencia) andá a la página principal. Para recibir mensajes entrantes ver Webhooks. Para conectar el WhatsApp del negocio por primera vez ver Onboarding.
Enviar mensajes
POST /v1/messages
Plantilla simple (primer mensaje a un contacto):
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US"
}
}
Plantilla con variables:
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "template",
"template": {
"name": "pedido_listo",
"language": "es",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Juan" },
{ "type": "text", "text": "12345" }
]
}
]
}
}
Texto libre (solo si la conversación tiene una sesión activa de 24h):
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "text",
"text": {
"body": "Hola, gracias por escribirnos. ¿En qué te ayudo?"
}
}
Errores:
403 plan_not_included— el plan de tu cuenta no incluye WhatsApp. Pasate a un plan que lo incluya desde el panel.402 subscription_suspended— la suscripción de la cuenta está suspendida. Se reactiva desde el panel.409 tenant_not_connected— el negocio no completó el onboarding de su WhatsApp.409 meta_outside_session_window— pasaron más de 24 horas desde el último mensaje del contacto (Meta 131047). Fuera de esa ventana solo se puede mandar una plantilla.422 meta_recipient_unreachable— el número no tiene WhatsApp, bloqueó al negocio, no está en el allowlist de una cuenta de prueba, o la cuenta del negocio está bloqueada (131031).422 meta_template_invalid— la plantilla no existe, no está aprobada o los parámetros no coinciden (Meta 132xxx).422 invalid_payload— el body no arma un mensaje válido para esetype.429 meta_rate_limited— Meta frenó al número por velocidad de envío. Esperá unos segundos.503 meta_token_invalid— se perdió el acceso al WhatsApp del negocio; tiene que reconectar el número.502 meta_other— cualquier otro rechazo de Meta; el código original viene enmeta_code.
Todos estos vienen como {"ok": false, "error", "message", "meta_code"}.
La respuesta de un envío exitoso es 201 (o 200 si fue
un reintento con la misma Idempotency-Key)
con la misma forma que GET /v1/messages/{id}.
GET /v1/messages/{id}
Consulta el estado actual de un mensaje enviado. El {id} es el
id que te devolvió POST /v1/messages — no el
wamid de Meta.
GET /v1/messages/019dcf...
Respuesta:
{
"id": "019dcf...",
"tenant_ref": "pizzeria-don-mario",
"channel": "whatsapp",
"type": "template",
"status": "delivered",
"to": "5491155555555",
"message_id": "wamid.HBg...",
"waba_message_id": "wamid.HBg...",
"error_code": null,
"error_message": null,
"idempotency_key": "pedido-12345-confirmacion",
"created_at": "2026-05-07T15:30:00Z",
"updated_at": "2026-05-07T15:30:12Z"
}
status: queued → sent →
delivered → read, o failed con
error_code y error_message de Meta. No retrocede:
un delivered que llega tarde no pisa un read.
waba_message_id y message_id son lo mismo (el
primero queda por compatibilidad y solo existe en WhatsApp). Si el
id no es de tu cuenta, 404 message_not_found.
Imágenes, audio, video y documentos
Adjuntá multimedia por URL pública (Meta la descarga). Los formatos aceptados los define Meta — los más comunes andan sin problema.
Imagen
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "image",
"image": {
"link": "https://tu-cdn.com/pizza.jpg",
"caption": "Hoy en promo: muzza con fainá."
}
}
Video
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "video",
"video": {
"link": "https://tu-cdn.com/horno.mp4",
"caption": "Recién salida del horno."
}
}
Audio
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "audio",
"audio": { "link": "https://tu-cdn.com/aviso.ogg" }
}
Documento
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "document",
"document": {
"link": "https://tu-cdn.com/factura-12345.pdf",
"filename": "factura-12345.pdf",
"caption": "Tu factura."
}
}
Sticker
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "sticker",
"sticker": { "link": "https://tu-cdn.com/gracias.webp" }
}
Stickers tienen que ser .webp y cumplir el tamaño exacto
que pide Meta (512x512 px, máx 100 KB para estáticos). Si Meta los
rechaza, te devolvemos meta_other con el detalle.
Botones y listas
Los mensajes interactivos permiten que el contacto te responda con un
click. Las respuestas te llegan por webhook como message.received
con type: "interactive": el id que pusiste acá vuelve
tal cual en data.message.interactive.button_reply.id (o
list_reply.id), y data.message.context.id te dice a
qué mensaje tuyo están contestando. La forma exacta está en
Webhooks → cuando tocan un botón.
El botón de una plantilla no es esto. Un quick reply de
template llega con type: "button" y
button.payload, no como interactive. Son dos formas
distintas y conviene manejar las dos.
Botones de respuesta rápida (Quick Reply)
Hasta 3 botones. El id es lo que te llega cuando aprieta.
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "¿Confirmás tu pedido #12345?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "ok", "title": "Sí, confirmar" } },
{ "type": "reply", "reply": { "id": "cancel", "title": "No, cancelar" } }
]
}
}
}
Lista de opciones
Hasta 10 opciones agrupadas en secciones. Útil para menús.
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "Elegí una opción." },
"action": {
"button": "Ver menú",
"sections": [
{
"title": "Pizzas",
"rows": [
{ "id": "muzza", "title": "Muzzarella", "description": "$3500" },
{ "id": "napoli", "title": "Napolitana", "description": "$4200" }
]
},
{
"title": "Bebidas",
"rows": [
{ "id": "coca", "title": "Coca 1.5L", "description": "$1800" }
]
}
]
}
}
}
Ubicación, contactos y reacciones
Ubicación
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "location",
"location": {
"latitude": -34.6037,
"longitude": -58.3816,
"name": "Pizzería Don Mario",
"address": "Av. Corrientes 1234, CABA"
}
}
Contacto
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "contacts",
"contacts": [
{
"name": { "formatted_name": "Mario Rossi", "first_name": "Mario" },
"phones": [
{ "phone": "+5491155551234", "type": "CELL" }
]
}
]
}
Reacción a un mensaje
Reaccionás con un emoji a un mensaje que te mandó el contacto.
Necesitás el waba_message_id que te llegó por webhook.
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBg...",
"emoji": "👍"
}
}
Para sacar una reacción, mandá la misma estructura con
"emoji": "" (string vacío).
Reglas de Meta: el mensaje tiene que tener menos de 30 días,
no puede estar borrado ni ser a su vez una reacción. Si no se cumple, la
reacción no se entrega y te llega un message.status
failed con el código 131009. De una reacción solo
vas a ver el estado sent: Meta no manda delivered
ni read para reacciones.
Las reacciones del contacto también te llegan: como un
message.received con type: "reaction" y
reaction.message_id apuntando al mensaje reaccionado. Ver
Webhooks.
Responder a un mensaje
Cualquier tipo de mensaje puede ser reply a un mensaje
anterior — visualmente aparece citado en el chat del contacto.
Agregá context.message_id con el waba_message_id
del mensaje al que respondés:
POST /v1/messages
{
"tenant_ref": "pizzeria-don-mario",
"to": "5491155555555",
"type": "text",
"context": { "message_id": "wamid.HBg..." },
"text": { "body": "Sí, está abierto hasta las 23." }
}
Marcar como leído
POST /v1/messages/mark-read
Le decimos a Meta que tu sistema ya leyó el mensaje del contacto (el contacto va a ver el doble tilde azul). Opcionalmente activa el indicador "escribiendo…".
POST /v1/messages/mark-read
{
"tenant_ref": "pizzeria-don-mario",
"waba_message_id": "wamid.HBg...",
"typing_indicator": { "type": "text" }
}
waba_message_id es el data.message.id del
message.received. typing_indicator es opcional y va
como objeto — { "type": "text" } es el único tipo que acepta
Meta hoy; un true suelto devuelve 422. Sin él, solo
marca como leído.
El indicador "escribiendo…" dura ~25 segundos o hasta que mandes el próximo mensaje (lo que ocurra primero). Siempre viaja junto con el leído: es el mismo pedido a Meta, así que no hay forma de mostrar "escribiendo…" sin que el contacto vea el doble tilde azul.
Acuse con reacción. Si además querés reaccionar apenas
llega el mensaje (un 👀, por ejemplo), es un
POST /v1/messages con
type: "reaction" aparte, con el mismo
waba_message_id. Son dos llamadas por mensaje recibido más tu
respuesta: tenelo en cuenta contra el límite de 60 por minuto de la API key.
Descargar media recibida
GET /v1/media/{id}
Cuando un contacto te manda una foto, audio, video o documento, en el
webhook message.received llega un id dentro de la
clave del tipo (data.message.image.id,
data.message.audio.id, …). Bajalo desde nuestro endpoint —
proxiamos con el token de Meta así no tenés que manejarlo vos.
El webhook también trae una url de Meta: no la uses.
Esa URL vive 5 minutos y hay que pedirla con el token de
Meta del negocio, que no exponemos a nadie. Nosotros pedimos una URL fresca
cada vez que llamás a este endpoint, así que del lado tuyo no hay ventana de
5 minutos: guardá el id y bajá el archivo cuando lo necesites.
GET /v1/media/MID01?tenant_ref=pizzeria-don-mario
Authorization: Bearer TU_API_KEY
Respuesta: el binario crudo, con headers:
Content-Type: el MIME real (ejimage/jpeg).Content-Length: tamaño en bytes.X-Sabado-Media-SHA256: hash del archivo para integridad.
Si querés sugerir un nombre de archivo para descarga directa:
GET /v1/media/MID01?tenant_ref=pizzeria-don-mario&filename=foto.jpg
Historial de coexistencia
GET /v1/history
Solo en números con coexistencia. Al conectar un negocio que sigue usando la app de WhatsApp Business en el celular, Meta sincroniza los chats previos (en los minutos posteriores al onboarding) y después avisa de cada mensaje que el negocio manda desde el celular. Todo eso queda guardado y lo leés con este endpoint — ideal para minar el historial y armarte el contexto de cada cliente.
GET /v1/history?tenant_ref=pizzeria-don-mario&limit=50
Authorization: Bearer TU_API_KEY
Respuesta:
{
"tenant_ref": "pizzeria-don-mario",
"data": [
{
"id": 812,
"wa_message_id": "wamid.HBg...",
"source": "history",
"direction": "inbound",
"from": "5491155555555",
"to": "5493412004406",
"peer": "5491155555555",
"type": "text",
"status": "DELIVERED",
"timestamp": "2026-08-01T18:22:10-03:00",
"message": { "id": "wamid.HBg...", "type": "text", "text": { "body": "¿Hacen envíos?" } }
}
],
"paging": { "count": 1, "last_id": 812, "has_more": false }
}
source:history= mensaje del sync inicial;echo= mensaje mandado desde el celular después del onboarding.peer: el cliente final (el otro lado de la conversación), venga el mensaje de ida o de vuelta.message: el objeto crudo de Meta con el contenido (text.body, media, etc.) — el mismo shape que viaja en los webhooks.
Filtros opcionales (query params):
peer: solo la conversación con ese contacto.source:historyoecho.direction:inbound/outbound.limit: 1 a 100 (default 50).since_id: cursor — devuelve solo filas conidmayor.
Para recorrerlo entero: pedí sin since_id, guardá el
paging.last_id y repetí con since_id=last_id
mientras has_more sea true. El orden es por
llegada (no cronológico): los chats previos llegan en tandas, así que
cada item trae su timestamp original para que ordenes vos.
El sync inicial de Meta llega algunos minutos después
del onboarding y en tandas. Si recién conectaste el negocio y esto viene
vacío, reintentá en unos minutos. El aviso en tiempo real de "el dueño
respondió desde el celular" es el webhook
message.echo —
este endpoint es la copia persistida, para leer a tu ritmo.
Plantillas
POST /v1/templates
Crear una plantilla en nombre del negocio. Meta la pasa a revisión.
POST /v1/templates
{
"tenant_ref": "pizzeria-don-mario",
"name": "pedido_listo",
"language": "es",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hola {{1}}, tu pedido #{{2}} ya está listo para retirar.",
"example": { "body_text": [["Juan", "12345"]] }
},
{
"type": "FOOTER",
"text": "Sabado Cloud · Notificación automática"
}
]
}
Reglas que aplica Meta:
- Nombre: solo lowercase, números, guiones bajos.
- Body: máximo 1024 chars. Variables
{{1}}, {{2}}, ...en orden. - Example: obligatorio cuando hay variables.
- Category:
UTILITY(notificaciones operativas),MARKETING(promocionales) oAUTHENTICATION(OTP).
La respuesta es 201 con la plantilla creada; el
status arranca en PENDING — Meta todavía no la
revisó. Cómo seguir, abajo.
Qué pasa después de crear una plantilla
Meta revisa en cualquier momento entre minutos y horas. Tenés dos caminos para enterarte, y conviene usar los dos:
-
Te avisamos. Cuando Meta resuelve, te mandamos el webhook
template.status_changedcon el payload crudo de Meta: el estado nuevo viene endata.event(APPROVED,REJECTED, …), el motivo endata.reasony el nombre endata.message_template_name. -
Preguntás vos.
GET /v1/templates/{name}te devuelve el estado actual sin esperar nada.
GET /v1/templates
Listar las plantillas del negocio.
GET /v1/templates?tenant_ref=pizzeria-don-mario&limit=20
GET /v1/templates/{name}
El estado de una plantilla puntual. Sin language te devuelve
todos los idiomas con ese nombre.
GET /v1/templates/pedido_listo?tenant_ref=pizzeria-don-mario&language=es
{
"tenant_ref": "pizzeria-don-mario",
"data": [
{
"id": "27686102984401620",
"name": "pedido_listo",
"language": "es",
"category": "UTILITY",
"status": "REJECTED",
"components": [ ... ],
"quality_score": null,
"rejected_reason": "INVALID_FORMAT"
}
]
}
Los status que vas a ver casi siempre son PENDING,
APPROVED y REJECTED (Meta también usa
PAUSED, DISABLED y algún otro según el caso). Cuando
es REJECTED, el motivo está en rejected_reason. Si no
existe ninguna plantilla con ese nombre, 404
template_not_found.
DELETE /v1/templates/{name}
Borrar una plantilla.
DELETE /v1/templates/pedido_listo?tenant_ref=pizzeria-don-mario
Errores al crear una plantilla
Cuando Meta rechaza una plantilla casi siempre devuelve un
message genérico ("Invalid parameter",
meta_code: 100) y pone el motivo concreto aparte. Te lo
pasamos en meta_detail (y el subcódigo en
meta_subcode): leé ese campo para saber qué corregir.
{
"ok": false,
"error": "meta_template_invalid",
"message": "Invalid parameter — Body text contains variables without examples.",
"meta_code": 100,
"meta_subcode": 2388043,
"meta_detail": "Body text contains variables without examples."
}
Causas frecuentes de meta_code: 100:
- Variables
{{1}}en el body sin elexamplecorrespondiente. typedel componente en minúscula — debe irBODY/HEADER/BUTTONS/FOOTERen mayúscula.- Botones o header mal formados (URL sin ejemplo, teléfono con formato inválido, etc.).
namecon mayúsculas o espacios (solo lowercase, números y guiones bajos).
No es un bloqueo de cuenta: crear plantillas no requiere business
verification. Si todas tus plantillas fallan con el mismo code 100,
el problema está en el payload, no en permisos — mirá meta_detail.
Perfil del negocio
Datos visibles cuando el contacto toca el nombre del negocio dentro de WhatsApp: descripción, sitio web, rubro, etc. El cliente final también lo puede editar desde su panel.
GET /v1/business-profile
GET /v1/business-profile?tenant_ref=pizzeria-don-mario
Authorization: Bearer TU_API_KEY
Respuesta:
{
"tenant_ref": "pizzeria-don-mario",
"profile": {
"about": "La mejor pizza al horno de barro.",
"address": "Av. Corrientes 1234, CABA",
"description": "Pizzería tradicional desde 1985...",
"email": "contacto@donmario.com.ar",
"vertical": "RESTAURANT",
"websites": ["https://donmario.com.ar"],
"profile_picture_url": "https://...",
"messaging_product": "whatsapp"
}
}
PUT /v1/business-profile
Mandá solo los campos que querés cambiar. Lo que no mandes queda intacto.
PUT /v1/business-profile
{
"tenant_ref": "pizzeria-don-mario",
"about": "Pizza al horno de barro · Delivery 30 min",
"vertical": "RESTAURANT",
"websites": ["https://donmario.com.ar"]
}
Reglas de Meta:
about: máximo 139 caracteres.description: máximo 512 caracteres.websites: máximo 2 URLs.vertical: uno deAUTO, BEAUTY, APPAREL, EDU, ENTERTAIN, EVENT_PLAN, FINANCE, GROCERY, GOVT, HOTEL, HEALTH, NONPROFIT, PROF_SERVICES, RETAIL, TRAVEL, RESTAURANT, OTHER, NOT_A_BIZ, UNDEFINED.
Para cambiar la foto de perfil hay que subir el binario al endpoint de Meta y mandar el handle resultante. Por ahora no lo exponemos — si lo necesitás, escribinos.
Estado del número
Diagnostics en vivo del número conectado del negocio: calidad, capacidad, throughput y salud. El cliente final ve lo mismo en su panel.
GET /v1/phone-number
GET /v1/phone-number?tenant_ref=pizzeria-don-mario
Authorization: Bearer TU_API_KEY
Respuesta:
{
"tenant_ref": "pizzeria-don-mario",
"phone_number": {
"display_phone_number": "+54 9 11 5555-1234",
"verified_name": "Pizzería Don Mario",
"quality_rating": "GREEN",
"throughput": { "level": "STANDARD" },
"health_status": {
"entities": [
{ "entity_type": "PHONE_NUMBER", "can_send_message": "AVAILABLE" }
]
},
"messaging_limit_tier": "TIER_1K",
"name_status": "APPROVED",
"code_verification_status": "VERIFIED",
"platform_type": "CLOUD_API",
"last_onboarded_time": "2026-04-23T18:22:00+0000"
}
}
| Campo | Valores | Qué significa |
|---|---|---|
quality_rating | GREEN / YELLOW / RED | Calidad percibida por los contactos. RED limita envíos. |
messaging_limit_tier | TIER_50 / 250 / 1K / 10K / 100K / UNLIMITED | Cantidad de conversaciones únicas por día. |
throughput.level | STANDARD / HIGH / NOT_APPLICABLE | Velocidad máxima de envío. |
health_status.entities[].can_send_message | AVAILABLE / LIMITED / BLOCKED | Estado operativo de cada entidad (PHONE_NUMBER, WABA, BUSINESS). |
POST /v1/phone-number/register
Re-registra el número contra Meta. Útil para destrabar errores
131031 Business Account locked en Test Numbers, o para
reanudar uso de un número que fue desactivado.
POST /v1/phone-number/register
{
"tenant_ref": "pizzeria-don-mario",
"pin": "000000"
}
Si el número tiene 2FA activado, el pin es obligatorio
y son los 6 dígitos del 2FA. Para Test Numbers de Meta el default
es 000000.
Respuesta:
{ "ok": true }
Errores específicos:
422 meta_pin_mismatch(Meta 133005) — el PIN no es el correcto.423 meta_account_locked(Meta 131031) — la cuenta sigue lockeada después del intento. Contactanos.
Estadísticas
Métricas históricas de conversaciones y plantillas, directo del WABA del negocio. Las fechas se mandan como Unix timestamp en segundos.
GET /v1/analytics/conversations
Volumen y costo de conversaciones en un rango temporal.
GET /v1/analytics/conversations
?tenant_ref=pizzeria-don-mario
&start=1714521600
&end=1717113600
&granularity=DAILY
Respuesta:
{
"tenant_ref": "pizzeria-don-mario",
"analytics": {
"data_points": [
{ "start": 1714521600, "end": 1714608000, "conversation": 47, "cost": 0.94 },
{ "start": 1714608000, "end": 1714694400, "conversation": 63, "cost": 1.26 }
]
}
}
Parámetros opcionales para filtrar:
| Parámetro | Valores |
|---|---|
granularity | HALF_HOUR, DAILY, MONTHLY (default DAILY) |
phone_numbers[] | Filtrar por display phone numbers específicos |
country_codes[] | ISO 3166-1 alpha-2 (ej AR, BR) |
metric_types[] | COST, CONVERSATION |
conversation_categories[] | AUTHENTICATION, MARKETING, SERVICE, UTILITY |
conversation_types[] | FREE_ENTRY_POINT, FREE_TIER, REGULAR |
conversation_directions[] | BUSINESS_INITIATED, USER_INITIATED |
GET /v1/analytics/templates
Métricas por plantilla: enviados, entregados, leídos, clicks. Hasta 10 templates por request.
GET /v1/analytics/templates
?tenant_ref=pizzeria-don-mario
&start=1714521600
&end=1717113600
&template_ids[]=TPL1
&template_ids[]=TPL2
&metric_types[]=SENT
&metric_types[]=DELIVERED
&metric_types[]=READ
&metric_types[]=CLICKED
Respuesta:
{
"tenant_ref": "pizzeria-don-mario",
"analytics": {
"data": [
{
"template_id": "TPL1",
"data_points": [
{ "start": 1714521600, "sent": 120, "delivered": 118, "read": 95 }
]
}
]
}
}
Los template_ids los obtenés con
GET /v1/templates — cada item tiene un id
de Meta.