Receta
Integrar un punto de venta en una tarde
Toda la integración se puede seguir con curl y nada más. Lo que
esta receta agrega sobre la guía de API es el otro lado del
mostrador: qué hace quien administra el venue mientras quien programa escribe.
Antes de nada, una pregunta que decide si esto toma una tarde o una semana: ¿tu proveedor va a recibir nombres y correos?
Si sí, es una transferencia de datos personales y tiene que estar nombrado en tu aviso de privacidad antes del primer dato. No se arregla después. Si no —y la mayoría de las integraciones no lo necesitan— arrancas hoy.
Crear la llave, en modo prueba
⚙️ Configuración → 🔌 Integraciones → ➕ Nueva integración. Modo prueba siempre la primera vez: guarda lo que manden para que lo veas, pero no mueve saldos reales.
Marca solo los permisos que necesiten. Empieza con menos de lo que te pidan: agregar uno después es rotar la llave, un minuto; quitar uno que ya usaron en producción rompe su integración un viernes.
La llave se muestra una sola vez. Mándala por un gestor de contraseñas o por teléfono, no por WhatsApp. Si dudas de cómo llegó, rótala: es gratis.
Sabes que quedó cuando la integración aparece en la lista con su pista de llave y dice «Todavía sin usar».
La primera llamada, siempre ping
Con la llave en una variable de entorno, nunca en el código:
export GZ_KEY=gz_test_ehyjhn45u7d34uk7zq7qc4ugbaskjmt5
curl -X POST https://app.gzanalytics.com/api/v1/ping \
-H "Authorization: Bearer $GZ_KEY" \
-H "Content-Type: application/json"
Contesta contra qué programa quedó la llave, con qué permisos y en qué modo:
{
"ok": true,
"data": {
"integracion": "Punto de venta",
"programa": "Fan Pass · Estadio Metropolitano",
"modo": "vivo",
"permisos": [
"fan:leer",
"tx:escribir"
],
"hora": "2026-08-21T05:15:22.282Z"
}
}
Que esto sea lo primero no es ceremonia: contesta de un golpe las tres preguntas que hacen perder una tarde entera —¿la llave llegó bien?, ¿apunta al programa correcto?, ¿estoy en pruebas o en vivo?
Sabes que quedó cuando "modo": "prueba". Si dice vivo, para y avisa antes de mandar nada más.
Resolver al fan y registrar una venta
Dos llamadas. La primera dice quién trae el QR; la segunda registra la venta.
curl -X POST https://app.gzanalytics.com/api/v1/fan/resolve \
-H "Authorization: Bearer $GZ_KEY" -H "Content-Type: application/json" \
-d '{"fan":"GZM26-2T69D"}'
curl -X POST https://app.gzanalytics.com/api/v1/tx \
-H "Authorization: Bearer $GZ_KEY" -H "Content-Type: application/json" \
-d '{"id_externo":"TICKET-9001","fan":"GZM26-2T69D","importe":"1,500.50"}'
El campo fan acepta lo que sea que dé el escáner —Fan ID, token del QR, la
URL completa, un código de cupón o el correo—, así que no hay que normalizar nada del lado
de la caja.
{
"ok": true,
"data": {
"duplicado": false,
"tx": 1,
"puntos_otorgados": 1500,
"saldo": 1543,
"fan": {
"id": "GZM26-2T69D",
"correo": "fan@ejemplo.mx"
}
}
}
El único detalle que decide si esta integración es buena o mala:
id_externo tiene que ser el folio del ticket, y tiene que ser el mismo en
cada reintento. Un identificador nuevo por intento convierte cada reintento en una
venta nueva, y ese es exactamente el bug que la idempotencia existe para evitar.
Cómo funciona.
Sabes que quedó cuando "duplicado": false la primera vez y true la segunda, con puntos_otorgados: 0. Prueben mandarlo dos veces a propósito.
Mirar la bitácora con ellos
Mientras prueban, abre Integraciones → 📋 Bitácora. Cada llamada con su ruta, su código de respuesta, cuánto tardó y un resumen.
Esta pantalla es tu mejor herramienta en esta relación. Cuando tu proveedor diga «se los mandamos y no llegó», la respuesta está ahí en cinco segundos y sin pedirle registros a nadie. Se conserva 30 días.
Si ves códigos 4xx repetidos, míralos juntos: los de autorización son de la
llave o los permisos —tuyos—, los de formato son de su código.
Sabes que quedó cuando la fila de la integración muestra llamadas en las últimas 24 h y cero con error.
El manejador de errores, que casi nadie escribe
Los doce códigos caen en cuatro reacciones. No hace falta una rama por código:
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.
default:
return registrarBug(cuerpo.error);
}
No pongan el mensaje de la API en la pantalla de la caja. Está escrito para
quien depura. La cajera necesita «pide a la persona que se conecte al WiFi», no
fan_no_encontrado. El catálogo completo.
Sabes que quedó cuando ramifican sobre error.codigo, no sobre error.mensaje. El texto puede cambiar; el código no.
Pasar a vivo
Crea una llave nueva en modo en vivo y entrégasela. Su código no cambia: solo el valor de la variable de entorno.
Deja la de prueba viva unos días — les sirve para seguir probando sin ensuciar saldos reales.
Sabes que quedó cuando la respuesta de ping dice "modo": "vivo" y una venta de prueba mueve el saldo de verdad. Compruébalo con tu propio Fan ID, no con el de un cliente.
Qué gana el venue con esto
Tres cosas que antes eran trabajo manual y ahora no existen:
| Antes | Ahora |
|---|---|
| Pegar la lista de canjes a mano al día siguiente | Entran solos, en el momento del canje |
| Los puntos solo suben por visitar | Suben con el gasto real, que es lo que hace volver a la gente |
| «Se los mandamos y no llegó» | La bitácora lo contesta en cinco segundos |
Si el punto de venta también emite los códigos de tus promociones, puede depositarlos él mismo con la llave de inventario, y se acabó el archivo que alguien tiene que pegar.