API de WhatsApp para desarrolladores: autenticación, plantillas y errores
La API pública de Waspy está en api.waspytech.com/api/v2. Cómo autenticarte, qué error devuelve una key inválida, cómo listar y enviar plantillas, y cómo evitar duplicados con idempotencia.
Respuesta corta: la API pública de Waspy está en https://api.waspytech.com/api/v2. Te autenticás con Authorization: Bearer wspy_.... Un texto se manda como { "text": { "body": "..." } }, no como un string suelto. Si no hay clave, o la clave no existe, la API responde 401 con código UNAUTHORIZED. Si la clave no tiene el permiso, responde 403 con código FORBIDDEN. Las plantillas se listan por REST (el filtro status va en minúscula) y se envían con POST /messages; crearlas por REST no está expuesto: se crean en el panel o con la herramienta create_template del MCP.
El 16 de octubre de 2026 pegamos dos pedidos de solo lectura a producción, sin enviar ningún mensaje. Sin header, la API respondió:
{"error":{"code":"UNAUTHORIZED","message":"Missing authorization header","status":401},"meta":{"requestId":"..."}}
Con una clave inventada que empieza con wspy_, respondió UNAUTHORIZED y "Invalid API key". Si un ejemplo viejo de documentación habla de INVALID_API_KEY o INSUFFICIENT_SCOPE, no es lo que acepta ni lo que devuelve la API hoy.
Para quién es y en qué plan está
Esta API es para sistemas: un ERP, una tienda, un script, n8n. No es para que una persona entre al panel. Está en los planes Scale y Enterprise. Starter y Growth no crean claves. Los precios de la plataforma están en Planes; lo que cobra Meta por cada mensaje, en cuánto cuesta la API en 2026.
La referencia navegable está en docs.waspytech.com. Esta guía sigue el código de la API y las respuestas de producción, que son las que tu cliente va a ver.
Scopes
Al crear la clave en Configuración → Claves de API podés marcar permisos. Si no marcás ninguno, la clave puede hacer todo lo que la API permite. Si marcás alguno, cada ruta exige el suyo:
| Scope | Permite |
|---|---|
messages:read / messages:write | Leer o enviar mensajes |
contacts:read / contacts:write | Leer o editar contactos |
conversations:read / conversations:write | Leer chats o escribir en ellos (notas, estado) |
templates:read | Listar plantillas. No existe templates:write en la API REST |
channels:read | Ver los números conectados |
account:read | Ver la cuenta |
media:read / media:write | Bajar o subir archivos |
webhooks:read / webhooks:write | Administrar webhooks |
social:read | Métricas de Instagram y Facebook |
API key missing required scope: , con código FORBIDDEN.
Mandar un texto
Primero necesitás el UUID del número, que sale de GET /api/v2/channels (scope channels:read). No es el id de teléfono de Meta: es el id interno de Waspy.
Dentro de la ventana de 24 horas (el cliente te escribió hace menos de un día) podés mandar texto libre. Fuera de esa ventana, la API responde 403 SERVICE_WINDOW_EXPIRED y solo acepta una plantilla. La regla está explicada en la ventana de 24 horas.
curl -sS https://api.waspytech.com/api/v2/messages \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1847-texto" \
-d '{
"phoneNumberId": "UUID-DEL-CANAL",
"to": "+5491155554444",
"type": "text",
"text": { "body": "Hola Laura, ya registramos el pago del pedido 1847." },
"clientRef": "pedido-1847"
}'
El teléfono va en E.164. Para un celular argentino es +549 + código de área sin 0 + número sin 15. La API también acepta el número sin el + y lo normaliza; si después de eso sigue siendo inválido, responde 400 INVALID_PHONE.
Un 201 devuelve el mensaje en data (con id, status, conversationId) y meta.requestId. El clientRef (hasta 256 caracteres) vuelve en cada webhook de estado.
Idempotencia
Si el script se corta después de enviar y no llega a guardar el id, un reintento crea un segundo WhatsApp. El header Idempotency-Key (hasta 256 caracteres) lo evita:
- La primera vez, Waspy manda el mensaje y guarda la respuesta 24 horas.
- Si repetís la misma clave, devuelve esa misma respuesta y agrega el header
Idempotency-Replayed: true. No manda otro mensaje. - Si dos pedidos con la misma clave llegan juntos, el segundo recibe 409
IDEMPOTENCY_CONFLICT("already being processed"). Esperá y reintentá: cuando el primero termine, el replay te da el resultado.
La clave es por cuenta. Usá algo estable del negocio (pedido-1847), no un Date.now().
Listar plantillas y mandar una
GET /api/v2/templates (scope templates:read) lista las plantillas. El query status solo acepta minúsculas: pending, approved, rejected, disabled, paused. APPROVED no es un valor válido.
curl -sS "https://api.waspytech.com/api/v2/templates?status=approved&page=1&pageSize=20" \
-H "Authorization: Bearer $WASPY_API_KEY"
También están GET /api/v2/templates/:id y GET /api/v2/templates/by-name/:name?language=es_AR&phoneNumberId=UUID, que resuelve el nombre aprobado (el mismo string que usás al enviar) sin que tengas que guardar el UUID.
No hay POST /api/v2/templates. Crear una plantilla y mandarla a revisión de Meta se hace desde el panel, o con la herramienta create_template del conector MCP (scope templates:write, solo en el MCP). Cómo redactarlas para que Meta las apruebe está en la guía de plantillas.
Para enviarla, el cuerpo usa type: "template" y los parámetros del cuerpo en orden {{1}}, {{2}}, …:
curl -sS https://api.waspytech.com/api/v2/messages \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1847-aviso" \
-d '{
"phoneNumberId": "UUID-DEL-CANAL",
"to": "+5491155554444",
"type": "template",
"template": {
"name": "aviso_despacho",
"language": { "code": "es_AR" },
"components": [{
"type": "body",
"parameters": [
{ "type": "text", "text": "Laura" },
{ "type": "text", "text": "1847" },
{ "type": "text", "text": "360000012345678" }
]
}]
},
"clientRef": "pedido-1847"
}'
Esa plantilla tiene que estar aprobada en ese número. Un aviso de despacho es utilidad: en Argentina, desde el 1 de octubre de 2026, Meta cobra alrededor de ARS 37,68 por mensaje entregado. El ejemplo completo con Google Sheets está en enviar WhatsApp desde una planilla.
Hasta 100 mensajes por pedido entran por POST /api/v2/messages/batch. Cada ítem se acepta, se saltea o falla por separado. Con respectOptOut en true (el default), quien se dio de baja se saltea con reason: "opted_out".
Errores que vas a ver
Todos vienen como { "error": { "code", "message", "status" }, "meta": { "requestId" } }.
| HTTP | Código | Cuándo |
|---|---|---|
| 400 | VALIDATION_ERROR, MISSING_REQUIRED_FIELD, INVALID_PHONE, INVALID_MESSAGE_TYPE, INVALID_CHANNEL | El cuerpo o el número no cierran |
| 401 | UNAUTHORIZED | Falta el header, la clave no existe, está revocada o venció |
| 403 | FORBIDDEN | A la clave le falta el scope |
| 403 | SERVICE_WINDOW_EXPIRED | Texto libre fuera de las 24 horas |
| 403 | CHANNEL_NOT_CONNECTED | El número no está activo |
| 404 | TEMPLATE_NOT_FOUND y similares | El id no es de tu cuenta |
| 409 | IDEMPOTENCY_CONFLICT | Esa clave ya se está procesando |
| 429 | RATE_LIMITED | Más de 120 pedidos por minuto en la cuenta |
Retry-After.
Waspy, Cloud API directa, Twilio, 360dialog y Z-API
| Camino | Qué programás vos | Cuándo tiene sentido |
|---|---|---|
| Cloud API de Meta, directa | Todo: token de sistema, webhooks de Meta, plantillas, ventana, reintentos | Tenés un equipo que quiere ser el proveedor |
| Twilio | La integración contra la API de Twilio, que a su vez habla con Meta | Ya estás metido en el ecosistema de Twilio |
| 360dialog | La integración contra su API de acceso a WhatsApp | Querés la Cloud API cruda, sin inbox |
| Z-API | Una API que se conecta leyendo un QR, como WhatsApp Web | No es la API oficial: el número puede bloquearse |
| API de Waspy | Pedidos a /api/v2 y, si hace falta, webhooks firmados | Querés el inbox del equipo y la integración en el mismo número |
Para recibir eventos en vez de consultar, la guía siguiente es webhooks de Waspy en Node y Python. Para que Claude o Cursor hablen con la bandeja sin que escribas un cliente, el camino es el MCP.
Si el número de la tienda todavía no está en la API, abrí la cuenta y conectalo desde el panel. La clave wspy_ se crea después, en Scale.
