Conexión con la API de Meta Graph
Hay dos formas de enviar mensajes de WhatsApp a través de WizMessage. La API de WizMessage es la que conviene si empieza desde cero: le da variables de plantilla con nombre, una interfaz de asignación y un campo reference con el que puede identificar sus propios registros.
Esta página documenta la otra. Reproduce la Cloud API de WhatsApp de Meta con tanta fidelidad que el código ya escrito para Meta funciona aquí después de cambiar dos cosas: la URL base y el token. Las mismas rutas, los mismos cuerpos de solicitud, los mismos cuerpos de respuesta, el mismo formato de error. Si ya tiene una integración, una biblioteca o un desarrollador que conoce la Cloud API, este es el camino más corto.
No es solo una superficie de envío. Junto con la mensajería incluye los endpoints de administración de la cuenta: crear y eliminar plantillas, leer y reescribir el perfil de empresa, códigos QR, ajustes de comercio, la lista de bloqueados, los ajustes del teléfono y flows, todo en las rutas propias de Meta. Administración de la cuenta es el catálogo, y Lo que no está disponible aquí es la otra mitad, igual de importante.
Ambas superficies usan las mismas claves de API, los mismos permisos y los mismos límites de frecuencia. Nada está obsoleto: elija según cada integración, y puede usar las dos a la vez.
Cuál elegir
Lo básico
URL base y la versión en la ruta
https://wizmessage.com/v26.0/{phone-number-id}/messages
El segmento de versión es real y obligatorio, pero su valor no cambia nada. Lo reconocemos como v{major}.{minor} y luego lo ignoramos: la plataforma decide con qué versión de Graph habla por detrás.
Dos consecuencias que conviene conocer:
- Fijar una versión antigua sigue funcionando. Cuando Meta retire la versión que su código tiene fija, las llamadas aquí continúan. No hay nada que programar.
v26a secas se rechaza. El número menor no es opcional.v26.0está bien,v26da un 404.
El {phone-number-id} debe ser el número de WhatsApp que está detrás de la clave de API que usa. Cualquier otro se rechaza; consulte Cuando un id no es suyo.
Autenticación
Envíe su clave de API como token de portador, exactamente como enviaría a Meta un token de acceso.
| Encabezado | Valor |
|---|---|
Authorization | Bearer pk_live_…: su clave de API completa, no el prefijo que se muestra en el panel. |
Aquí también se acepta X-API-Key, así que una clave que ya funciona con la API de WizMessage no necesita ningún cambio de encabezado. Bearer es la forma que conviene preferir: es lo que envía un SDK de Meta sin modificar, que es el objetivo de esta superficie.
Nota: “Require signature” debe estar desactivado en la clave. Aquí no es una preferencia. Una clave que lo tenga activado no puede guardar un webhook de Meta Graph (el panel lo rechaza con "Signed-request enforcement is not compatible with the Graph format — disable it on this key first") y toda llamada sin firma que haga en esta superficie se rechaza como
code 190con un401. Un cliente estándar de la Cloud API no puede generar nuestra firma de solicitud, así que ambas cosas son mutuamente excluyentes por diseño.
Su suscripción
El módulo de API requiere una suscripción activa a Wiz Bot o Wiz Pro. Esto se aplica a las dos superficies, la API de WizMessage y esta, y se verifica en cada llamada, no solo cuando se crea la clave.
Si la suscripción vence, la API no se detiene en ese instante. La primera llamada posterior al vencimiento inicia una ventana de siete días y se deja pasar, y cada respuesta durante esa ventana lleva un encabezado:
| Encabezado | Valor |
|---|---|
X-Subscription-Warning | Subscription inactive. API access ends in 5 day(s). |
Después de la ventana, las llamadas se rechazan con el code 10 de Graph en un 403:
{
"error": {
"message": "(#10) Access denied. The API module requires an active Wiz Bot or Wiz Pro subscription. Renew at /subscriptions to restore access.",
"type": "OAuthException",
"code": 10,
"fbtrace_id": "AKnBaXsYl7FlmMVw3ql5Ywo"
}
}
Tres detalles que deciden si se da cuenta a tiempo:
- El reloj empieza con su primera llamada, no en la fecha de facturación. Una clave que queda inactiva dos semanas después del vencimiento sigue teniendo sus siete días completos, a partir de cuando vuelva a llamar.
- La ventana es por clave de API, no por cuenta. Cada clave corre su propio reloj desde su propia primera llamada, así que una clave con la que prueba y una clave que corre en producción vencerán en días distintos. Revise el encabezado en la clave que importa.
- El rechazo llega sin aviso en la solicitud que lo provoca.
X-Subscription-Warninges el único aviso previo que existe: regístrelo, o lo primero que sabrá es un403.
Nota: Renovar tarda unos minutos en surtir efecto, no segundos. La respuesta de derechos de uso se guarda en caché cinco minutos y el estado de la suscripción que hay detrás otros cinco, así que una renovación hecha mientras una integración está fallando no se reflejará en el siguiente reintento. Espere unos minutos antes de concluir que no funcionó.
Cómo enviar un mensaje
POST /{version}/{phone-number-id}/messages, con el cuerpo de Meta. No lo validamos más allá de comprobar que es un mensaje de WhatsApp: Meta es la autoridad sobre cómo es un mensaje válido, y sus errores son mejores que cualquiera que pudiéramos inventar.
POST https://wizmessage.com/v26.0/106540352242922/messages
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "919876543210",
"type": "text",
"text": { "preview_url": false, "body": "Your order has shipped." }
}
Nota:
torecibe solo dígitos aquí:919876543210, sin+. Es el formato de Meta, y es lo que más se confunde al pasar de una superficie a otra: la API de WizMessage exige+E.164y rechaza cualquier otra cosa, mientras que esta pasa su valor tal cual a Meta. La única excepción esPOST /messages/contacts, que acepta ambas formas precisamente para que un cuerpo de Graph se traslade sin cambios.
Un envío de plantilla también tiene la forma de Meta: componentes posicionales, no variables con nombre. En esta superficie no hay capa de asignación, que es lo que se cede a cambio de la compatibilidad.
{
"messaging_product": "whatsapp",
"to": "919876543210",
"type": "template",
"template": {
"name": "payment_failed_alert",
"language": { "code": "en" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Priya Sharma" },
{ "type": "text", "text": "12 Aug 2026" },
{ "type": "text", "text": "Wiz Pro" }
]
}
]
}
}
Cómo enviar una tarjeta de contacto
type: "contacts" es una vCard: un nombre y lo que más adjunte. Siempre ha funcionado en esta superficie y lo sigue haciendo, con el cuerpo de Meta sin cambios.
POST https://wizmessage.com/v26.0/106540352242922/messages
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "919876543210",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "Priya Sharma",
"first_name": "Priya",
"last_name": "Sharma"
},
"phones": [
{ "phone": "+911122334455", "type": "WORK", "wa_id": "911122334455" }
],
"emails": [
{ "email": "[email protected]", "type": "WORK" }
],
"org": { "company": "Acme Retail", "department": "Support", "title": "Account Manager" }
}
]
}
name.formatted_name es el único campo que exige WhatsApp. phones[].wa_id es el que conviene agregar de todos modos: con él la tarjeta lleva un botón Message que abre un chat con esa persona; sin él, la tarjeta solo se puede guardar.
Ese mismo cuerpo también se envía por POST /messages/contacts en la API de WizMessage, con las claves del sobre, el to de solo dígitos y todo. Es el único endpoint de esa superficie que acepta esta forma, y existe porque las dos cosas que esta ruta no puede darle valen la pena: un envío aquí no se archiva bajo un message_id, así que no hay estado que consultar ni un webhook message.sent con el cual compararlo, y el cuerpo apenas se revisa antes de llegar a Meta. Cambiar la URL es toda la migración.
Nota: Enviar varias tarjetas en un mensaje es el arreglo
contactsde Meta, y Meta acepta hasta 257. El endpoint de WizMessage lo limita a diez, así que un envío masivo de tarjetas que funciona aquí es uno de los pocos cuerpos que no se trasladará sin cambios.
Cómo marcar un mensaje como leído
Las confirmaciones de lectura y los indicadores de escritura se envían al mismo endpoint, con un cuerpo que no lleva ni to ni type. El markAsRead() de todos los SDK de la Cloud API depende de que esto funcione, así que se admite exactamente como lo especifica Meta.
POST https://wizmessage.com/v26.0/106540352242922/messages
{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgUQ0Uz…"
}
La respuesta es {"success": true}, no el sobre de envío que aparece abajo. Ramifique según la llamada que hizo en lugar de intentar leer messages[0].id en todas las respuestas.
Qué devuelve
La respuesta de Meta, intacta:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "919876543210", "wa_id": "919876543210" }],
"messages": [{ "id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgUQ0Uz…" }]
}
Ese wamid es su único asidero sobre el mensaje. Esta superficie deliberadamente no tiene campo reference ni endpoint de consulta de estado: ambos son adiciones de WizMessage que la API de Meta no tiene, así que incluirlos rompería la compatibilidad para la que existe esta superficie.
Para seguir el destino de un mensaje, compare el wamid con los estados de entrega que llegan a su webhook. Si prefiere tener un campo propio para identificarlo, para eso sirve el reference de la API de WizMessage.
Errores
Los errores llegan en el formato de Meta, nunca en el nuestro. Dónde se originó el error decide cuánto de él es nuestro.
Errores que generamos nosotros
code | HTTP | Cuándo |
|---|---|---|
190 | 401 | Clave ausente, desconocida, revocada, suspendida o vencida, o una llamada sin firma a una clave que exige firmas. También se genera cuando la propia cuenta de WhatsApp perdió su conexión con Meta; vea la nota de abajo. |
10 | 403 | La clave no tiene el permiso que necesita esta llamada (vea Permisos), o la suscripción de la cuenta venció más allá de su ventana de gracia. |
4 | 429 | Se agotó el límite por minuto o diario de la clave. |
100 | 400 | Solicitud mal formada, un id en la ruta que no es suyo o un endpoint que esta superficie no atiende. |
2 | 500 | Algo falló de nuestro lado. Reintente. |
Nota: Dos cosas distintas devuelven
190, y necesitan arreglos opuestos. Si el mensaje menciona la clave, rote o vuelva a introducir la clave. Si dice "the connection to this WhatsApp Business Account needs to be renewed", la clave está bien y es la cuenta la que perdió su vínculo con Meta: vuelva a conectarla desde el panel. Rotar una clave que funciona como respuesta a la segunda situación desperdicia una tarde y no cambia nada.
Errores que genera Meta
Se reenvían exactamente como se reciben, con el estado HTTP de Meta. Todo lo que envió Meta se conserva: error_subcode, error_data.details y el fbtrace_id propio de Meta:
{
"error": {
"message": "Unsupported post request. Object with ID '106540352242922' does not exist, cannot be loaded due to missing permissions, or does not support this operation.",
"code": 100,
"type": "GraphMethodException",
"error_subcode": 33,
"fbtrace_id": "AKnBaXsYl7FlmMVw3ql5Ywo"
}
}
Nota: Observe el
typede ese ejemplo:GraphMethodException, noOAuthException. Meta usa varios tipos de excepción y no los normalizamos. Decida segúnerror.code, que es un número y es estable; tratetypecomo una etiqueta para personas.
Cuando un id no es suyo
Cada id de la ruta (el id del número de teléfono, el id de la WABA, un id de multimedia) se verifica contra la cuenta que está detrás de su clave antes de que ocurra cualquier otra cosa. Una discrepancia devuelve el código 100 redactado como Meta redacta un objeto que usted no puede ver:
Unsupported post request. Object with ID '…' does not exist, cannot be loaded due to missing permissions, or does not support this operation.
Esa redacción es deliberada de parte de Meta y se copió deliberadamente aquí: un id que existe pero pertenece a otra persona y un id que no existe en absoluto deben ser indistinguibles, o el error se convierte en una forma de descubrir los números de teléfono de otras personas.
Multimedia
Cuatro endpoints, equivalentes a los de Meta.
| Método | Ruta | Qué hace |
|---|---|---|
POST | /{version}/{phone-number-id}/media | Sube un archivo. Multipart, devuelve {"id": "…"}. |
GET | /{version}/{media-id} | El objeto de metadatos de Meta para el archivo. |
GET | /{version}/{media-id}/download | Los bytes. |
DELETE | /{version}/{media-id} | Lo elimina, devuelve {"success": true}. |
La única diferencia deliberada
GET /{version}/{media-id} devuelve el objeto de metadatos de Meta (messaging_product, mime_type, sha256, file_size, id), pero la url apunta a nosotros, no al host lookaside de Meta.
Tiene que ser así. La URL de multimedia de Meta solo se puede obtener con el token de empresa de la cuenta de WhatsApp, que vive de nuestro lado y nunca sale de él. Así que le entregamos una URL que sí puede obtener: el endpoint de descarga de arriba, en el mismo host y segmento de versión que usó para llamar, autenticado con el mismo token de portador que ya envió.
Los clientes tratan esa URL como opaca y simplemente le hacen GET con su token, por eso un SDK sin modificar no lo nota. Si su código trata el nombre de host como un caso especial, es el único lugar donde necesitará un cambio.
Permisos
Los mismos botones de permiso que la API de WizMessage, en las mismas claves. De dónde se lee el permiso depende de la llamada: el endpoint de envío lo deriva del cuerpo de la solicitud, porque Meta pone todos los tipos de mensaje en una sola ruta; en todo lo demás sale de la ruta y del método.
Envío y multimedia:
| Llamada | Permiso |
|---|---|
POST /{v}/{pnid}/messages con "type": "template" | send_template |
igual, con type de image, video, audio, document o sticker | send_media |
| igual, cualquier otro: text, interactive, reaction, location, contacts, y marcar como leído | send_text |
POST /{v}/{pnid}/media: subir | send_media |
DELETE /{v}/{media-id} | send_media |
GET /{v}/{media-id} y /download | view_messages |
Administración de la cuenta; vea Administración de la cuenta:
| Llamada | Permiso |
|---|---|
GET /{v}/{waba-id}/message_templates | view_templates |
POST y DELETE /{v}/{waba-id}/message_templates | manage_templates |
GET /{v}/{waba-id}/template_analytics | view_analytics |
GET en perfil de empresa, códigos QR, ajustes de comercio, lista de bloqueados, ajustes del teléfono y flows | view_templates |
POST en esos mismos seis | manage_profile |
DELETE /{v}/{pnid}/message_qrdls/{code} y DELETE /{v}/{pnid}/block_users | manage_profile |
Esos son los únicos dos DELETE del lado de administración. No hay DELETE sobre el perfil de empresa, los ajustes de comercio, los ajustes del teléfono ni los flows: un DELETE a cualquiera de ellos se rechaza como code 100, igual que un endpoint que no existe.
Nota: Subir multimedia requiere
send_mediaaquí. En la API de WizMessage no requiere ningún permiso, con el razonamiento de quesend_mediagobierna el envío de un archivo a un cliente y no ponerlo en los servidores de Meta. Esta superficie adopta la línea más estricta. Si está trasladando una clave y las subidas empiezan a devolver403, este es el motivo.
Nota:
view_templatescontrola mucho más que las plantillas. Leer el perfil de empresa, los códigos QR, los ajustes de comercio, la lista de bloqueados, los ajustes del teléfono y los flows queda detrás de él. El nombre es histórico: es anterior a todo lo de la segunda tabla. Si lo desmarca al restringir una clave, seis lecturas no relacionadas empiezan a devolver403con un mensaje sobre plantillas, que no es una pista útil.
Nota: Tres botones son solo de Meta Graph.
manage_templates,manage_profileyview_analyticsno controlan nada en la API de WizMessage: existen para los endpoints de esta página. A todas las claves que ya existían se les otorgaron los tres, así que nada de lo que funcionaba dejó de hacerlo; restringir es desmarcarlo a propósito en el formulario de edición de la clave.
Un rechazo es el code 10 de Graph en un 403, nunca nuestro PERMISSION_DENIED. Un solo controlador de errores cubre esta superficie por completo.
La propiedad se verifica antes que el permiso. Si el id de la ruta no es el que está detrás de su clave, la respuesta es code 100 y nunca revela qué permiso le faltaba; de lo contrario, el propio error se convertiría en una forma de sondear las cuentas de otras personas.
Administración de la cuenta
Además de enviar, esta superficie incluye los endpoints de administración de cuenta de Meta en las rutas propias de Meta. Ocho recursos, y el permiso que necesita depende de si está leyendo o escribiendo.
Tres cosas son ciertas de todos los endpoints de esta sección, y son la razón por la que puede existir:
- El cuerpo de la solicitud es el propio de Meta, reenviado sin cambios y sin validar. Aquí no volvemos a documentar los cuerpos de Meta ni los revisamos. La referencia de Meta es la especificación, y el error de Meta es lo que regresa si se equivoca en uno, que es un error mejor que cualquiera que pudiéramos inventar.
- Solo se envía un cuerpo en
POST. Un cuerpo adjunto a unDELETEse descarta antes de que la solicitud salga de nosotros. El borrado de plantillas de Meta recibe?name=o?hsm_id=como parámetros de consulta, no como cuerpo, así que funciona como está documentado; cualquier otra cosa que espere un cuerpo en un delete no funcionará. - Su cadena de consulta se reenvía, menos
access_tokenyappsecret_proof. Esos dos se eliminan porque Meta da prioridad a un token en la cadena de consulta sobre el encabezadoAuthorization, y dejarlos pasar haría opcional su bearer. Un cliente que se autentique por cadena de consulta verá que se ignora en silencio: use el encabezado.
Nota: El id de la WABA y el id del número de teléfono de estas rutas deben ser los que están detrás de su clave. Eso se verifica antes de que algo salga de nuestra red, y una discrepancia es el mismo
code 100descrito en Cuando un id no es suyo.
Nota:
paging.nextapunta a Meta, no a nosotros. Las respuestas se devuelven exactamente como las envió Meta, así que un enlacenexten una respuesta paginada es una URL degraph.facebook.com, y su clave de API no autentica allí. Pagine tomandopaging.cursors.aftery volviendo a emitir la solicitud contra este host con?after=.
Plantillas
GET https://wizmessage.com/v26.0/{waba-id}/message_templates?limit=25
POST https://wizmessage.com/v26.0/{waba-id}/message_templates
DELETE https://wizmessage.com/v26.0/{waba-id}/message_templates?name=order_shipped
| Método | Permiso | Devuelve |
|---|---|---|
GET | view_templates | El { data, paging } de Meta |
POST | manage_templates | La respuesta de creación de Meta |
DELETE | manage_templates | La respuesta de eliminación de Meta |
GET acepta limit (1–100, 25 por defecto) y after para la paginación por cursor.
Nota:
GETacepta sololimityafter. Los demás filtros de Meta en este borde (name,status,fields,language,category,name_or_content) se descartan en silencio, no se rechazan. Una solicitud con?status=APPROVEDdevuelve un200con todas las plantillas, aprobadas o no. Filtre de su lado, o useafterpara paginar la lista completa. Es el único lugar de esta página donde un cliente trasladado obtiene una respuesta equivocada en lugar de un error, así que vale la pena revisar su código.
Analítica de plantillas
GET https://wizmessage.com/v26.0/{waba-id}/template_analytics
Requiere view_analytics. Meta exige start, y responde code 100 ("(#100) The parameter start is required") si falta. granularity y los demás parámetros son de Meta; páselos y llegan sin cambios.
Perfil de empresa
GET https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical
POST https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_business_profile
GET requiere view_templates y devuelve el { data } de Meta. POST requiere manage_profile.
Códigos QR
GET https://wizmessage.com/v26.0/{phone-number-id}/message_qrdls
POST https://wizmessage.com/v26.0/{phone-number-id}/message_qrdls
DELETE https://wizmessage.com/v26.0/{phone-number-id}/message_qrdls/{code}
GET requiere view_templates y devuelve el { data } de Meta. POST y DELETE requieren manage_profile.
La ruta /{code} es solo para DELETE. No hay GET ni POST sobre un solo código: lea la colección y elija de ella.
Ajustes de comercio
GET https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_commerce_settings
POST https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_commerce_settings
GET requiere view_templates y devuelve el { data } de Meta. POST requiere manage_profile.
Clientes bloqueados
GET https://wizmessage.com/v26.0/{phone-number-id}/block_users
POST https://wizmessage.com/v26.0/{phone-number-id}/block_users
DELETE https://wizmessage.com/v26.0/{phone-number-id}/block_users
GET requiere view_templates y devuelve el { data } de Meta. POST y DELETE requieren manage_profile. Observe que desbloquear es un DELETE a la colección misma, no a una ruta por cliente.
Ajustes del número de teléfono
GET https://wizmessage.com/v26.0/{phone-number-id}/settings
POST https://wizmessage.com/v26.0/{phone-number-id}/settings
GET requiere view_templates y devuelve { calling, storage_configuration }. POST requiere manage_profile.
Nota: La ruta es
/settings. Algunas referencias de API de terceros la llamanwhatsapp_calling_settings; esa ruta no existe en Meta y devuelvecode 2500aquí, porque la reenviamos y Meta la rechaza. Vea Lo que no está disponible aquí.
Flows
GET https://wizmessage.com/v26.0/{waba-id}/flows
POST https://wizmessage.com/v26.0/{waba-id}/flows
GET requiere view_templates y devuelve el { data, paging } de Meta. POST requiere manage_profile.
Lo que no está disponible aquí
Todo lo anterior se confirmó contra una cuenta de WhatsApp real. Lo de abajo también, por eso esta sección puede decirle por qué falta un endpoint, y eso suele ser lo que decide qué hacer a continuación.
Qué devuelve un endpoint no disponible. Todo lo que no esté en la lista de arriba responde con la redacción propia de Meta para una ruta que no atenderá: code 100 en un 400:
Unsupported get request. Object with ID '…' does not exist, cannot be loaded due to missing permissions, or does not support this operation.
Una ruta que sí se admite pero se llama con un método que no ofrece devuelve el mismo código con un mensaje más corto: "Unsupported delete request." Ambos son deliberadamente indistinguibles de un objeto que simplemente no puede ver: un error que confirmara qué endpoints existen sería una forma de mapear la superficie.
Rechazado a propósito
Estos se bloquean en todos los métodos, antes de reenviar nada:
| Ruta | Por qué |
|---|---|
register, deregister, request_code, verify_code | Un número de coexistencia nunca debe registrarse en la Cloud API. Hacerlo es precisamente como una cuenta termina fallando en cada envío con el error 133010, y no es reversible de su lado. |
subscribed_apps | Todos los métodos, incluido GET. Un DELETE aquí daría de baja nuestros webhooks de su propia cuenta de WhatsApp y rompería en silencio su propia integración; tampoco ofrecemos la lectura, para no dejar un camino donde un descuido haga eso. |
marketing_messages | Los envíos de marketing pasan por las herramientas de campañas del panel, que aplican la velocidad de envío por cuenta. |
/{template-id}/unpause | Meta identifica la plantilla con un id simple sin cuenta en la ruta, así que no podemos comprobar que la plantilla es suya antes de actuar sobre ella. Reanude desde el panel. |
messages en cualquier método excepto POST | La ruta de envío lleva el limitador de frecuencia y el manejo de suspensión de cuentas. Solo se enruta POST. |
No disponible en un número de coexistencia
Respuestas propias de Meta, comprobadas contra una cuenta real el 10 de agosto de 2026:
| Ruta | Respuesta de Meta |
|---|---|
/{phone-number-id}/conversational_automation | code 100: "Tried accessing nonexisting field" |
/{phone-number-id}/calls | code 100: "Tried accessing nonexisting field" |
/{phone-number-id}/groups | code 131000 en un 500 |
Son restricciones de Meta para los números que funcionan en coexistencia con la aplicación WhatsApp Business, no nuestras: reenviamos la solicitud y esto es lo que regresa. Meta amplía la disponibilidad de vez en cuando, así que si una de estas le importa, vale la pena volver a probar en lugar de confiar indefinidamente en esta tabla.
Rutas que no existen en ningún lado
Cada una de estas aparece en referencias de API de terceros y no es un endpoint de Meta. Las cuatro devuelven code 2500, "Unknown path components":
| Lo que pudo haber leído | Qué usar en su lugar |
|---|---|
POST /{phone-number-id}/mark_message_as_read | POST /{phone-number-id}/messages con un cuerpo status: "read"; vea Cómo marcar un mensaje como leído |
GET /{phone-number-id}/message_status | Los estados de entrega llegan a su webhook; vea Elegir qué recibe |
/{phone-number-id}/whatsapp_calling_settings | /{phone-number-id}/settings |
/{phone-number-id}/conversational_components | Nada: la ruta real más cercana es conversational_automation, que no está disponible en un número de coexistencia |
Real en Meta, pero no en nuestra lista
GET /{waba-id}/phone_numbers funciona en Meta y aquí ni se bloquea ni se reenvía, así que responde code 100. Es uno de los primeros endpoints que llama un cliente de la Cloud API, por eso conviene nombrarlo: su clave ya fija exactamente un número de teléfono, y su id es el que coloca en cada ruta de esta página. Puede leerlo en la pestaña Overview de la clave en el panel.
Lecturas de nodo simples
GET /{v}/{waba-id}?fields=… y GET /{v}/{phone-number-id}?fields=… funcionan en Meta pero no aquí. Una ruta de un solo segmento en esta superficie es un id de multimedia, eso es lo que significa GET /{v}/{media-id}, así que un id de WABA en esa posición se busca como multimedia y falla.
El fallo confunde si no lo espera: primero verifica view_messages, así que una clave sin ese permiso recibe un 403 que nombra un permiso que no tiene nada que ver con lo que se pidió. Use las formas de borde: /{waba-id}/message_templates, /{phone-number-id}/settings y el resto de Administración de la cuenta.
Webhooks en el formato de Meta
La pestaña de webhooks del panel se abre con un formato de carga útil. WizMessage (v1) es la carga útil que documenta la otra página; es la que usa una clave hasta que la cambie.

Elija Meta Graph y su endpoint deja de recibir nuestra carga útil {version, event, timestamp, data} y empieza a recibir el sobre que Meta nos envió, reenviado textualmente: una entrega por cada cambio.
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"time": 1770000123,
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550783881",
"phone_number_id": "106540352242922"
},
"contacts": [{ "profile": { "name": "Priya Sharma" }, "wa_id": "919876543210" }],
"messages": [
{
"from": "919876543210",
"id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgg…",
"timestamp": "1770000123",
"type": "text",
"text": { "body": "Has it shipped yet?" }
}
]
},
"field": "messages"
}
]
}
]
}
Si ya tiene un controlador de webhook de la Cloud API, esa es la forma para la que está escrito.
Cambiar de formato también cambia lo que le pide la pestaña: las casillas de eventos se sustituyen por los nombres de campo propios de Meta, y aparece un token de verificación.

Antes de poder guardar uno
Se requieren tres cosas, y el panel le dirá cuál falta:
- Un secreto de webhook. Es lo único que sirve de clave para
X-Hub-Signature-256, así que sin él una entrega no tendría firma y no se podría verificar. Genere uno en la pestaña Webhooks de la clave. - Un token de verificación. Cualquier cadena que elija; su endpoint también debe conocerla.
- Una URL que ya esté respondiendo. Vea abajo.
El protocolo de verificación
Al guardar, llamamos a su URL exactamente como lo haría Meta:
GET https://your-server.example/webhooks
?hub.mode=subscribe
&hub.verify_token=<the token you set>
&hub.challenge=482910337
Su endpoint debe comprobar que hub.verify_token coincide con lo que configuró y luego responder 200 con hub.challenge como todo el cuerpo de la respuesta: sin envoltorio JSON, sin comillas, sin nada más. El challenge es un número de 9 dígitos, así que un controlador que hace parseInt antes de devolverlo lo regresa sin cambios.
Si el challenge no regresa exactamente igual, el guardado se rechaza y no se almacena nada. Esto es lo que impide que alguien apunte nuestra cola de entregas a una URL que no controla.
// express
app.get('/webhooks', (req, res) => {
if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === VERIFY_TOKEN) {
return res.status(200).send(req.query['hub.challenge'])
}
res.sendStatus(403)
})
Verificar la firma
Cada entrega lleva X-Hub-Signature-256, calculada como la calcula Meta: HMAC-SHA256 del cuerpo sin procesar de la solicitud, con su secreto de webhook como clave, en hexadecimal, con el prefijo sha256=.
| Encabezado | Valor |
|---|---|
X-Hub-Signature-256 | sha256= + HMAC-SHA256 del cuerpo sin procesar, con su secreto de webhook como clave, hexadecimal en minúsculas. |
X-Webhook-Event | meta.<field>: qué campo lleva esta entrega. |
X-Webhook-Id | Id único de este intento de entrega. Elimine duplicados con él. |
User-Agent | facebookplatform/1.0 (+http://developers.facebook.com) |
Nota: Esta no es la misma derivación que la del formato de WizMessage. Aquella firma
timestamp + "." + rawBodyy envía la marca de tiempo en su propio encabezado; esta firma solo el cuerpo, sin marca de tiempo en ninguna parte. Si ejecuta ambos formatos en un mismo servidor, mantenga separados los dos verificadores: uno compartido fallará con el formato para el que no fue escrito.
Firme los bytes sin procesar tal como se reciben. Analizar el JSON y volver a serializarlo cambia el orden de las claves y los espacios, y la firma nunca coincidirá.
const crypto = require('node:crypto')
// express: app.post('/webhooks', express.raw({ type: 'application/json' }), ...)
function verify(req) {
const provided = req.get('X-Hub-Signature-256') || ''
if (!provided.startsWith('sha256=')) return false
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body) // the raw Buffer, not a parsed object
.digest('hex')
// Constant-time compare — a plain === leaks timing information.
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(provided.slice('sha256='.length), 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Elegir qué recibe
El formato de WizMessage le hace marcar eventos. Este le hace marcar los nombres de campo de Meta, que coinciden con aquello a lo que se suscribiría en el App Dashboard de Meta.
| Campo | Qué llega |
|---|---|
messages | Mensajes entrantes y estados de entrega. Lea la nota de abajo. |
message_status | Estados de entrega, como campo propio. |
smb_message_echoes | Mensajes que la empresa envió desde la aplicación WhatsApp Business en su teléfono. |
smb_app_state_sync | Cambios en la libreta de direcciones hechos en ese teléfono. |
user_id_update | Cambió la identidad de WhatsApp de un contacto. |
message_template_status_update | Una plantilla fue aprobada, rechazada o pausada. |
message_template_quality_update | Cambió la puntuación de calidad de una plantilla. |
account_update | La cuenta de WhatsApp se conectó, se desconectó, se restringió o se dio de baja. |
history | Conversaciones pasadas, durante la sincronización inicial. Cargas útiles grandes: actívelo solo si las quiere. |
Los webhooks de Graph nuevos empiezan suscritos solo a messages. Desmarcar todos los campos apaga las entregas sin eliminar la configuración.
Nota:
messagesya incluye los estados de entrega. Meta envía ambos bajo"field": "messages": los mensajes entrantes en un arreglomessagesy los reportes de entrega en un arreglostatusesen el mismo objeto de valor. Así que la suscripción predeterminada basta para correlacionar elwamidque obtuvo de un envío.message_statuses un campo adicional, no el que necesita para el seguimiento básico de estados. Leavalue.statusesantes de suponer que le falta una suscripción.
Tres cosas que difieren del formato de WizMessage
Vale la pena leerlas si está trasladando una integración, porque cada una cambia en silencio lo que aparece.
- La entrega no depende del sistema de chatbot. En el formato de WizMessage, los mensajes entrantes solo llegan a su endpoint cuando el sistema de chatbot de la cuenta está en Webhook. El formato Graph ignora ese ajuste por completo: configurar una URL y suscribirse a un campo es toda la activación.
- Recibe
status: "sent"y los estados de campaña. El formato de WizMessage descarta ambos deliberadamente. Aquí recibe lo que envió Meta, lo que significa notablemente más tráfico de estados en una cuenta que ejecuta campañas masivas. - No hay un
message.sentsintético. El formato de WizMessage fabrica uno para los mensajes enviados desde la API, el panel o el teléfono. Meta cubre lo mismo consmb_message_echoesy el estadosent, así que aquí no se inventa nada.
Entrega, reintentos y desactivación
Idéntico al formato de WizMessage: se aplica la misma tabla. En resumen: esperamos 10 segundos, nunca seguimos redirecciones, reintentamos 5xx/408/429/tiempos de espera agotados cinco veces con retroceso, nunca reintentamos otros 4xx y tratamos 410 Gone como “desactive este webhook”.
Las entregas son de al menos una vez. Elimine duplicados con X-Webhook-Id y responda 2xx rápido.
Probarlo sin escribir código
El cuadro de diálogo Test API de la clave tiene un control API Format. Póngalo en Meta Graph y el selector de endpoint se sustituye por el destino real de su cuenta, POST /v26.0/{your phone number id}/messages, con un cuerpo con la forma de Meta ya completado.

“Simulate Only” devuelve la solicitud sin contactar a Meta; al cambiar a Send Real Message se envía y le muestra la respuesta real de Meta, con todo y su formato de error.
El cuadro de diálogo apunta a POST /{phone-number-id}/messages y a nada más, así que los endpoints de administración de la cuenta no se pueden alcanzar desde él: pruébelos con curl o con su propio cliente.
Get code tiene el mismo control, así que el fragmento generado apunta a la superficie que elija, en cualquiera de los ocho lenguajes. Deje “Require signature” desactivado en una clave que piense usar aquí: si está activado, el fragmento de Graph generado también firma la solicitud, que es lo único que un cliente de la Cloud API sin modificar no puede hacer.