Ir al contenido
32 min read

Conexión con la API de Meta Graph

Apunte una integración existente de la Cloud API de WhatsApp a WizMessage sin reescribirla: las rutas de Meta, los cuerpos de solicitud de Meta, el formato de error de Meta, webhooks en el formato propio de Meta y los endpoints de administración de la cuenta para plantillas, perfil de empresa, códigos QR y flows.

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

La API de WizMessage comparada con la API compatible con Meta GraphElija la que corresponda a lo que ya tieneAPI de WizMessage/api/v1/externalvariables de plantilla con nombreinterfaz de asignación en el panelcampo reference, consulta de estadonuestro propio formato JSONlo mejor si empieza desde ceroAPI de Meta Graph/v26.0/{phone-number-id}/…cuerpos de solicitud de Metaformato de error de Metaadministre plantillas y perfilwebhooks en el formato de Metalo mejor si ya tiene código de la Cloud APILas mismas claves, los mismos permisos, los mismos límites de frecuencia. Use ambas si quiere.
Dos superficies, una cuenta. Ninguna va a desaparecer.

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.
  • v26 a secas se rechaza. El número menor no es opcional. v26.0 está bien, v26 da 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.

EncabezadoValor
AuthorizationBearer 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 190 con un 401. 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:

EncabezadoValor
X-Subscription-WarningSubscription 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"
	}
}
Qué ocurre con el acceso a la API después de que vence una suscripciónuna clave de APInormalsin encabezadoventana de gracia7 días, las llamadas siguen funcionandoX-Subscription-Warning en cada respuestarechazadocode 10 en 403primera llamada tras el vencimientoaquí empieza el relojtermina la ventanasin más avisosRenovar en cualquier momento restablece el acceso en unos minutos.
El vencimiento en sí no cambia nada. El reloj empieza con su siguiente llamada.

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-Warning es el único aviso previo que existe: regístrelo, o lo primero que sabrá es un 403.

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: to recibe 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.164 y rechaza cualquier otra cosa, mientras que esta pasa su valor tal cual a Meta. La única excepción es POST /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 contacts de 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.

Cómo le llega un error según si lo produjo MetaUn error en esta superficieLo rechazó Metase reenvía textualel código y subcódigo de Metael fbtrace_id propio de Metael estado también es de MetaLo rechazamos nosotrosautenticación, permisos, límite de frecuenciaescritos con los códigos de Metapara que un solo controlador cubra ambos401 · 403 · 429 · 400 · 500Nunca suponga que type es OAuthException: Meta usa varios.
Los errores propios de Meta se reenvían intactos. Los nuestros toman prestados los códigos de Meta.

Errores que generamos nosotros

codeHTTPCuándo
190401Clave 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.
10403La 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.
4429Se agotó el límite por minuto o diario de la clave.
100400Solicitud mal formada, un id en la ruta que no es suyo o un endpoint que esta superficie no atiende.
2500Algo 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 type de ese ejemplo: GraphMethodException, no OAuthException. Meta usa varios tipos de excepción y no los normalizamos. Decida según error.code, que es un número y es estable; trate type como 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étodoRutaQué hace
POST/{version}/{phone-number-id}/mediaSube un archivo. Multipart, devuelve {"id": "…"}.
GET/{version}/{media-id}El objeto de metadatos de Meta para el archivo.
GET/{version}/{media-id}/downloadLos bytes.
DELETE/{version}/{media-id}Lo elimina, devuelve {"success": true}.
Subir un archivo una vez y enviarlo repetidamentePOST /{v}/{pnid}/mediael archivo en sí{ id }reutilizablePOST /{v}/{pnid}/messageshaga referencia al idEntregadolos estados llegan por webhook
Subir devuelve un id. El id es lo que se coloca en un mensaje.

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:

LlamadaPermiso
POST /{v}/{pnid}/messages con "type": "template"send_template
igual, con type de image, video, audio, document o stickersend_media
igual, cualquier otro: text, interactive, reaction, location, contacts, y marcar como leídosend_text
POST /{v}/{pnid}/media: subirsend_media
DELETE /{v}/{media-id}send_media
GET /{v}/{media-id} y /downloadview_messages

Administración de la cuenta; vea Administración de la cuenta:

LlamadaPermiso
GET /{v}/{waba-id}/message_templatesview_templates
POST y DELETE /{v}/{waba-id}/message_templatesmanage_templates
GET /{v}/{waba-id}/template_analyticsview_analytics
GET en perfil de empresa, códigos QR, ajustes de comercio, lista de bloqueados, ajustes del teléfono y flowsview_templates
POST en esos mismos seismanage_profile
DELETE /{v}/{pnid}/message_qrdls/{code} y DELETE /{v}/{pnid}/block_usersmanage_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_media aquí. En la API de WizMessage no requiere ningún permiso, con el razonamiento de que send_media gobierna 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 devolver 403, este es el motivo.

Nota: view_templates controla 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 devolver 403 con un mensaje sobre plantillas, que no es una pista útil.

Nota: Tres botones son solo de Meta Graph. manage_templates, manage_profile y view_analytics no 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.

Los endpoints de administración de la cuenta agrupados por el permiso que necesita cada unoLeer cualquier cosalos ocho recursosperfil, códigos QR, comerciolista de bloqueados, ajustes, flowsplantillasview_templatesEscribir en la cuentaperfil y presenciaperfil de empresa, códigos QRcomercio, lista de bloqueadosajustes, flowsmanage_profileEscribir plantillascrear y eliminarmessage_templatesanalytics va apartemanage_templatesLa analítica de plantillas tiene su propio botón: view_analytics.
Leer es un solo botón. Escribir se divide según lo que escriba.

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 un DELETE se 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_token y appsecret_proof. Esos dos se eliminan porque Meta da prioridad a un token en la cadena de consulta sobre el encabezado Authorization, 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 100 descrito en Cuando un id no es suyo.

Nota: paging.next apunta a Meta, no a nosotros. Las respuestas se devuelven exactamente como las envió Meta, así que un enlace next en una respuesta paginada es una URL de graph.facebook.com, y su clave de API no autentica allí. Pagine tomando paging.cursors.after y 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étodoPermisoDevuelve
GETview_templatesEl { data, paging } de Meta
POSTmanage_templatesLa respuesta de creación de Meta
DELETEmanage_templatesLa respuesta de eliminación de Meta

GET acepta limit (1–100, 25 por defecto) y after para la paginación por cursor.

Nota: GET acepta solo limit y after. 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=APPROVED devuelve un 200 con todas las plantillas, aprobadas o no. Filtre de su lado, o use after para 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 llaman whatsapp_calling_settings; esa ruta no existe en Meta y devuelve code 2500 aquí, 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.

Por qué un endpoint podría no estar disponible en esta superficieun endpoint que no está aquíLo rechazamos nosotrosregistration, subscribed_appsdañaría su cuentano hay alternativaLo rechaza Metano en un número de coexistenciaconversational_automation, callsla respuesta propia de MetaNo existecuatro rutas de referencias de APIMeta responde code 2500use la ruta realUn cuarto grupo simplemente aún no está en nuestra lista; vea más abajo.
Cuatro motivos, y solo uno es nuestro.

Rechazado a propósito

Estos se bloquean en todos los métodos, antes de reenviar nada:

RutaPor qué
register, deregister, request_code, verify_codeUn 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_appsTodos 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_messagesLos envíos de marketing pasan por las herramientas de campañas del panel, que aplican la velocidad de envío por cuenta.
/{template-id}/unpauseMeta 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 POSTLa 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:

RutaRespuesta de Meta
/{phone-number-id}/conversational_automationcode 100: "Tried accessing nonexisting field"
/{phone-number-id}/callscode 100: "Tried accessing nonexisting field"
/{phone-number-id}/groupscode 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ídoQué usar en su lugar
POST /{phone-number-id}/mark_message_as_readPOST /{phone-number-id}/messages con un cuerpo status: "read"; vea Cómo marcar un mensaje como leído
GET /{phone-number-id}/message_statusLos 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_componentsNada: 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.

La pestaña Webhooks con el formato de carga útil en WizMessage, que muestra las casillas de eventos de v1

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.

La pestaña Webhooks en formato Meta Graph, que muestra el token de verificación y los nueve nombres de campo de Meta a los que se puede suscribir

Antes de poder guardar uno

Se requieren tres cosas, y el panel le dirá cuál falta:

Lo que debe estar listo antes de poder guardar un webhook de Meta GraphSecreto del webhookes la clave de la firmaToken de verificaciónprueba que la URL es suyaUna URL activala llamamos antes de guardarGuardadoempiezan las entregasUna URL que todavía no responde no se puede guardar. Primero publique, luego configure.
Las tres se verifican cuando presiona guardar, en este orden.
  • 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=.

EncabezadoValor
X-Hub-Signature-256sha256= + HMAC-SHA256 del cuerpo sin procesar, con su secreto de webhook como clave, hexadecimal en minúsculas.
X-Webhook-Eventmeta.<field>: qué campo lleva esta entrega.
X-Webhook-IdId único de este intento de entrega. Elimine duplicados con él.
User-Agentfacebookplatform/1.0 (+http://developers.facebook.com)

Nota: Esta no es la misma derivación que la del formato de WizMessage. Aquella firma timestamp + "." + rawBody y 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.

CampoQué llega
messagesMensajes entrantes y estados de entrega. Lea la nota de abajo.
message_statusEstados de entrega, como campo propio.
smb_message_echoesMensajes que la empresa envió desde la aplicación WhatsApp Business en su teléfono.
smb_app_state_syncCambios en la libreta de direcciones hechos en ese teléfono.
user_id_updateCambió la identidad de WhatsApp de un contacto.
message_template_status_updateUna plantilla fue aprobada, rechazada o pausada.
message_template_quality_updateCambió la puntuación de calidad de una plantilla.
account_updateLa cuenta de WhatsApp se conectó, se desconectó, se restringió o se dio de baja.
historyConversaciones 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: messages ya incluye los estados de entrega. Meta envía ambos bajo "field": "messages": los mensajes entrantes en un arreglo messages y los reportes de entrega en un arreglo statuses en el mismo objeto de valor. Así que la suscripción predeterminada basta para correlacionar el wamid que obtuvo de un envío. message_status es un campo adicional, no el que necesita para el seguimiento básico de estados. Lea value.statuses antes 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.

Tres comportamientos que difieren entre los dos formatos de webhookSin filtro de chatbotse entrega de todos modosv1 necesita el modo de bot = WebhookGraph ignora ese ajusteRecibe senty estados de campañav1 filtra ambosGraph reenvía todoSin sent sintéticoeventos propios de Meta en su lugarsmb_message_echoesy estadosEspere más tráfico en el formato Graph que el que produjo la misma cuenta en v1.
Las tres hacen que el formato Graph entregue más, no menos.
  • 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.sent sintético. El formato de WizMessage fabrica uno para los mensajes enviados desde la API, el panel o el teléfono. Meta cubre lo mismo con smb_message_echoes y el estado sent, 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.

El cuadro de diálogo Test API en modo Meta Graph, que muestra el endpoint messages de solo lectura y un cuerpo de solicitud con la forma de Meta

“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.