Saltar al contenido
API & Webhooks

Integra AVEX en tus flujos

Documentación mínima para integradores. Bearer tokens con alcance por cliente, webhooks firmados HMAC-SHA256 y tiers de rate-limit publicados — los mismos números que corren en producción.

01 — Autenticación

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.

cURLauth-header.sh
curl https://www.avex.com.co/api/v1/institutions \
  -H "Authorization: Bearer avex_k1_<tu_clave>"
GET /api/v1/institutions con el header Authorization. Reemplaza avex_k1_<tu_clave> con la API key real que generas en tu panel; los ejemplos completos por vertical están en la sección de ingesta.

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:

Enforcement de suscripción por transporte — comportamiento con suscripción bloqueada
TransporteAutenticaciónCon suscripción bloqueada
Webhooks de pago entrantes/api/payments/* · /api/webhooks/*HMAC por clienteSiguen 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 keyResponde 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/validateAPI keyExenta — un lector de puerta no se detiene a mitad de evento por un trial vencido.
02 — Endpoints de ingesta

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 OpenAPI

Education — pases de estudiante

scope: passes:update

Refleja 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.

cURLpass-upsert.sh
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"
  }
}'
POST /api/v1/pass/upsert — crea o refleja un pase. Idempotency-Key opcional para reintentos seguros.
TypeScriptpass-upsert.ts
// 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}`);
}
Mismo upsert desde Node 22 con fetch nativo. crypto.randomUUID() genera la Idempotency-Key.
cURLpass-event.sh
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"
  }
}'
POST /api/v1/pass/{externalId}/event — empuja el cambio de estado (tu SIS es la autoridad).
cURLpass-revoke.sh
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"
}'
POST /api/v1/pass/{externalId}/revoke — retira el pase con una razón auditable.

Loyalty — miembros y saldos

scope: loyalty:write

Refleja 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.

cURLloyalty-upsert.sh
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"
  }
}'
POST /api/v1/loyalty/member/upsert — crea o refleja un miembro por su externalId.
cURLloyalty-snapshot.sh
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
}'
POST /api/v1/loyalty/member/{externalId}/snapshot — empuja el balance entero ya calculado. tierId null limpia el nivel.

Real estate — cuotas

scope: installments:write

Refleja 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.

cURLinstallment-snapshot.sh
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"
}'
POST /api/v1/installment/{externalId}/snapshot — status ∈ Active | Defaulted | Completed.

Utilities — facturas de servicios y conjuntos residenciales

scope: utilities:write

Refleja 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.

cURLutility-upsert.sh
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"
}'
POST /api/v1/utility/bill/upsert — crea o refleja la factura por paymentReference. accountReference (opcional) activa el pase persistente por cuenta.
cURLutility-snapshot.sh
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"
}'
POST /api/v1/utility/bill/{paymentReference}/snapshot — paymentStatus ∈ pending | paid | overdue | cancelled; overdue solo si tu sistema lo empuja.
cURLutility-install-link.sh
curl https://www.avex.com.co/api/v1/utility/bill/FAC-2026-07-004821/install-link \
  -H "Authorization: Bearer avex_k1_<tu_clave>"
GET /api/v1/utility/bill/{paymentReference}/install-link — el enlace tokenizado para imprimir el QR en el recibo físico.

Entertainment — boletas de evento

scope: events:write · tickets:validate

Emite 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.

cURLentertainment-upsert.sh
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"
}'
POST /api/v1/entertainment/ticket/upsert — crea la boleta por externalId; seat/row/section opcionales. tierId (opcional, A38) asigna la tarifa AL ACUÑAR — un HIT existente lo ignora.
cURLentertainment-validate.sh
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"
}'
POST /api/v1/entertainment/validate — qrPayload es el valor crudo del QR (token, o token|totp con barcode rotatorio).

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.

cURLdry-run.sh
# 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"}'
El mismo body que el endpoint real; solo cambia el header X-AVEX-Dry-Run.
03 — Webhooks

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.

cURLdebug-signature.sh
# 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"
Reproducción local de la firma para depurar — usa exactamente la misma fórmula que el backend.
TypeScriptverify-webhook.ts
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 reintentos
Implementación de referencia multi-firma: split por coma + some, aceptando si cualquier firma coincide. Mismo algoritmo que corre en producción dentro de AVEX.
04 — Eventos

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.

Catálogo de eventos webhook salientes — cuándo dispara y cuándo no cada evento, con payload de ejemplo
EventoCuándo disparaCuándo NO dispara
pass.createdCada 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.updatedUna 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.revokedUna 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.installedPor 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.receivedUna 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_changedUna 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.validatedUna 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.

05 — Endpoints de entrada

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.

Inventario de endpoints de entrada — método, autenticación y credencial por ruta
EndpointMétodoAutenticaciónCredencial
Ingesta v1 — API key Bearer avex_k1_* + scope por vertical
/api/v1/pass/upsertPOSTAPI key · scope passes:updateAPI key (Bearer avex_k1_*)
/api/v1/pass/{externalId}/eventPOSTAPI key · scope passes:updateAPI key (Bearer avex_k1_*)
/api/v1/pass/{externalId}/period/snapshotPOSTAPI key · scope passes:updateAPI key (Bearer avex_k1_*)
/api/v1/pass/{externalId}/revokePOSTAPI key · scope passes:updateAPI key (Bearer avex_k1_*)
/api/v1/loyalty/member/upsertPOSTAPI key · scope loyalty:writeAPI key (Bearer avex_k1_*)
/api/v1/loyalty/member/{externalId}/snapshotPOSTAPI key · scope loyalty:writeAPI key (Bearer avex_k1_*)
/api/v1/loyalty/member/{externalId}/revokePOSTAPI key · scope loyalty:writeAPI key (Bearer avex_k1_*)
/api/v1/installment/{externalId}/snapshotPOSTAPI key · scope installments:writeAPI key (Bearer avex_k1_*)
/api/v1/installment/{externalId}/revokePOSTAPI key · scope installments:writeAPI key (Bearer avex_k1_*)
/api/v1/installment/upsertPOSTAPI key · scope installments:writeAPI key (Bearer avex_k1_*)
/api/v1/utility/bill/upsertPOSTAPI key · scope utilities:writeAPI key (Bearer avex_k1_*)
/api/v1/utility/bill/{paymentReference}/snapshotPOSTAPI key · scope utilities:writeAPI key (Bearer avex_k1_*)
/api/v1/utility/bill/{paymentReference}/revokePOSTAPI key · scope utilities:writeAPI key (Bearer avex_k1_*)
/api/v1/utility/bill/{paymentReference}/install-linkGETAPI key · scope utilities:read o utilities:writeAPI key (Bearer avex_k1_*)
/api/v1/entertainment/ticket/upsertPOSTAPI key · scope events:writeAPI key (Bearer avex_k1_*)
/api/v1/entertainment/ticket/{externalId}/install-linkGETAPI key · scope tickets:credentialAPI key (Bearer avex_k1_*)
/api/v1/loyalty/member/{externalId}/install-linkGETAPI key · scope loyalty:read o loyalty:writeAPI key (Bearer avex_k1_*)
/api/v1/installment/{externalId}/install-linkGETAPI key · scope installments:read o installments:writeAPI key (Bearer avex_k1_*)
/api/v1/entertainment/ticket/{externalId}/snapshotPOSTAPI key · scope events:writeAPI key (Bearer avex_k1_*)
/api/v1/entertainment/ticket/{externalId}/voidPOSTAPI key · scope events:writeAPI key (Bearer avex_k1_*)
/api/v1/entertainment/validatePOSTAPI key · scope tickets:validateAPI key (Bearer avex_k1_*)
/api/v1/verify/{institutionId}/{uniqueIdentifier}/{careerCode}GETAPI key · scope passes:readAPI key (Bearer avex_k1_*)
/api/v1/verify/utility/{billId}GETAPI key · scope passes:readAPI key (Bearer avex_k1_*)
/api/v1/verify/loyalty/{memberId}GETAPI key · scope passes:readAPI key (Bearer avex_k1_*)
/api/v1/verify/real-estate/{installmentId}GETAPI key · scope passes:readAPI key (Bearer avex_k1_*)
/api/v1/verify/entertainment/{ticketId}GETAPI key · scope passes:readAPI key (Bearer avex_k1_*)
/api/v1/institutionsGETAPI key · scope institutions:readAPI key (Bearer avex_k1_*)
/api/v1/institutions/{institutionId}/careersGETAPI key · scope careers:readAPI key (Bearer avex_k1_*)
/api/v1/eventsGETAPI key · scope events:readAPI key (Bearer avex_k1_*)
/api/v1/events/{id}/showtimesGETAPI key · scope events:readAPI key (Bearer avex_k1_*)
/api/v1/loyalty/programsGETAPI key · scope loyalty:readAPI key (Bearer avex_k1_*)
Webhooks de pago por cliente — HMAC-SHA256, un secreto entrante por cliente
/api/payments/utility-webhook/{clientSlug}POSTHMAC secreto por clienteSecreto HMAC por cliente
/api/webhooks/installment-payment/{clientSlug}POSTHMAC secreto por clienteSecreto HMAC por cliente
/api/webhooks/payment-confirmation/{clientSlug}POSTHMAC secreto por clienteSecreto HMAC por cliente
/api/payments/loyalty-snapshot/{clientSlug}POSTHMAC secreto por clienteSecreto HMAC por cliente
Webhook de Resend — HMAC Svix, gestionado por la plataforma
/api/webhooks/resendPOSTHMAC 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).

Mapa de identidad — qué identificador viaja en cada superficie de la API
IdentificadorDónde viajaQué 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 descubrimientoSolo 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 / 201Informativo: 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 pagoCuerpo 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.

06 — Flujos

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.

Ingesta v1 desde tu SIS o ERP

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.

Confirmación de pago

La pasarela firma el body con HMAC, AVEX verifica con timingSafeEqual y delega al servicio de install del vertical para activar el pase.

Install token

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.

07 — Rate limits

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.

Rate limits por tier — AVEX API pública
TierLímiteVentanaAlcanceEndpoint ejemplo
userUsuario autenticado100 req1 minPor usuarioGET /api/institution
publicEndpoints públicos (lectura)30 req1 minPor IPGET /api/public/installments/{installmentId}
public_strictEndpoints públicos (escritura)10 req1 minPor IPPOST /api/contact
public_installInstalación de pases (descarga)60 req1 minPor IPGET /api/public/loyalty/{memberId}/pass-info/apple
strictEndpoints críticos5 req1 minPor IP o usuarioPOST /api/auth/mfa/enable
api_integrationIntegraciones por API key300 req1 minPor API keyPOST /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.

08 — Errores

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.

JSONerror-response.json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Demasiadas solicitudes. Intente de nuevo más tarde."
  }
}
Ejemplo de respuesta 429. Los segundos de espera viajan en el header HTTP Retry-After, no en el body.
JSONvalidation-error.json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Datos inválidos",
    "details": {
      "errors": [],
      "properties": {
        "data": {
          "errors": [],
          "properties": {
            "email": {
              "errors": ["Correo electrónico inválido"]
            }
          }
        }
      }
    }
  }
}
Ejemplo de respuesta 400. details es el árbol de z.treeifyError(error): cada campo anida sus errores bajo properties.

Códigos comunes

  • HTTP 401SESSION_INVALID

    Falta el header Authorization, o la API key es inválida, expiró o fue revocada.

  • HTTP 401CLIENT_INACTIVE

    La API key es válida pero el cliente al que pertenece está inactivo o suspendido.

  • HTTP 402SUBSCRIPTION_INACTIVE

    La 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 402SUBSCRIPTION_LIMIT_EXCEEDED

    El 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 400VALIDATION_ERROR

    El 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 404RESOURCE_NOT_FOUND

    El recurso no existe o no pertenece al cliente asociado a la API key.

  • HTTP 409CONFLICT

    La 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 429RATE_LIMIT_EXCEEDED

    El caller excedió el tier aplicable. Los segundos de espera llegan en el header HTTP Retry-After — nunca en el body.

  • HTTP 403INSUFFICIENT_PERMISSIONS

    La 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 403FEATURE_DISABLED

    La funcionalidad que requiere el endpoint está deshabilitada para el cliente. Un administrador debe activarla antes de reintentar.

  • HTTP 400PASS_TYPE_INVALID

    El 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 401WEBHOOK_UNAUTHORIZED

    Aplica 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 503WEBHOOK_NOT_CONFIGURED

    Aplica 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 503SERVICE_UNAVAILABLE

    El 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 502EXTERNAL_SERVICE_ERROR

    Un 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 500INTERNAL_ERROR

    Error interno del servidor. El mensaje es genérico a propósito y no trae detalles; reintenta y reporta a soporte si persiste.

  • HTTP 501NOT_IMPLEMENTED

    El 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.

09 — Idempotencia

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.

cURLcreate-pass.sh
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"
  }
}'
Misma clave = el efecto corre una sola vez; el replay devuelve la respuesta original con status 200. Reemplaza el UUID por uno generado por tu cliente.
  • 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.

10 — Estabilidad

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.

11 — Próximos pasos

¿Listo para integrar?

Estos son los tres pasos que normalmente siguen los equipos técnicos cuando han terminado de leer esta documentación.