GoZap Analytics

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ódigoHTTPReintentoQué 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ódigoHTTPReintentoQué 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ódigoQuiere decirEn 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ódigoEl 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

Cuatro ramas, no 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.