API v1
Errores
16 códigos, y una regla que decide qué hacer con todos ellos:
se reintentan el 429 y el 500. Los demás no — o el cuerpo está mal,
o el código no está donde crees, y volver a mandarlo va a fallar igual.
La forma
{
"ok": false,
"error": {
"codigo": "campo_requerido",
"mensaje": "Falta `id_externo`: el identificador de la venta en tu sistema.",
"detalle": "id_externo"
}
}
Ramifica sobre codigo, nunca sobre mensaje. El texto de
mensaje está escrito para la persona que está depurando y puede mejorar en
cualquier despliegue; codigo es el contrato y no cambia.
detalle es opcional.
El estado HTTP acompaña al código y nunca lo contradice: si necesitas decidir rápido y no
quieres leer el cuerpo, r.status alcanza.
Los que no se reintentan
Son 14. Reintentar no cambia nada: o falta un permiso, o el cuerpo está mal, o el fan no existe. Arréglalo y vuelve a mandar una sola vez.
| Código | HTTP | Reintento | Qué pasó | Qué hacer |
|---|---|---|---|---|
no_autorizado |
401 | No reintentar | La llave no existe, fue revocada, o el programa está desactivado. | Revisa la llave. Reintentar no la va a arreglar. |
sin_permiso |
403 | No reintentar | La llave es válida pero no tiene el permiso que pide esa ruta. | Pídele el permiso al dueño del programa. |
cuerpo_invalido |
400 | No reintentar | El JSON no parsea. | Es un bug de tu lado. Revisa el Content-Type y el cuerpo. |
campo_requerido |
400 | No reintentar | Falta un campo obligatorio. | detalle dice cuál. Corrige y vuelve a mandar. |
fan_no_reconocido |
400 | No reintentar | El valor de fan no se parece a ninguna de las formas aceptadas. |
Revisa qué te dio el escáner. Ver el campo fan. |
fan_no_encontrado |
404 | No reintentar | Ese fan no existe en este programa. | En caja: invita a la persona a conectarse al WiFi. La API nunca crea fans. |
importe_invalido |
400 | No reintentar | El importe no se pudo leer como cantidad. | Ver los formatos aceptados en tx. Aceptamos casi cualquiera. |
codigo_no_encontrado |
404 | No reintentar | Ese código no existe en este programa. | Revisa que sea del programa correcto. Reintentar no lo va a crear. |
codigo_liberado |
409 | No reintentar | El código salió de circulación sin usarse, así que no puede canjearse. | No lo reintentes: repórtalo. Significa que de tu lado hay códigos viejos vivos — un lote reenviado, una caché, un respaldo. |
codigo_no_asignado |
409 | No reintentar | Ese código nunca se le entregó a nadie, así que no puede canjearse. | No lo reintentes: repórtalo. O entró por otro canal, o alguien lo adivinó. |
promo_no_encontrada |
404 | No reintentar | Ningún id ni nombre coincide con una promoción de este programa. | Lista las que hay con GET /api/v1/promos. |
promo_terminada |
409 | No reintentar | Esa promoción ya terminó y no admite inventario nuevo. | Depositar ahí sería tirar códigos a un pozo del que nadie los va a sacar. |
demasiados_codigos |
400 | No reintentar | Pasaste el tope de códigos por llamada. | Parte el lote. El tope está en la referencia de codes. |
fecha_invalida |
400 | No reintentar | El desde del roster no se pudo leer como fecha. |
Manda el siguiente que te dimos, tal cual. No lo reconstruyas. |
Los que sí, con espera
Son 2, y los dos se reintentan igual: backoff exponencial y
siempre el mismo id_externo. Eso último es lo que hace que sea seguro —
ver idempotencia.
| Código | HTTP | Reintento | Qué pasó | Qué hacer |
|---|---|---|---|---|
demasiadas_llamadas |
429 | Reintentar con espera | Pasaste el tope de ritmo. | Backoff exponencial, con el mismo id_externo. |
error_interno |
500 | Reintentar con espera | Es nuestro, no tuyo. | Reintenta con el mismo id_externo: la idempotencia hace que sea seguro. |
Cuatro que se malinterpretan seguido
promo: null no es un error
fan/resolve devuelve 200 con "promo": null cuando
la persona no trae promoción vigente. Es el caso normal, no una falla: la mayoría de los
fans, la mayoría de las noches, no tienen una promoción encima. Si tu caja enseña un
mensaje de error ahí, va a enseñarlo casi siempre.
duplicado: true tampoco
Un reintento que llega tarde contesta 200, no 409. Trae
puntos_otorgados: 0 y el saldo real. Por qué
lo hacemos así.
fan_no_reconocido y fan_no_encontrado no son lo mismo
| Código | Quiere decir | En la caja |
|---|---|---|
fan_no_reconocido 400 |
Lo que mandaste no se parece a ninguna de las cinco formas del campo
fan. Es un problema de formato |
Bug de tu lado, o el escáner leyó basura. Revisa qué estás mandando |
fan_no_encontrado 404 |
El valor está bien formado, pero ese fan no existe en este programa. Es un problema de identidad | Invita a la persona a conectarse al WiFi. La API nunca crea fans |
La distinción importa porque llevan a acciones opuestas: uno lo arregla quien programa, el otro lo arregla quien cobra.
Los dos 409 de redeem son una alarma, no un error tuyo
Un cupón solo se canjea desde asignado: se le entregó a una persona y esa persona lo usó. Los otros dos estados se rechazan, y cada rechazo dice algo distinto sobre lo que está pasando del otro lado del mostrador.
| Código | El cupón estaba… | Qué significa de verdad |
|---|---|---|
codigo_liberado 409 |
Liberado: se repartió y salió de circulación sin usarse | Alguien presentó un cupón viejo. Casi siempre hay un lote reenviado, una lista impresa de otro evento o una caché en la terminal |
codigo_no_asignado 409 |
Disponible: nunca se le entregó a nadie | Ese código no salió por este canal. O entró por otro lado, o alguien lo adivinó |
No los reintentes y no los conviertas en canje. Aceptarlos inflaría la tasa de canje y haría provisionar dinero por descuentos que este sistema nunca repartió. Peor: cada uno es la señal de que algo va mal, y volverlo un 200 la borra.
Los contamos por código y el panel del venue los muestra bajo el funnel de la promoción. Si el número crece, la conversación es con quien opera la caja — no con nosotros.
Un manejador que cubre los 16
const r = await fetch("https://app.gzanalytics.com/api/v1/tx", opciones);
const cuerpo = await r.json();
if (cuerpo.ok) return cuerpo.data;
switch (cuerpo.error.codigo) {
// Esperan y vuelven, con el MISMO id_externo.
case 'demasiadas_llamadas':
case 'error_interno':
return reintentarConBackoff();
// La persona no está registrada: es acción de caja, no de código.
case 'fan_no_encontrado':
return pedirRegistroEnElPortal();
// Nuestra llave. Alerta al equipo, no a la cajera.
case 'no_autorizado':
case 'sin_permiso':
return alertarSoporte(cuerpo.error);
// Bug propio: cuerpo mal armado. Loguear con detalle.
default:
return registrarBug(cuerpo.error);
}
No necesitas una rama por código. Los 16 caen en cuatro reacciones, y las dos tablas de arriba los agrupan exactamente así.
No pongas el mensaje de la API en la pantalla de la caja. Está
escrito para quien depura, en el idioma de la API y no del negocio. La cajera necesita
"pide a la persona que se conecte al WiFi", no "fan_no_encontrado". Traduce los cuatro
casos de arriba a las cuatro frases que tenga sentido leer en el mostrador.
Cuando nada de esto explica lo que ves
Cada llamada queda registrada de nuestro lado con ruta, código, latencia y un resumen, visible para el venue en su panel (⚙️ Configuración → 🔌 Integraciones), y se conserva 30 días. Cuando la conversación sea "se los mandamos y no llegó", la respuesta está ahí en cinco segundos, sin que nadie tenga que buscar logs.
Si necesitas que la miremos contigo, escríbenos con el
id_externo y la hora aproximada.