Guías
Compatible con la Cloud API de Meta
Si ya tienes código hecho para la Cloud API de WhatsApp (graph.facebook.com/v21.0/{phone_number_id}/messages), no tienes que reescribirlo: cambia la URL base por la de heysteff y el token por tu API key, y sigue funcionando. Cada mensaje pasa por heysteff igual que uno de POST /v1/messages: queda en la bandeja de la web y de la app, cuenta para tu plan, respeta la ventana de 24 horas y aparece en los Logs de la API.
Lo único que cambia#
| Meta directo | heysteff | |
|---|---|---|
| URL base | https://graph.facebook.com/v21.0 | https://app.heysteff.com/api/meta/whatsapp/v21.0 |
| Token | Authorization: Bearer EAAG… (token de sistema de Meta) | Authorization: Bearer sk_live_… (tu API key de heysteff) |
| Versión | La que uses | Cualquier v<n>.<n>: v21.0, v22.0, v24.0… heysteff responde igual con todas. |
curl https://graph.facebook.com/v21.0/106540352242922/messages \
-H "Authorization: Bearer $META_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messaging_product": "whatsapp", "to": "56912345678", "type": "text", "text": { "body": "Hola Ana" } }'curl https://app.heysteff.com/api/meta/whatsapp/v21.0/106540352242922/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "messaging_product": "whatsapp", "to": "56912345678", "type": "text", "text": { "body": "Hola Ana" } }'La respuesta tiene la forma de Meta:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "56912345678", "wa_id": "56912345678" }],
"messages": [{ "id": "6f1d2c1e-6a1b-4c43-9a51-2f9f6a0e8b7d" }]
}Con un cliente de la Cloud API que deje cambiar la URL base (el SDK de Node de Meta, whatsapp-api-js o tu propio wrapper de fetch) solo configuras esa URL y el token.
El phone_number_id#
Es el mismo id de Meta de tu número de WhatsApp Cloud, ahora conectado a heysteff: lo ves en la consola, sección Números, y en GET /{phone_number_id}. Sirve cualquier número de WhatsApp Cloud de tu espacio: uno que conectaste tú, un número de heysteff o el que conectó uno de tus clientes con su link de conexión. Un id que no es de tu espacio responde el mismo error que Graph para un objeto ajeno (código 100, subcódigo 33).
Sandbox. Con una key sk_test_, el phone_number_id es el del número compartido de heysteff (lo muestra la consola, sección Sandbox) y solo puedes escribir a los teléfonos que se unieron. También solo ves la media del sandbox: lo que subiste con una key sk_test_ y lo que te mandaron al número compartido; cualquier otro archivo responde 403 test_key_not_allowed. Ver Sandbox.
Qué cubre#
| Ruta | Para qué | Scope |
|---|---|---|
POST /{phone_number_id}/messages | Mandar texto, imagen, audio, video, documento, sticker, ubicación, contactos, reacción, interactivos (botones, lista, cta_url, location_request_message) y plantillas. También marcar leído con status: "read" y typing_indicator. | messages:write |
POST /{phone_number_id}/media | Subir un archivo (multipart, campo file). Devuelve { "id": "…" }. | messages:write |
GET /{media_id} | url, mime_type, sha256 y file_size. La url se descarga con el mismo Bearer. | messages:write |
DELETE /{media_id} | Borrar un archivo que subiste con POST /{phone_number_id}/media. El resto de la media de tu espacio no se borra por aquí. | messages:write |
GET /{phone_number_id} | Nombre verificado, número visible, calidad y plataforma. | messages:write |
GET y POST /{phone_number_id}/whatsapp_business_profile | Leer y cambiar el perfil de empresa (about, dirección, descripción, correo, sitios, rubro). | messages:write para leer, numbers:write para cambiar |
GET /{waba_id}/phone_numbers | Los números de esa WABA conectados a tu espacio. | messages:write |
GET, POST y DELETE /{waba_id}/message_templates | Listar, crear (van a revisión de Meta) y borrar plantillas (?name=). | messages:write para listar, templates:write para crear y borrar |
No están las rutas de administración de la cuenta de Meta (registrar un número, verificación en dos pasos, suscripciones de apps, analítica de la WABA): eso lo hace heysteff por ti al conectar el número. Para todo lo que no es de WhatsApp (Instagram, Messenger, SMS, correo) usa la API de heysteff.
Ids de mensaje#
- Lo que mandas.
messages[0].ides el id del mensaje en heysteff (un UUID), no el wamid: el envío a WhatsApp sale por una cola, así que el wamid todavía no existe cuando respondemos. Los webhooks de estado traen ese mismo id, así cruzas envío y estados como hacías con el wamid. - Lo que te mandan. Un mensaje entrante trae su wamid real, igual que en Meta.
- Donde la API pide un id (
context.message_idpara citar,reaction.message_id,message_idal marcar leído) sirve cualquiera de los dos.
Cómo se comportan los envíos#
- Ventana de 24 horas. Fuera de la ventana solo sale una plantilla, como en Meta. Un mensaje libre fuera de la ventana se acepta y después llega el estado
failedcon el código 131047, igual que en Meta. - Idempotencia. Además de lo de Meta, puedes mandar
Idempotency-Key: un reintento con la misma clave no manda el mensaje dos veces. - Archivos. En
image,audio,video,documentystickersirven elidque devolvióPOST /{phone_number_id}/media, elidde un archivo que te mandó un cliente (para reenviarlo) o unlinkhttps público. heysteff guarda una copia para la bandeja. - Plantillas.
template.componentsva tal cual a Meta. Un encabezado de imagen, video o documento aceptalinko elidde un archivo subido. Antes de aceptar el envío, heysteff revisa que la plantilla exista, esté aprobada y lleve las variables que corresponden, y si no responde con el código de Meta (132001 o 132000). - Interactivos. Botones y listas sin encabezado ni pie salen por el mismo camino que
/v1. El resto (encabezado, pie,cta_url, pedido de ubicación, productos) se manda a Meta tal como lo escribiste. - Marcar leído.
{ "messaging_product": "whatsapp", "status": "read", "message_id": "wamid…" }marca la conversación como leída en heysteff y manda el recibo; con"typing_indicator": { "type": "text" }además muestra "escribiendo…". Responde{ "success": true }.
Webhooks en formato Meta#
Para que tu receptor de webhooks de Meta funcione sin cambios, crea el webhook con format: "meta" (en la consola, sección Webhooks, elige "Cloud API de Meta", o por API):
curl https://app.heysteff.com/api/v1/webhooks \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://api.tuapp.com/whatsapp/webhook", "format": "meta" }'Ese webhook recibe el payload de la Cloud API: object: "whatsapp_business_account" y entry[].changes[].value con metadata.phone_number_id, contacts y messages (entrantes) o statuses (sent, delivered, read, failed). Solo llegan los eventos de mensajes de WhatsApp Cloud (message:received, message:sent, message.delivered, message.read, message.failed); los demás canales y eventos siguen en el formato de heysteff.
Firma. Va en X-Hub-Signature-256: sha256=<hex>, el HMAC-SHA256 del cuerpo crudo, igual que la de Meta. La diferencia es la clave: en vez del app secret de tu app de Meta, usa el secreto que te dio heysteff al crear el webhook.
import crypto from 'node:crypto';
// rawBody: el cuerpo tal como llegó. secret: el del webhook de heysteff.
function isFromHeysteff(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}heysteff no hace la verificación inicial de Meta (el GET con hub.challenge): la URL queda activa al crearla. El botón "Enviar evento de prueba" de la consola manda un mensaje de texto de ejemplo con la forma de Meta y la misma firma.
Errores#
Los errores tienen la forma de la Graph API, con los códigos de la Cloud API donde existe uno equivalente:
{
"error": {
"message": "(#100) Invalid parameter",
"type": "OAuthException",
"code": 100,
"error_data": { "messaging_product": "whatsapp", "details": "text.body: Required" },
"fbtrace_id": "Az8or2yhqkZfEZ-_4Qn_Bam",
"heysteff_code": "validation_error"
}
}| Código | Cuándo |
|---|---|
190 (401) | La API key falta, no existe, se revocó o venció. |
200 (403) | A la key le falta el scope, o es una key de prueba en algo que no es del sandbox. |
100 | Parámetro inválido; con subcódigo 33, un id que no existe o no es de tu espacio. |
130429 (429) | Límite de llamadas por minuto de la key, o el tope diario del sandbox. |
131030 | Sandbox: el teléfono no se unió. |
131042 | Tu plan llegó al tope de mensajes del mes. |
131047 | La ventana de 24 horas está cerrada. |
131053 | El archivo no existe o no se pudo bajar. |
132000, 132001 | La plantilla no existe, no está aprobada o le faltan variables. |
2 (424) | Meta no respondió al leer o cambiar el perfil o una plantilla. Reintenta. |
heysteff_code es el código de heysteff del mismo error (ver Errores y límites); los clientes hechos para Meta lo ignoran.
Cuándo usar cada API#
Esta API sirve para mover lo que ya tienes sin tocarlo. Para código nuevo conviene POST /v1/messages: con el mismo cuerpo mandas por WhatsApp, Instagram, Messenger, SMS o correo cambiando solo channel. Las dos conviven en el mismo espacio y con la misma key.