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 OpenAPIEducación — carnets de estudiante
scope: passes:updatecareers:writeRefleja el pase del estudiante desde tu SIS. El orden es carreras → pases: resuelve los careerId con el descubrimiento (GET /api/v1/institutions/{institutionId}/careers) o crea el programa que falte con el POST de carreras (scope careers:write; renombrar/retirar sigue siendo del panel), y emite usando el code. El upsert crea en el primer push y refleja los campos mutables en los siguientes; photoUrl debe ser una URL directa (sin redirects — el fetch server-side corta en 301/302) que sirva JPG/PNG/WEBP; event empuja el cambio de estado; revoke retira el pase. Si la institución tiene captura diferida de foto (photoCaptureInInstallEmail), un create sin foto de un passType que la exige responde además photoCaptureUrl (+ photoCaptureUrlExpiresAt, 30 días): el enlace tokenizado de auto-captura que distribuyes al estudiante. Mientras la foto no llegue, el carnet instalado muestra una silueta neutra — en Apple en el espacio de la foto, en Google como banner superior de la tarjeta; al empujar la photoUrl con el mismo upsert, la foto real la reemplaza y el pase instalado se actualiza solo. studentStatus acepta exactamente Active · Inactive · Graduated · Withdrawn · Expelled · Suspended — con mayúscula inicial; un valor fuera del set responde 400. En el upsert es opcional: si viene se refleja tal cual, si se omite se conserva (un create nace Active); el upsert no emite el webhook pass.updated — para notificar a tus suscriptores el cambio de estado usa event.
curl -X POST https://www.avex.com.co/api/v1/institutions/11111111-1111-4111-8111-111111111111/careers \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Content-Type: application/json" \
-d '{
"code": "ING-IA",
"name": "Ingeniería en Inteligencia Artificial",
"modality": "Virtual",
"totalPeriods": 10
}'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"
}'curl -X POST https://www.avex.com.co/api/v1/pass/EST-12345/period/snapshot \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000005" \
-H "Content-Type: application/json" \
-d '{
"institutionId": "11111111-1111-4111-8111-111111111111",
"periodName": "2026-2",
"startDate": "2026-07-01",
"dueDate": "2026-08-15",
"totalToPay": 2500000,
"amountPaid": 0,
"paymentReference": "REF-2026-2-000123",
"onlinePaymentUrl": "https://pagos.example.com/REF-2026-2-000123"
}'Lealtad — miembros y saldos
scope: loyalty:writeloyalty:readRefleja al miembro y su saldo desde tu POS/CRM. Primero resuelve el programId con el descubrimiento (GET /api/v1/loyalty/programs, scope loyalty:read — paginado con page/limit, responde { data, total, page, limit }); luego el upsert crea o refleja al miembro por externalId y el snapshot empuja el balance ya computado más el nivel resuelto (por su nombre, tierName, o su UUID, tierId) — AVEX nunca calcula puntos. La misma familia trae POST .../member/{externalId}/revoke (retira el pase), GET .../member/{externalId}/install-link (el enlace tokenizado para tu canal) y, si tu POS prefiere push firmado en vez de API key, el webhook entrante /api/payments/loyalty-snapshot/{clientSlug} direccionado por accountNumber.
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
}'Inmobiliario — cuotas
scope: installments:writeEl orden es upsert → enlace de instalación → snapshot o webhook → verificación. El upsert crea la cuota por tu externalId (y la refleja en pushes siguientes: solo los datos del titular; los financieros son del snapshot) y devuelve el installUrl que tú distribuyes; el snapshot empuja el acumulado pagado, el saldo restante y el estado desde tu sistema de cobranza — AVEX los refleja verbatim en el pase — o, si prefieres push firmado, el webhook entrante /api/webhooks/installment-payment/{clientSlug}. La misma familia trae POST .../{externalId}/revoke (409 si la cuota ya tiene pagos), GET .../{externalId}/install-link y GET /api/v1/verify/real-estate/{installmentId} para consultar el estado sin datos personales.
curl -X POST https://www.avex.com.co/api/v1/installment/upsert \
-H "Authorization: Bearer avex_k1_<tu_clave>" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000006" \
-H "Content-Type: application/json" \
-d '{
"externalId": "CUOTA-456",
"institutionId": "77777777-7777-4777-8777-777777777777",
"projectName": "Torres del Parque",
"unitIdentifier": "Torre 2 · Apto 804",
"totalInstallments": 24,
"currentInstallment": 3,
"installmentAmount": 4200000,
"dueDate": "2026-09-15",
"holderName": "Andrés Felipe Cárdenas",
"holderEmail": "andres.cardenas@example.com",
"holderDocument": "1020304050"
}'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"
}'Servicios públicos — 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",
"accountNumber": "1029384821",
"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>"curl -X POST https://www.avex.com.co/api/v1/utility/bill/FAC-2026-07-004821/revoke \
-H "Authorization: Bearer avex_k1_<tu_clave>"Entretenimiento — entradas de evento
scope: events:readevents:writetickets:credentialtickets:validatePrimero resuelve los identificadores con el descubrimiento (scope events:read): GET /api/v1/events (paginado con page/limit) te da el eventId y GET /api/v1/events/{id}/showtimes las funciones con su showtimeId — ambos UUID los exige el upsert. Luego emite entradas por externalId dentro de esa función, entrega la credencial de instalación con install-link (scope tickets:credential) y valida en puerta con el payload real del wallet. La validación es de un solo uso por entrada (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 "https://www.avex.com.co/api/v1/entertainment/ticket/TICKET-2026-00891/install-link?expiryDays=30" \
-H "Authorization: Bearer avex_k1_<tu_clave>"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 la autenticación y la validación de schema, ownership y existencia, resuelve el recurso igual que el push real (una referencia ausente o ajena responde el mismo 404) y, cuando la fila pasaría, devuelve un 200 con la previsualización del efecto sin crear ni modificar datos — ningún pase llega a los titulares. La autenticación cuenta igual que en el push real: consume tu límite de solicitudes y actualiza el último uso de tu API key. Un 4xx bajo dry-run es el mismo 4xx que recibirías en el push real. Dos cosas solo se evalúan al escribir: el aforo por función (entertainment) y la descarga de la foto (education) — un dry-run en verde no las garantiza.
# 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 la entrada; 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. Después comprueba que deliveredAt (firmado, nuevo en cada intento) esté a ±5 minutos de tu reloj y deduplica por x-avex-delivery-id.
# 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);
});
}
/** Tolerancia de reloj para deliveredAt: ±5 minutos. */
export const AVEX_MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
/**
* Frescura: deliveredAt viaja DENTRO del cuerpo firmado y AVEX lo regenera en
* cada intento (reintentos y reenvíos incluidos). Llámala DESPUÉS de verificar
* la firma. Rechaza si deliveredAt falta, no es una fecha válida o está a más de
* ±5 min de tu reloj: un cuerpo re-enviado tal cual por un tercero queda viejo,
* y un rechazo tuyo (no-2xx) hace que AVEX reintente con un deliveredAt nuevo.
*/
export function isFreshAvexDelivery(rawBody: string, now: Date = new Date()): boolean {
let deliveredAt: unknown;
try {
deliveredAt = JSON.parse(rawBody)?.deliveredAt;
} catch {
return false;
}
if (typeof deliveredAt !== "string") return false;
const sentAt = Date.parse(deliveredAt);
if (Number.isNaN(sentAt)) return false;
return Math.abs(now.getTime() - sentAt) <= AVEX_MAX_CLOCK_SKEW_MS;
}
// Uso en tu handler HTTP (lee los headers en minúsculas), en este orden:
// if (!verifyAvexSignature(rawBody, req.headers["x-avex-signature"], secret)) return 401;
// if (!isFreshAvexDelivery(rawBody)) return 400; // AVEX reintenta con deliveredAt nuevo
// const deliveryId = req.headers["x-avex-delivery-id"]; // = deliveryId del cuerpo
// if (await yaProcesada(deliveryId)) return 200; // duplicado: acusa sin re-aplicar
// // procesa el evento y guarda deliveryId (≥30 días: un reenvío desde la DLQ conserva el id)Catá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 | Verticales que lo emiten | Cuándo dispara | Cuándo NO dispara |
|---|---|---|---|
| pass.created | Educación | 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. Las creaciones individuales (upsert de la API v1 que crea, alta individual del panel) emiten una notificación con count: 1 y la misma forma; el create de la API v1 incluye además externalId por pase (la clave natural del integrador — ignora claves que no conozcas). La carga masiva NUNCA emite una notificación por fila, traiga o no la columna externalId: sus creados se agrupan en los mismos bloques. | 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",
"externalId": "SIS-2026-0042"
},
{
"uniqueIdentifier": "1002003002",
"careerId": "9e4a7d21-…",
"passType": "alumni"
}
]
} | |||
| pass.updated | Educación | Una vez por solicitud de cambio de estado de estudiante, con changeType: "student_status" y los pases agrupados por estado destino en groups[]. El emisor de la API v1 (POST /v1/pass/{externalId}/event) incluye además passType y externalId en cada pase (cubre tipos no-student; ignora claves que no conozcas). Bifurca sobre changeType: emisores futuros añadirán nuevos discriminadores en vez de cambiar la forma existente. Un cambio masivo desde el panel se fragmenta en bloques de hasta 500 pases: cada bloque es un evento propio con count = total del lote y un campo adicional batch { id, index, total, passCount } para reensamblar (ninguna fila se descarta); el emisor de la API v1 no lo incluye. | 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-…",
"passType": "student",
"externalId": "SIS-2026-0042"
},
{
"uniqueIdentifier": "1002003002",
"careerId": "9e4a7d21-…"
}
]
}
]
} | |||
| pass.revoked | Educación | 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 | Todas | 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 la referencia de PLATAFORMA que emitió la señal (en education el serial de Apple o el objectId de Google; en las otras cuatro el id interno del recurso), así que NO colapsa Apple con Google para el mismo recurso: para correlacionar usa externalId, que es la clave natural que tu propio sistema envió — external_id en education, membresías, cuotas y entradas; paymentReference o accountReference en servicios. entityKind dice qué entidad designan ambas referencias (pass, bill, account, member, ticket o installment) y es lo que distingue, en servicios, una factura de un contrato. Ninguna de las dos lleva datos personales: el identificador único de education (con frecuencia un documento de identidad) nunca viaja en este evento. Trata la entrega como al-menos-una-vez: deduplica los reintentos con x-avex-delivery-id y, para colapsar instalaciones repetidas del mismo recurso en ambas plataformas, con la tupla (vertical, institutionId, entityKind, externalId): externalId solo es único por institución en education, y en servicios entityKind separa una factura de un contrato con la misma referencia. Sin externalId no hay clave natural: deduplica solo por x-avex-delivery-id (resourceRef no colapsa Apple con Google). | 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. Las dos claves nuevas viajan AUSENTES —jamás en null— pero no juntas: externalId falta cuando el recurso se creó desde el panel de AVEX y no tiene clave tuya, mientras entityKind sigue llegando (AVEX sabe qué entidad es, aunque no tengas clave para ella); las dos faltan solo cuando el recurso ya no existe al momento de la señal o la instalación no trae referencia de entidad. |
Ejemplo de payload{
"vertical": "education",
"platform": "apple",
"resourceRef": "c81d4e2e-…",
"externalId": "SIS-1001",
"entityKind": "pass",
"institutionId": "6b2f0c1d-…",
"installedAt": "2026-07-29T15:04:05.000Z"
} | |||
| payment.received | Servicios públicos | 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 | Todas | 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. El alta inicial del trial tampoco emite (no hay plan anterior); iniciar un trial en un plan distinto del vigente sí emite. |
Ejemplo de payload{
"clientId": "d94b1e77-…",
"previousTier": "starter",
"newTier": "growth",
"changedAt": "2026-07-29T15:04:05.000Z"
} | |||
| ticket.validated | Entretenimiento | Una vez por validación EFECTIVA (la que transiciona la entrada 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 entrada ya usada y rechazos de puerta (entrada anulada, 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}/academic | PATCH | API key · scope passes:update | API key (Bearer avex_k1_*) |
| /api/v1/pass/{externalId}/transfer-career | 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 · obsoleto (solo pases legacy) | 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/institutions/{institutionId}/careers | POST | API key · scope careers:write | 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) en la pestaña Webhook entrante. Cada webhook de pago pertenece a un vertical: la URL vigente para tu cliente es la de su vertical — el panel la muestra — y una entrega dirigida al receptor de otro vertical responde 404. 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 — con una excepción: el webhook de snapshot loyalty exige el secreto por cliente desde su nacimiento (sin secreto responde 401). 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 / sistema de venta de entradas 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 o externalId —exactamente uno— + 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. Una diferencia a tener en cuenta: el evento saliente payment.received lo emite SOLO el webhook entrante firmado de utilities — el snapshot v1 aplica el mismo estado pero no dispara ese evento a tus webhooks salientes.
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. Todo 429 emitido por el rate limiter de esta tabla incluye el header Retry-After con los segundos restantes — también en la ingesta v1 con API key. La diferencia entre caminos está en las cabeceras informativas X-RateLimit-*: en el panel y los endpoints públicos acompañan cada respuesta; en la ingesta v1 viajan solo con el 429. Algunos topes de negocio (p. ej. el límite diario de envíos de foto) también responden 429 pero SIN Retry-After — no dependas del header para distinguirlos: usa el contexto del endpoint.
| Tier | Límite | Ventana | Alcance | Endpoint ejemplo |
|---|---|---|---|---|
| userUsuario autenticado | 300 req | 1 min | Por usuario | GET /api/institution |
| publicEndpoints públicos (lectura) | 30 req | 1 min | Por IP | GET /api/docs/openapi.json |
| 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/events/{id}/duplicate |
| heavy_notificationNotificaciones masivas | 20 req | 1 h | Por usuario o API key | POST /api/institution/{institutionId}/pass/notifications |
| 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. Ojo: este mismo code también llega con status 409 en el conflicto de catálogo del POST de carreras (código ya existente) — discrimina por status, no solo por code.
- HTTP 404
RESOURCE_NOT_FOUNDEl recurso no existe o no pertenece al cliente asociado a la API key.
- HTTP 404
IMAGE_SERVICEEl fetch server-side de una imagen falló (photoUrl del upsert education): URL con redirect, host inaccesible o que filtra el User-Agent, tamaño mayor a 20 MB, timeout de 30 s, o bytes que no son una imagen decodificable. Distinto de RESOURCE_NOT_FOUND: el pase/carrera existen — la que falló fue la foto. Una falla INTERNA al procesar la foto nunca llega con este code: responde 500 INTERNAL_ERROR; y una caída de nuestro almacenamiento responde 502 EXTERNAL_SERVICE_ERROR — en ambos casos reintenta más tarde, no es tu URL.
- 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 409
CONFIRMATION_REQUIREDLa operación exige consentimiento explícito antes de proceder. En los envíos de notificación (pass/notifications, reminders/csv): la audiencia completa es Google Wallet con la cuota de notificaciones visibles (24 h) agotada y sin destinatarios Apple — nadie vería el aviso. Reintenta la MISMA request con acknowledgeNoPush=true para entregar la actualización sin notificación visible, o espera a que la ventana de cuota se libere.
- 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 400
CUSTOM_FIELD_KEY_RESERVEDUna clave de customFields (upsert o snapshot de utilities/installment/loyalty/entertainment) colisiona con un campo del motor de campos del vertical — sombrearía el resolver o produciría un render duplicado. Renombra la clave; las reservadas varían por vertical.
- 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 entradas 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, el proveedor de email o nuestro almacenamiento de imágenes) 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 y congela el body
Genera la clave con un UUID v4 único por operación lógica. La misma clave, reenviada con el MISMO body dentro de la ventana de 24 horas, NO ejecuta el efecto de nuevo. La clave efectiva incluye el hash del payload: reintenta siempre con los mismos bytes — la misma Idempotency-Key con un body distinto se procesa como una operación nueva.
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 credencial y recurso
El espacio de claves se compone por recurso y por credencial, no es global: la misma Idempotency-Key enviada con OTRA API key es una entrada distinta (no hay deduplicación cross-key — no la esperes al rotar credenciales), y 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.
Carreras: idempotencia por clave natural
El POST de carreras no usa Idempotency-Key (enviarla no tiene efecto): su reintento seguro es el contrato mismo — el code es la clave natural del catálogo, así que repetir el POST responde 409 «ya existía» y tu sync sigue de largo. Para sondear sin crear, usa X-AVEX-Dry-Run.
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 — hasta hoy cambios aditivos y correctivos, ningún retiro.
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: el contrato nació el 2026-07-24. Las deprecaciones futuras se listarán aquí con su fecha de retiro; los cambios aditivos (nunca breaking) y los correctivos (la respuesta pasa a ser la que el contrato ya prescribía) quedan registrados abajo.
2026-09-26 — Correctivo (#3115): la tupla recomendada el 2026-09-22 para deduplicar pass.installed, (vertical, externalId), no es única dentro de tu cuenta — en education el externalId es único por institución, y en servicios una factura y un contrato pueden compartir referencia. Usa (vertical, institutionId, entityKind, externalId); todos esos campos ya viajan en el evento. Sin externalId no hay clave natural: deduplica solo por x-avex-delivery-id. El payload no cambia.
2026-09-22 — Aditivo (#2585): el evento pass.installed suma dos campos opcionales — externalId (la clave natural que tu propio sistema envió: external_id en education, membresías, cuotas y entradas; paymentReference o accountReference en servicios) y entityKind (pass, bill, account, member, ticket o installment). resourceRef no cambia. Como resourceRef es la referencia de plataforma, no colapsa Apple con Google del mismo recurso: para deduplicar instalaciones usa ahora la tupla (vertical, externalId). Los campos llegan ausentes, nunca en null, y no siempre juntos: si el recurso nació en el panel de AVEX y no tiene clave tuya falta solo externalId y entityKind sigue llegando; ambos faltan solo si el recurso ya no existe al momento de la señal.
2026-09-05 — Aditivo (#1898): en event, revoke y period/snapshot de education, si el pase cambia de carrera dos veces durante la misma llamada la respuesta es 409 reintentable; una transferencia concurrente simple se absorbe re-resolviendo el externalId al pase vigente. En el upsert (HIT) una sola transferencia concurrente ya responde 409 sin escribir: reconciliá el careerId del payload con la carrera nueva y reenviá.
2026-09-05 — Correctivo (#1882): la transferencia de carrera desde el panel mueve el externalId al pase destino. Antes /v1/pass/{externalId} seguía resolviendo al pase revocado — un push con el careerId viejo respondía 200 y con el nuevo 409; ahora es al revés, como la identidad inmutable ya prescribía. Sin efecto para pases nunca transferidos.
2026-09-03 — Aditivo (ADR-034): valor nuevo del enum modality de carreras — «Presencial / Virtual» — en crear/actualizar carrera y en el discovery de carreras.
2026-08-30 — Aditivo (ADR-034): customFields opcional en upsert+snapshot de utilities/installment/loyalty/entertainment.
¿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. Si tu organización ya tiene cuenta AVEX, un administrador la mintea directo en el panel: Mi organización → API (máximo 3 keys activas, rotación atómica).
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 los titulares de tus pases y qué garantías de seguridad cumplimos antes de procesar el primer pase.
Leer la política