Errores
Todo fallo generado por Joinotify usa el mismo sobre:
{
"error": {
"message": "Frase legible que describe lo que ocurrió.",
"type": "codigo_estable"
}
}
Programa contra el campo type — es estable. message está escrito para humanos y su
redacción puede cambiar sin aviso.
Campos opcionales
Algunos campos aparecen cuando tienen sentido.
issues — fallos de validación
{
"error": {
"message": "Invalid request payload.",
"type": "invalid_request",
"issues": [
{
"code": "too_small",
"path": ["body"],
"message": "String must contain at least 1 character(s)"
}
]
}
}
meta — error reenviado de Meta
Cuando el rechazo viene de Meta, devolvemos su cuerpo original intacto. Ahí están el
code, el error_subcode y el fbtrace_id que pide el soporte de Meta.
{
"error": {
"message": "The Meta Cloud API rejected the request.",
"type": "meta_error",
"meta": {
"error": {
"message": "(#131047) Re-engagement message",
"type": "OAuthException",
"code": 131047,
"error_subcode": 2494010,
"fbtrace_id": "AbCdEfGhIjK"
}
}
}
}
reason y action — sin derecho de uso
Un 402 dice por qué se bloqueó el acceso y cuál es la salida: qué hacer
(action.kind) y la página del panel donde se hace (action.url). Muestra ambas cosas
a quien administra la integración — en un log de errores, "pago requerido" sin un enlace
es un callejón sin salida.
{
"error": {
"message": "Your free trial has ended. Subscribe to keep using the API.",
"type": "payment_required",
"reason": "trial_ended",
"billable_numbers": 1,
"action": {
"kind": "subscribe",
"url": "https://app.joinotify.com/checkout"
}
}
}
action.kind | Cuándo |
|---|---|
subscribe | Prueba terminada, suscripción cancelada o inexistente |
update_payment_method | El último pago falló (past_due) |
pay_invoices | Hay facturas pendientes (unpaid) |
finish_checkout | El primer pago aún no se confirmó (incomplete) |
resume_subscription | La suscripción está en pausa (paused) |
renew_license | La licencia caducó (license_expired) |
upgrade_plan | Se alcanzó el techo mensual de mensajes del plan (message_limit) |
manage_numbers | Hay más números conectados de los que la cuenta paga (capacity_exceeded) |
El bloqueo no llega sin aviso. Durante el período de gracia las peticiones siguen
pasando, y la respuesta trae la cabecera X-Joinotify-Billing-Warning con el motivo —
past_due, por ejemplo. Registra esa cabecera en tus logs: es la que avisa que el 402
está en camino.
allowed — método no aceptado
Una ruta que existe, llamada con el método equivocado, responde 405 y no 404. Los
métodos aceptados vienen en la cabecera Allow y en error.allowed, para que busques
el error en el verbo y no en la URL.
{
"error": {
"message": "PUT is not supported on this route. Allowed: DELETE, GET, HEAD.",
"type": "method_not_allowed",
"allowed": ["DELETE", "GET", "HEAD"]
}
}
request_id — fallo inesperado
Un 500 no dice qué se rompió — la causa queda en nuestros registros, nunca en la
respuesta. Lo que trae es request_id, el mismo valor de la cabecera x-request-id.
Envía ese id a soporte y encontraremos el fallo.
{
"error": {
"message": "Internal server error.",
"type": "internal",
"request_id": "0d72872e-f70e-4c25-95c6-6bec4d33a800"
}
}
Tabla de códigos
| HTTP | type | Significado | Qué hacer |
|---|---|---|---|
400 | invalid_request | Ningún ID de recurso después de /v1/. | Corrige la ruta. |
400 | invalid_request | El cuerpo no es JSON válido, o la ruta tiene una secuencia % mal formada. | Corrige el cuerpo o la codificación de la URL. |
401 | authentication | Token ausente, mal formado, desconocido o revocado. | Revisa la cabecera y la clave. |
402 | payment_required | Sin derecho de uso: prueba terminada, pago pendiente, techo del plan… | Sigue error.action. |
403 | tenant_inactive | Cuenta suspendida o cancelada. | Habla con soporte. |
403 | forbidden | El recurso no pertenece a tu cuenta. | Confirma el phone_number_id / waba_id. |
403 | forbidden_endpoint | Ruta en la lista de bloqueo del proxy. | Consulta Compatibilidad con Meta. |
404 | not_found | Ruta, clave, endpoint o entrega inexistente. | Comprueba el ID. |
405 | method_not_allowed | La ruta existe, pero no con ese método. | Usa uno de los métodos de Allow. |
409 | no_phone_number | Ningún número conectado y from omitido. | Conecta un número o indica from. |
409 | no_waba | Ninguna cuenta de empresa conectada y wabaId omitido. | Conecta una WABA o indica wabaId. |
409 | idempotency_in_progress | Una petición con el mismo Idempotency-Key sigue en curso. | Espera y repite con la misma clave. |
422 | invalid_request | El payload no pasó la validación. | Lee issues. |
413 | payload_too_large | El cuerpo es mayor de lo que acepta la ruta. | Divide el lote o reduce el cuerpo. |
415 | unsupported_media_type | Charset o Content-Encoding del cuerpo no aceptado. | Envía JSON en UTF-8, sin compresión o con gzip. |
422 | meta_error | Meta lo rechazó (4xx). | Lee error.meta. |
422 | idempotency_key_reused | El Idempotency-Key ya se usó con otro cuerpo. | Usa una clave nueva por operación. |
429 | rate_limit | Límite de la categoría superado. | Espera Retry-After. Consulta Límites de uso. |
500 | internal | Fallo inesperado por nuestro lado. | Reintenta; si persiste, envíanos el request_id. |
502 | meta_error | Meta respondió con un 5xx. | Reintenta con backoff. |
502 | upstream_error | No pudimos alcanzar a Meta. | Reintenta con backoff. |
504 | upstream_error | Meta tardó más de 30 segundos. | Reintenta con backoff. |
¿Error nuestro o de Meta?
Una regla sencilla: si existe el campo error.meta, el rechazo fue de Meta. Joinotify
solo lo reenvió.
Eso importa a la hora de actuar. invalid_request significa que tu payload está mal y un
reintento idéntico volverá a fallar. meta_error suele ser una regla de negocio de
WhatsApp — ventana de 24 horas cerrada, plantilla no aprobada, número inválido — y la
corrección es otra.
En el espejo /v1/* la regla cambia: cuando Meta responde, recibes su respuesta, en
su formato, sin nuestro sobre. El sobre de Joinotify solo aparece en fallos anteriores a
la llamada.
Rastrear una petición
Cada respuesta trae x-request-id. Si envías tu propio x-request-id, te lo devolvemos —
lo que facilita cruzar los registros de ambos lados.
Tu id solo se acepta si tiene de 8 a 128 caracteres, entre letras, dígitos, ., _, :
y -. Si no, generamos uno nuestro (un UUID) — confía siempre en el que vuelve en la
cabecera.
curl -i https://api.joinotify.com/templates \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'x-request-id: pedido-1042-intento-1'
Guarda ese identificador en tus registros. Es lo primero que pide soporte.
Qué errores conviene reintentar
| Situación | ¿Reintentar? |
|---|---|
429 rate_limit | Sí, después de Retry-After. |
500 · 502 · 504 | Sí, con backoff exponencial y un tope de intentos. |
409 idempotency_in_progress | Sí, tras unos segundos, con la misma clave. |
402 payment_required | No. Resuelve lo que indica error.action. |
401 · 403 · 404 · 405 · otros 409 | No. El reintento fallará igual. |
400 · 413 · 415 · 422 invalid_request | No. Corrige el payload. |
422 meta_error | Depende del code de Meta. Un error de ventana o de plantilla no se arregla reintentando. |