Bearer token por cliente
Cada API key de AVEX está limitada al cliente que la solicita. Envíala en el header Authorization como Bearer token y AVEX restringe automáticamente cada operación a los recursos de ese cliente. El mismo header viaja en todas las rutas v1; el ejemplo lista las instituciones visibles para tu clave — los UUID que después usas en los upserts.
curl https://www.avex.com.co/api/v1/institutions \
-H "Authorization: Bearer avex_k1_<tu_clave>"Suscripción del cliente y enforcement por transporte
Cuando la suscripción del cliente está bloqueada (trial vencido, suspendida o cancelada), cada transporte se comporta distinto — la regla es la operación hacia el titular sigue; la gestión se bloquea:
| Transporte | Autenticación | Con suscripción bloqueada |
|---|---|---|
| Webhooks de pago entrantes/api/payments/* · /api/webhooks/* | HMAC por cliente | Siguen procesando — reflejan pagos de titulares existentes en pases ya emitidos. Solo un cliente con estado suspended a nivel de cuenta se rechaza. |
| API v1 de gestión e ingesta/api/v1/* | API key | Responde 402 SUBSCRIPTION_INACTIVE (el estado concreto viaja en error.details.subscriptionStatus) — los upserts pueden emitir pases NUEVOS, distribución que una suscripción vencida no obtiene. |
| Verificación de puerta/api/v1/verify/** · /api/v1/entertainment/validate | API key | Exenta — un lector de puerta no se detiene a mitad de evento por un trial vencido. |
La ingesta v1 en los 5 verticales
AVEX es un canal, no un CRM: tu sistema empuja los datos ya computados y AVEX los refleja en el pase de Apple y Google Wallet. Todas las rutas v1 comparten la misma autenticación — Bearer API key con alcance por cliente — y cada una exige el scope de su vertical. Estos son los ejemplos por vertical; el inventario completo de rutas vive en el mapa de endpoints de entrada. Reemplaza avex_k1_<tu_clave> por la API key real que generas en tu panel.
Referencia completa del API
Explora el contrato OpenAPI interactivo — esquemas de request/response, códigos de error y los headers de cada ruta pública.
Abrir referencia OpenAPIEducation — pases de estudiante
scope: passes:updateRefleja el pase del estudiante desde tu SIS. El upsert crea en el primer push y refleja los campos mutables en los siguientes; event empuja el cambio de estado; revoke retira el pase.
curl -X POST https://www.avex.com.co/api/v1/pass/upsert \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000001" \
-H "Content-Type: application/json" \
-d '{
"institutionId": "11111111-1111-4111-8111-111111111111",
"externalId": "EST-12345",
"data": {
"uniqueIdentifier": "1098765432",
"careerId": "ING-SIS",
"name": "Ana María Gómez",
"email": "ana.gomez@example.edu.co",
"semester": 6,
"enrollmentYear": 2023,
"studentStatus": "Active",
"academicCalendarLink": null,
"photoUrl": "https://cdn.example.edu.co/fotos/est-12345.jpg"
}
}'// Node 22+ / TypeScript — fetch nativo, sin dependencias externas.
const res = await fetch("https://www.avex.com.co/api/v1/pass/upsert", {
method: "POST",
headers: {
Authorization: "Bearer avex_k1_<tu_clave>",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"institutionId": "11111111-1111-4111-8111-111111111111",
"externalId": "EST-12345",
"data": {
"uniqueIdentifier": "1098765432",
"careerId": "ING-SIS",
"name": "Ana María Gómez",
"email": "ana.gomez@example.edu.co",
"semester": 6,
"enrollmentYear": 2023,
"studentStatus": "Active",
"academicCalendarLink": null,
"photoUrl": "https://cdn.example.edu.co/fotos/est-12345.jpg"
}
}),
});
// 201 en la primera creación, 200 cuando reflejas un pase ya existente.
if (!res.ok) {
throw new Error(`AVEX respondió ${res.status}`);
}curl -X POST https://www.avex.com.co/api/v1/pass/EST-12345/event \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"institutionId": "11111111-1111-4111-8111-111111111111",
"type": "status.changed",
"data": {
"studentStatus": "Graduated"
}
}'curl -X POST https://www.avex.com.co/api/v1/pass/EST-12345/revoke \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"institutionId": "11111111-1111-4111-8111-111111111111",
"reason": "Retiro voluntario del programa"
}'Loyalty — miembros y saldos
scope: loyalty:writeRefleja al miembro y su saldo desde tu POS/CRM. AVEX nunca calcula puntos: el upsert mantiene los datos de contacto y el snapshot empuja el balance ya computado más el nivel resuelto.
curl -X POST https://www.avex.com.co/api/v1/loyalty/member/upsert \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000002" \
-H "Content-Type: application/json" \
-d '{
"programId": "22222222-2222-4222-8222-222222222222",
"externalId": "SOCIO-789",
"data": {
"name": "Carlos Restrepo",
"email": "carlos.restrepo@example.com",
"phone": "+573001234567",
"accountNumber": "ACC-0099"
}
}'curl -X POST https://www.avex.com.co/api/v1/loyalty/member/SOCIO-789/snapshot \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"balance": 4500,
"tierId": null
}'Real estate — cuotas
scope: installments:writeRefleja el avance de la cuota desde tu sistema de cobranza. El snapshot empuja el acumulado pagado, el saldo restante y el estado — AVEX los refleja verbatim en el pase.
curl -X POST https://www.avex.com.co/api/v1/installment/CUOTA-456/snapshot \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"accumulatedPaid": 1500000,
"remainingBalance": 8500000,
"status": "Active"
}'Utilities — facturas de servicios y conjuntos residenciales
scope: utilities:writeRefleja la factura del período desde tu sistema de facturación. Con accountReference, AVEX mantiene UN pase persistente por cuenta que se actualiza cada período sin emitir un pase nuevo; el snapshot empuja el estado de pago que tu biller ya computó (AVEX nunca calcula vencimientos). Perfil conjunto residencial (PA-018): usa serviceType 'administration' y la nomenclatura del apto como accountReference ('Torre 3 · Apto 501') — la unidad se renderiza en la tarjeta del residente y la cuota mensual actualiza su pase existente.
curl -X POST https://www.avex.com.co/api/v1/utility/bill/upsert \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000003" \
-H "Content-Type: application/json" \
-d '{
"institutionId": "33333333-3333-4333-8333-333333333333",
"accountHolderName": "Luisa Fernanda Ruiz",
"accountHolderEmail": "luisa.ruiz@example.com",
"accountNumberMasked": "****4821",
"serviceType": "electricity",
"billingPeriod": "2026-07",
"amountDue": 186500,
"dueDate": "2026-08-15",
"paymentReference": "FAC-2026-07-004821",
"accountReference": "CONTRATO-004821"
}'curl -X POST https://www.avex.com.co/api/v1/utility/bill/FAC-2026-07-004821/snapshot \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"paymentStatus": "paid",
"paidAt": "2026-07-01T14:30:00-05:00"
}'curl https://www.avex.com.co/api/v1/utility/bill/FAC-2026-07-004821/install-link \
-H "Authorization: Bearer avex_k1_<tu_clave>"Entertainment — boletas de evento
scope: events:write · tickets:validateEmite boletas por externalId dentro de una función existente y valida en puerta con el payload real del wallet. La validación es de un solo uso por boleta (already_used idempotente) y cross-tenant responde 404 sin oráculo. El evento de la función debe estar Publicado — el upsert responde 400 si todavía está en Borrador, Completado o Cancelado.
curl -X POST https://www.avex.com.co/api/v1/entertainment/ticket/upsert \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000004" \
-H "Content-Type: application/json" \
-d '{
"externalId": "TICKET-2026-00891",
"eventId": "44444444-4444-4444-8444-444444444444",
"showtimeId": "55555555-5555-4555-8555-555555555555",
"holderName": "Mariana Duque",
"holderEmail": "mariana.duque@example.com",
"seat": "14",
"row": "C",
"section": "Platea",
"tierId": "66666666-6666-4666-8666-666666666666"
}'curl -X POST https://www.avex.com.co/api/v1/entertainment/validate \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"qrPayload": "b3f1c2d4e5f60718293a4b5c6d7e8f90|482913",
"validatedBy": "puerta-norte-1"
}'Sandbox de validación (dry-run)
Antes de empujar datos reales, valida tu integración con el header X-AVEX-Dry-Run: true. Corre toda la autenticación y validación, devuelve un 200 con la previsualización del efecto y no persiste nada — ningún pase llega a tus usuarios finales.
# Sandbox de validación: agrega X-AVEX-Dry-Run: true a cualquier
# endpoint de ingesta (upserts, snapshots, eventos, revocaciones y void).
# AVEX corre auth + validación + el efecto teórico y responde 200 con la
# previsualización — sin crear pases, sin push, sin email. Quita el
# header para ejecutar de verdad. La validación de puerta NO es ingesta:
# validar quema el boleto; su consulta sin quemar es /v1/verify/*.
curl -X POST https://www.avex.com.co/api/v1/installment/CUOTA-456/snapshot \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "X-AVEX-Dry-Run: true" \
-H "Content-Type: application/json" \
-d '{"accumulatedPaid":1500000,"remainingBalance":8500000,"status":"Active"}'Firmados con HMAC-SHA256
AVEX firma cada webhook con HMAC-SHA256 sobre el body crudo. El header x-avex-signature lleva una o más firmas sha256=<hex> separadas por coma — durante la ventana de rotación de secret (24h tras rotar) van dos, la nueva primero. Acepta la entrega si cualquiera coincide, verificando cada firma con timingSafeEqual antes de leer el payload — nunca confíes en el header sin validar.
# Ejemplo de llamada que AVEX envía a tu webhook.
# La firma HMAC-SHA256 viaja en x-avex-signature: una o más firmas
# sha256=<hex> separadas por coma — durante la ventana de rotación de
# secret (24h tras rotar) van DOS, la del secret nuevo primero —,
# calculadas sobre el body EXACTO que recibes. Headers adicionales:
# x-avex-event → tipo de evento (ej. pass.updated)
# x-avex-delivery-id → id único de esta entrega (para dedupe)
curl -X POST https://tu-servidor.com/webhooks/avex \
-H "Content-Type: application/json" \
-H "User-Agent: AVEX-Webhook/1.0" \
-H "x-avex-event: pass.updated" \
-H "x-avex-delivery-id: 3f6c1e2a-..." \
-H "x-avex-signature: sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$AVEX_WEBHOOK_SECRET" -r | cut -d' ' -f1)" \
-d "$BODY"import crypto from "node:crypto";
/**
* Verifica la firma HMAC-SHA256 que AVEX envía en x-avex-signature.
* El header lleva UNA o MÁS firmas "sha256=<hex>" separadas por coma:
* durante la ventana de rotación de secret (24h tras rotar) van dos,
* la del secret nuevo primero. Acepta la entrega si CUALQUIERA de las
* firmas coincide con tu secret. Cada firma se valida por separado —
* prefijo, formato y crypto.timingSafeEqual POR FIRMA contra timing
* attacks. Si el prefijo cambia, AVEX cambió de algoritmo y tu
* verificador debe rechazar en vez de pasar en silencio.
*/
export function verifyAvexSignature(rawBody: string, headerValue: string | undefined, secret: string): boolean {
if (!headerValue) return false;
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const expectedBuf = Buffer.from(expected, "hex");
return headerValue.split(",").some((signature) => {
const value = signature.trim();
if (!value.startsWith("sha256=")) return false;
const hex = value.slice("sha256=".length);
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return false;
const receivedBuf = Buffer.from(hex, "hex");
if (expectedBuf.length !== receivedBuf.length) return false;
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
});
}
// Uso en tu handler HTTP (lee el header en minúsculas):
// const ok = verifyAvexSignature(rawBody, req.headers["x-avex-signature"], secret);
// const deliveryId = req.headers["x-avex-delivery-id"]; // dedupe de reintentosCatálogo de eventos salientes
Los 7 eventos que AVEX puede entregar a tus endpoints suscritos, con la semántica exacta de cada uno: cuándo dispara, cuándo NO, y el payload real que vas a recibir. Suscribes cada webhook a un subconjunto de eventos desde el panel; cada entrega llega firmada como describe la sección Webhooks firmados.
| Evento | Cuándo dispara | Cuándo NO dispara |
|---|---|---|
| pass.created | Cada vez que un bloque de creación queda confirmado en base de datos. Un lote se procesa en bloques de hasta 100 pases, así que un lote grande produce VARIAS notificaciones: una por bloque confirmado, cada una con el count y el passes[] de ESE bloque (identificador, carrera y tipo de cada pase), más defaultPassType (el tipo de respaldo del lote). No llega una notificación por pase, ni una sola por lote: si necesitas el total del lote, agrega por institutionId de tu lado. Los bloques se procesan en paralelo, así que las notificaciones de un mismo lote pueden llegar desordenadas. | Bloques que no persistieron ningún pase (fallo de inserción o límite de plan alcanzado), y la creación que corre sin contexto de tenant (procesos programáticos internos). |
Ejemplo de payload{
"institutionId": "6b2f0c1d-…",
"defaultPassType": "student",
"count": 2,
"passes": [
{
"uniqueIdentifier": "1002003001",
"careerId": "9e4a7d21-…",
"passType": "student"
},
{
"uniqueIdentifier": "1002003002",
"careerId": "9e4a7d21-…",
"passType": "alumni"
}
]
} | ||
| pass.updated | Una vez por solicitud de cambio de estado de estudiante, con changeType: "student_status" y los pases agrupados por estado destino en groups[]. Bifurca sobre changeType: emisores futuros añadirán nuevos discriminadores en vez de cambiar la forma existente. | Ningún otro camino de actualización emite hoy: ediciones de campos, reemplazos de foto y renovaciones de período de pago NO disparan este evento. |
Ejemplo de payload{
"institutionId": "6b2f0c1d-…",
"changeType": "student_status",
"count": 2,
"groups": [
{
"studentStatus": "Inactive",
"passes": [
{
"uniqueIdentifier": "1002003001",
"careerId": "9e4a7d21-…"
},
{
"uniqueIdentifier": "1002003002",
"careerId": "9e4a7d21-…"
}
]
}
]
} | ||
| pass.revoked | Una vez por revocación efectiva de un pase. El payload lleva el identificador completo (institución, identificador único, carrera y tipo de pase), la razón y la fecha de revocación. | Revocaciones programáticas sin contexto de tenant (procesos automáticos internos) no emiten. |
Ejemplo de payload{
"institutionId": "6b2f0c1d-…",
"uniqueIdentifier": "1002003001",
"careerId": "9e4a7d21-…",
"passType": "student",
"reason": "Retiro del programa",
"revokedAt": "2026-07-29T15:04:05.000Z"
} | ||
| pass.installed | Por cada señal de instalación en wallet, en las cinco verticales y ambas plataformas. En Apple la señal es el registro PassKit nuevo; en Google es el callback de guardado (autenticado con ECv2 — es una señal de que el wallet guardó el pase, no una prueba de que siga en el dispositivo). resourceRef es el id interno del recurso según la vertical (pase, factura, miembro, boleta o cuota), sin datos personales. Trata la entrega como al-menos-una-vez: deduplica los reintentos con x-avex-delivery-id y, para colapsar instalaciones repetidas del mismo recurso, con la tupla (vertical, resourceRef, platform). | En Apple, un registro repetido del mismo dispositivo sobre un pase ya registrado no re-emite (solo el registro nuevo dispara). En Google NO existe esa garantía: el callback deduplica por nonce, así que un guardado NUEVO del mismo objeto vuelve a emitir — de ahí el dedupe del lado del integrador. Tampoco emiten las señales de un objeto de generación muerta (tras un traspaso o una rotación de identidad) ni las que llegan fuera de orden cuando ya se aplicó una señal más fresca del mismo objeto. |
Ejemplo de payload{
"vertical": "education",
"platform": "apple",
"resourceRef": "c81d4e2e-…",
"institutionId": "6b2f0c1d-…",
"installedAt": "2026-07-29T15:04:05.000Z"
} | ||
| payment.received | Una vez por factura de servicios marcada como pagada a través del webhook entrante firmado. paymentReferenceHash es el hash SHA-256 de la referencia de pago — el texto plano nunca viaja. | Reintentos idempotentes del webhook entrante (la misma confirmación repetida) y confirmaciones concurrentes que pierden la carrera de marcado no emiten. |
Ejemplo de payload{
"billId": "c81d4e2e-…",
"institutionId": "6b2f0c1d-…",
"paidAt": "2026-07-29T15:04:05.000Z",
"paymentReferenceHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
} | ||
| subscription.tier_changed | Una vez por transición real de plan, con previousTier y newTier (nomenclatura simétrica, convención de transiciones de estado). | Actualizaciones de solo estado o de solo límites no emiten — únicamente el cambio efectivo de tier dispara. |
Ejemplo de payload{
"clientId": "d94b1e77-…",
"previousTier": "starter",
"newTier": "growth",
"changedAt": "2026-07-29T15:04:05.000Z"
} | ||
| ticket.validated | Una vez por validación EFECTIVA (la que transiciona la boleta a validada), tanto por escaneo del código en puerta como por entrada manual del validador en el panel. validatedBy llega solo cuando quien valida lo reporta. El barcodeToken (la credencial de escaneo) jamás viaja en el payload. | Re-escaneos idempotentes de una boleta ya usada y rechazos de puerta (boleta cancelada, función fuera de ventana) no emiten. |
Ejemplo de payload{
"ticketId": "f37a9c40-…",
"externalId": "TCK-88412",
"eventId": "0d9b6c3a-…",
"showtimeId": "5a1e8f92-…",
"validatedAt": "2026-07-29T20:15:00.000Z",
"validatedBy": "puerta-2"
} | ||
Entrega: al menos una vez, con reintentos y cola de mensajes muertos
Cada evento se entrega al menos una vez — diseña tu handler idempotente y deduplica con el header x-avex-delivery-id: los reintentos de una misma entrega reutilizan el mismo identificador. Una entrega tiene hasta 5 intentos: el primero dispara de inmediato y, si tu endpoint no responde 2xx, los reintentos esperan 1 min, 5 min, 15 min, 1 h respectivamente. Agotados los intentos, la entrega pasa a la cola de mensajes muertos (DLQ), visible para los operadores del panel — ninguna entrega fallida se descarta en silencio.
El contrato de entrada completo en un solo mapa
Todo endpoint al que tu sistema llama hacia AVEX, en una sola tabla, con su mecanismo de autenticación y el tipo de credencial por fila. Tres familias de auth: API key (Bearer avex_k1_* con scope por vertical) para la ingesta v1, HMAC-SHA256 con un secreto entrante por cliente para los webhooks de pago, y el HMAC Svix del webhook de Resend gestionado por la plataforma.
| Endpoint | Método | Autenticación | Credencial |
|---|---|---|---|
| Ingesta v1 — API key Bearer avex_k1_* + scope por vertical | |||
| /api/v1/pass/upsert | POST | API key · scope passes:update | API key (Bearer avex_k1_*) |
| /api/v1/pass/{externalId}/event | POST | API key · scope passes:update | API key (Bearer avex_k1_*) |
| /api/v1/pass/{externalId}/period/snapshot | POST | API key · scope passes:update | API key (Bearer avex_k1_*) |
| /api/v1/pass/{externalId}/revoke | POST | API key · scope passes:update | API key (Bearer avex_k1_*) |
| /api/v1/loyalty/member/upsert | POST | API key · scope loyalty:write | API key (Bearer avex_k1_*) |
| /api/v1/loyalty/member/{externalId}/snapshot | POST | API key · scope loyalty:write | API key (Bearer avex_k1_*) |
| /api/v1/loyalty/member/{externalId}/revoke | POST | API key · scope loyalty:write | API key (Bearer avex_k1_*) |
| /api/v1/installment/{externalId}/snapshot | POST | API key · scope installments:write | API key (Bearer avex_k1_*) |
| /api/v1/installment/{externalId}/revoke | POST | API key · scope installments:write | API key (Bearer avex_k1_*) |
| /api/v1/installment/upsert | POST | API key · scope installments:write | API key (Bearer avex_k1_*) |
| /api/v1/utility/bill/upsert | POST | API key · scope utilities:write | API key (Bearer avex_k1_*) |
| /api/v1/utility/bill/{paymentReference}/snapshot | POST | API key · scope utilities:write | API key (Bearer avex_k1_*) |
| /api/v1/utility/bill/{paymentReference}/revoke | POST | API key · scope utilities:write | API key (Bearer avex_k1_*) |
| /api/v1/utility/bill/{paymentReference}/install-link | GET | API key · scope utilities:read o utilities:write | API key (Bearer avex_k1_*) |
| /api/v1/entertainment/ticket/upsert | POST | API key · scope events:write | API key (Bearer avex_k1_*) |
| /api/v1/entertainment/ticket/{externalId}/install-link | GET | API key · scope tickets:credential | API key (Bearer avex_k1_*) |
| /api/v1/loyalty/member/{externalId}/install-link | GET | API key · scope loyalty:read o loyalty:write | API key (Bearer avex_k1_*) |
| /api/v1/installment/{externalId}/install-link | GET | API key · scope installments:read o installments:write | API key (Bearer avex_k1_*) |
| /api/v1/entertainment/ticket/{externalId}/snapshot | POST | API key · scope events:write | API key (Bearer avex_k1_*) |
| /api/v1/entertainment/ticket/{externalId}/void | POST | API key · scope events:write | API key (Bearer avex_k1_*) |
| /api/v1/entertainment/validate | POST | API key · scope tickets:validate | API key (Bearer avex_k1_*) |
| /api/v1/verify/{institutionId}/{uniqueIdentifier}/{careerCode} | GET | API key · scope passes:read | API key (Bearer avex_k1_*) |
| /api/v1/verify/utility/{billId} | GET | API key · scope passes:read | API key (Bearer avex_k1_*) |
| /api/v1/verify/loyalty/{memberId} | GET | API key · scope passes:read | API key (Bearer avex_k1_*) |
| /api/v1/verify/real-estate/{installmentId} | GET | API key · scope passes:read | API key (Bearer avex_k1_*) |
| /api/v1/verify/entertainment/{ticketId} | GET | API key · scope passes:read | API key (Bearer avex_k1_*) |
| /api/v1/institutions | GET | API key · scope institutions:read | API key (Bearer avex_k1_*) |
| /api/v1/institutions/{institutionId}/careers | GET | API key · scope careers:read | API key (Bearer avex_k1_*) |
| /api/v1/events | GET | API key · scope events:read | API key (Bearer avex_k1_*) |
| /api/v1/events/{id}/showtimes | GET | API key · scope events:read | API key (Bearer avex_k1_*) |
| /api/v1/loyalty/programs | GET | API key · scope loyalty:read | API key (Bearer avex_k1_*) |
| Webhooks de pago por cliente — HMAC-SHA256, un secreto entrante por cliente | |||
| /api/payments/utility-webhook/{clientSlug} | POST | HMAC secreto por cliente | Secreto HMAC por cliente |
| /api/webhooks/installment-payment/{clientSlug} | POST | HMAC secreto por cliente | Secreto HMAC por cliente |
| /api/webhooks/payment-confirmation/{clientSlug} | POST | HMAC secreto por cliente | Secreto HMAC por cliente |
| /api/payments/loyalty-snapshot/{clientSlug} | POST | HMAC secreto por cliente | Secreto HMAC por cliente |
| Webhook de Resend — HMAC Svix, gestionado por la plataforma | |||
| /api/webhooks/resend | POST | HMAC Svix (svix-signature) | Gestionado por la plataforma |
Esta tabla es documentación de solo lectura. Las credenciales se crean y rotan desde Mi organización (para superadmin: Admin cliente) en el panel de AVEX: las API keys en la pestaña API, y el secreto entrante (uno por cliente, cubre las cuatro URLs por-cliente) en la pestaña Webhook entrante. Los webhooks salientes viven en su propia pestaña Webhooks salientes. No existe una URL de webhook con secreto global: si aún no aprovisionas tu secreto entrante, la misma URL por-cliente acepta temporalmente la firma con el secreto de plataforma. El webhook de Resend lo administra la plataforma.
Qué identificador usa cada endpoint
Regla única: los paths de la ingesta v1 se direccionan SIEMPRE por la clave natural que asignas en tu sistema — nunca por un UUID de AVEX. El UUID interno aparece en un path solo donde lo entrega el propio pase (verify) o donde lo pides explícitamente (discovery).
| Identificador | Dónde viaja | Qué es |
|---|---|---|
| externalId | /api/v1/pass/{externalId}/… · /api/v1/loyalty/member/{externalId}/… · /api/v1/installment/{externalId}/… · /api/v1/entertainment/ticket/{externalId}/… | La clave natural que tú asignas en tu SIS / CRM / ERP / ticketera y envías en el upsert. Es el identificador de snapshot, revoke, void e install-link; los upserts de loyalty, real_estate y entertainment te lo devuelven en la respuesta (education opera por externalId sin ecoarlo; utilities responde por paymentReference). |
| paymentReference | /api/v1/utility/bill/{paymentReference}/… | La clave natural de utilities: la referencia de pago que emite tu facturador. Cumple el mismo papel que el externalId en las otras verticales. |
| UUID interno de AVEX | /api/v1/verify/… · GET de descubrimiento | Solo aquí. En verify el identificador llega dentro del QR del pase; los GET de descubrimiento existen precisamente para resolver los UUIDs que los upserts piden en el CUERPO (institutionId, programId, showtimeId). |
| id (respuesta del upsert) | Cuerpo de la respuesta 200 / 201 | Informativo: correlaciona el registro con el panel de AVEX y con los webhooks salientes. NO es un path param de v1 — para los verbos siguientes usa tu externalId. |
| Webhooks de pago | Cuerpo del POST (el path solo lleva tu clientSlug) | El destinatario viaja en el cuerpo, no en la ruta: paymentReference (utilities), installmentId + paymentReference (real estate), accountNumber (loyalty), referenceId + transactionId (confirmación de pago). |
Firma de los webhooks de pago (HMAC-SHA256 con timestamp)
Envía X-Webhook-Timestamp (epoch en segundos) y firma la cadena `${timestamp}.${rawBody}` con tu secreto entrante; el resultado en hex viaja en X-Webhook-Signature. AVEX rechaza firmas con un desfase mayor a ±5 minutos (anti-replay). El timestamp es obligatorio: una firma del body sin timestamp responde 401.
TS=$(date +%s) SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -X POST "$URL" \ -H "Content-Type: application/json" \ -H "X-Webhook-Timestamp: $TS" \ -H "X-Webhook-Signature: $SIG" \ -d "$BODY"
El body JSON esperado por cada webhook está publicado en la referencia OpenAPI (operaciones de Webhooks entrantes).
Los webhooks de pago son un canal de eventos en tiempo real: un POST por confirmación, limitado a 10 solicitudes por minuto por IP (un 429 incluye Retry-After). Para reconciliación en lote —reprocesos, cierres de mes, cargas históricas— usa los endpoints de snapshot v1 con tu API key: corren en el tier de integración (300 solicitudes por minuto por clave) y aplican el mismo estado sobre el pase.
Tres patrones que ya están en producción
Cada diagrama refleja un flujo que clientes integran hoy. Las cajas en azul AVEX son código nuestro; las cajas neutras son tu sistema o un proveedor externo.
Tu sistema empuja el pase por externalId a POST /api/v1/pass/upsert con Bearer API key e Idempotency-Key: el primer push crea (201), los siguientes reflejan los campos mutables (200) y AVEX entrega la actualización a Apple Wallet y Google Wallet.
La pasarela firma el body con HMAC, AVEX verifica con timingSafeEqual y delega al servicio de install del vertical para activar el pase.
AVEX firma un token corto con HMAC y entrega el enlace por email; tu sistema puede repartirlo por su propio canal con el enlace tokenizado de la API (install-link). La ruta pública de instalación — /install/utility/{billId}/{token} y sus equivalentes por vertical — valida firma y expiry antes de emitir el pase.
Tiers públicos sincronizados con producción
Los números de esta tabla se generan desde la misma configuración que corre en producción — no pueden divergir de los límites que la API realmente aplica. En los endpoints públicos y del panel, cada respuesta 429 incluye el header Retry-After con los segundos restantes; en la ingesta v1 con API key el 429 llega sin ese header — programa el reintento con la ventana del tier.
| Tier | Límite | Ventana | Alcance | Endpoint ejemplo |
|---|---|---|---|---|
| userUsuario autenticado | 100 req | 1 min | Por usuario | GET /api/institution |
| publicEndpoints públicos (lectura) | 30 req | 1 min | Por IP | GET /api/public/installments/{installmentId} |
| public_strictEndpoints públicos (escritura) | 10 req | 1 min | Por IP | POST /api/contact |
| public_installInstalación de pases (descarga) | 60 req | 1 min | Por IP | GET /api/public/loyalty/{memberId}/pass-info/apple |
| strictEndpoints críticos | 5 req | 1 min | Por IP o usuario | POST /api/auth/mfa/enable |
| api_integrationIntegraciones por API key | 300 req | 1 min | Por API key | POST /api/v1/pass/upsert |
Los límites se aplican según la columna Alcance: por IP en los endpoints públicos, por usuario en el panel y por API key en la ingesta v1. Si tu integración requiere un tier más alto, escríbenos al correo del final de la página.
Un solo formato de error para toda la API
Todos los endpoints responden errores con el mismo formato JSON anidado bajo error: el campo error.code es estable y es sobre el que tu código debe decidir; error.message es un mensaje legible en español — no lo parsees — y error.details es opcional y depende del código.
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Demasiadas solicitudes. Intente de nuevo más tarde."
}
}{
"error": {
"code": "VALIDATION_ERROR",
"message": "Datos inválidos",
"details": {
"errors": [],
"properties": {
"data": {
"errors": [],
"properties": {
"email": {
"errors": ["Correo electrónico inválido"]
}
}
}
}
}
}
}Códigos comunes
- HTTP 401
SESSION_INVALIDFalta el header Authorization, o la API key es inválida, expiró o fue revocada.
- HTTP 401
CLIENT_INACTIVELa API key es válida pero el cliente al que pertenece está inactivo o suspendido.
- HTTP 402
SUBSCRIPTION_INACTIVELa suscripción del cliente no está activa. Las rutas operacionales de puerta (verify y validate) quedan exentas; el resto de la API v1 bloquea hasta renovar.
- HTTP 402
SUBSCRIPTION_LIMIT_EXCEEDEDEl plan del cliente quedó sin cupo para el recurso solicitado (pases, usuarios o instituciones). La suscripción sigue activa; libera cupo o amplía el plan para continuar.
- HTTP 400
VALIDATION_ERROREl body no cumple la validación Zod del endpoint. details es el árbol de z.treeifyError: los errores se anidan por campo bajo properties, no llegan como lista plana.
- HTTP 404
RESOURCE_NOT_FOUNDEl recurso no existe o no pertenece al cliente asociado a la API key.
- HTTP 409
CONFLICTLa misma Idempotency-Key está en vuelo (otro intento del mismo push sigue procesándose — reintenta en unos segundos) o su resultado original ya no está disponible (re-push con una clave nueva). Una key completada con resultado no da 409: los upserts v1 responden 200 re-entregando la respuesta original almacenada, verbatim.
- HTTP 429
RATE_LIMIT_EXCEEDEDEl caller excedió el tier aplicable. Los segundos de espera llegan en el header HTTP Retry-After — nunca en el body.
- HTTP 403
INSUFFICIENT_PERMISSIONSLa credencial es válida pero no tiene permisos sobre el recurso o la operación: scope de la API key insuficiente o institución fuera del alcance asignado.
- HTTP 403
FEATURE_DISABLEDLa funcionalidad que requiere el endpoint está deshabilitada para el cliente. Un administrador debe activarla antes de reintentar.
- HTTP 400
PASS_TYPE_INVALIDEl tipo de pase enviado no existe en el catálogo de la institución o usa un código reservado del sistema. Corrige el campo passType del request.
- HTTP 401
WEBHOOK_UNAUTHORIZEDAplica a los receptores de webhooks entrantes (rutas firmadas de pagos y notificaciones), no a la API v1 con API key: la firma del webhook falta, es inválida o ya fue usada.
- HTTP 503
WEBHOOK_NOT_CONFIGUREDAplica a los receptores de webhooks entrantes: la recepción está habilitada pero falta configurar el secreto de verificación del lado de AVEX. El emisor debe reintentar más tarde.
- HTTP 503
SERVICE_UNAVAILABLEEl gate de validación de tickets de entertainment no está disponible (llave TOTP ausente o registro incompleto). El escáner debe reintentar más tarde; el request era válido.
- HTTP 502
EXTERNAL_SERVICE_ERRORUn servicio externo del canal (Apple Wallet, Google Wallet o el proveedor de email) falló al procesar la operación. El request era válido; reintenta con backoff.
- HTTP 500
INTERNAL_ERRORError interno del servidor. El mensaje es genérico a propósito y no trae detalles; reintenta y reporta a soporte si persiste.
- HTTP 501
NOT_IMPLEMENTEDEl vertical del cliente aún no implementa el flujo solicitado (red de seguridad del contrato vertical-sin-flujo). El request era válido pero la operación no existe para ese vertical.
Reintenta sin duplicar efectos
Envía Idempotency-Key en los POST de ingesta para que tus reintentos sean seguros aunque la red falle a medio camino. El efecto corre una sola vez: la repetición de la misma clave dentro de la ventana de 24 horas responde 200 con la respuesta original almacenada, sin ejecutar nada de nuevo.
curl -X POST https://www.avex.com.co/api/v1/pass/upsert \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000001" \
-H "Content-Type: application/json" \
-d '{
"institutionId": "11111111-1111-4111-8111-111111111111",
"externalId": "EST-12345",
"data": {
"uniqueIdentifier": "1098765432",
"careerId": "ING-SIS",
"name": "Ana María Gómez",
"email": "ana.gomez@example.edu.co",
"semester": 6,
"enrollmentYear": 2023,
"studentStatus": "Active",
"academicCalendarLink": null,
"photoUrl": "https://cdn.example.edu.co/fotos/est-12345.jpg"
}
}'Usa un UUID v4
Genera la clave con un UUID v4 único por operación lógica. La misma clave vuelta a enviar dentro de la ventana de 24 horas NO ejecuta el efecto de nuevo.
El replay devuelve la respuesta original
En los upserts, la repetición devuelve el body original almacenado, verbatim y con status 200 (aunque la primera ejecución fuera 201 — el body conserva su created). Si la clave sigue en vuelo, responde 409: reintenta en unos segundos.
Ámbito por cliente y recurso
El espacio de claves se compone por cliente y recurso, no es global: dos API keys del mismo cliente comparten espacio, y tu Idempotency-Key nunca colisiona con la de otro cliente. Aplica solo a POST — el único verbo de escritura de la ingesta v1 hoy; GET lo ignora para no inducir falsa seguridad.
Contrato de estabilidad de la API v1
La API v1 es un contrato: lo publicado no se rompe sin aviso. Estas son las reglas de deprecación (ADR-034) y el registro de cambios de contrato — si está vacío, nada de lo que integraste ha cambiado.
Qué es un breaking change
Quitar o renombrar un endpoint, campo o valor de enum publicado; estrechar la validación de un input existente; cambiar el significado de un status code ya emitido. Añadir endpoints, campos opcionales, campos nuevos de respuesta o valores nuevos de enum NO es breaking: tu cliente debe tolerar campos y valores que no conoce.
Ventana de deprecación: 6 meses
Nada publicado se retira sin al menos 6 meses de convivencia con su reemplazo — dentro de /v1/, o publicando /v2/ y manteniendo /v1/ completo durante la ventana.
Señalización mecánica
Todo lo que entra en deprecación emite los headers Sunset (RFC 8594, con la fecha de retiro) y Deprecation: true, y su operación queda marcada deprecated en la spec OpenAPI pública — monitorea esos headers en tus integraciones.
Registro de cambios de contrato: sin cambios desde el nacimiento del contrato (2026-07-24). Las deprecaciones futuras se listarán aquí con su fecha de retiro.
¿Listo para integrar?
Estos son los tres pasos que normalmente siguen los equipos técnicos cuando han terminado de leer esta documentación.
Solicita tu API key
Escríbenos con el caso de uso y el cliente al que va asociada. Respondemos en horario laboral colombiano.
Escribir a dev@avex.com.coEstado del servicio
Estado agregado del servicio con monitoreo automático y avisos de incidentes publicados por el equipo.
Ver /statusPolítica de datos
Cómo tratamos los datos de tus usuarios finales y qué garantías de seguridad cumplimos antes de procesar el primer pase.
Leer la política