API y webhooks
Conecta tu sistema de inscripciones, tu plataforma académica o tu herramienta de certificados con UPass. Consulta resultados de asistencia, carga participantes y recibe avisos automáticos cuando algo ocurre.
Inicio rápido
- Un administrador de la organización crea una clave en Administración, Integraciones. Elige Solo lectura si solo vas a consultar, o Lectura y escritura si vas a cargar participantes. La clave se muestra una sola vez.
Prueba la conexión: la respuesta indica la organización y el permiso de la clave.
bashcurl https://upass.day/api/v1/yo \ -H "Authorization: Bearer $CLAVE_API"json{ "datos": { "organizacion": { "id": "cm4...", "nombre": "Mi organización" }, "permiso": "ESCRITURA" } }- Lista los eventos con
GET /api/v1/eventosy las salas de cada uno conGET /api/v1/eventos/{eventoId}/salas. Con esos identificadores ya puedes cargar participantes y consultar resultados.
Autenticación y límites
Envía la clave en la cabecera Authorization: Bearer ak_... en cada solicitud. No la pongas en la URL.
La clave da acceso a todos los eventos de la organización: úsala solo desde tu servidor, nunca en una página web, una app móvil o un repositorio de código. La API no acepta solicitudes desde navegadores.
Crea una clave por sistema que se conecta. Si una se filtra, revócala desde el panel: deja de funcionar en el acto y los demás sistemas siguen operando.
Cada clave admite 120 solicitudes por minuto. Al superarlo, la respuesta es 429 con la cabecera Retry-After: espera esos segundos y reintenta.
La API y los webhooks están incluidos en todos los planes.
Formato, paginación y errores
Todo va en JSON. Una respuesta correcta trae los datos en datos; las listas agregan paginacion.
Los instantes van en UTC con formato ISO 8601 (2026-10-14T13:05:00.000Z). Las fechas de evento y de jornada van como AAAA-MM-DD en la zona horaria del evento (zonaHoraria).
Las listas se paginan con pagina y porPagina (50 por omisión, máximo 200). Recorre hasta totalPaginas.
La versión va en la ruta (/api/v1). Dentro de una versión solo agregamos campos: ignora los que tu sistema no conozca.
| HTTP | codigo | Cuándo |
|---|---|---|
| 401 | no_autenticado | Falta la clave, no existe o fue revocada |
| 403 | sin_permiso | Escritura con una clave de solo lectura |
| 404 | no_encontrado | El recurso no existe o es de otra organización |
| 422 | datos_invalidos | Datos inválidos; detalles trae los mensajes por campo |
| 422 | regla_de_negocio | Una regla lo impide, por ejemplo, el límite de participantes del plan |
| 413 | cuerpo_demasiado_grande | El cuerpo de la solicitud supera 1 MB |
| 429 | limite_excedido | Se superó el límite de solicitudes por minuto, o hubo demasiados intentos con una clave inválida desde la misma dirección |
| 500 | error_interno | Error inesperado; ya se avisó al equipo, reintenta más tarde |
Rutas de la API
Todas las rutas empiezan con https://upass.day/api/v1. La especificación OpenAPI completa se puede importar en Postman o usar para generar un cliente.
| Método | Ruta | Permiso | Descripción |
|---|---|---|---|
| GET | /yo | Lectura | Organización y permiso de la clave |
| GET | /eventos | Lectura | Eventos de la organización |
| GET | /eventos/{eventoId} | Lectura | Un evento |
| GET | /eventos/{eventoId}/salas | Lectura | Salas con sus jornadas |
| GET | /eventos/{eventoId}/participantes | Lectura | Participantes; filtros identificacion, salaId y activa |
| POST | /eventos/{eventoId}/participantes | Escritura | Crea o actualiza un participante |
| POST | /eventos/{eventoId}/participantes/lote | Escritura | Crea o actualiza hasta 500 participantes |
| GET | /eventos/{eventoId}/participantes/{identificacion} | Lectura | Un participante por su identificación |
| DELETE | /eventos/{eventoId}/participantes/{identificacion} | Escritura | Retira la inscripción |
| GET | /eventos/{eventoId}/asistencia | Lectura | Asistencia calculada; filtros identificacion, salaId y estado |
| GET | /eventos/{eventoId}/marcaciones | Lectura | Marcaciones para sincronizar; filtro actualizadasDesde |
Cargar participantes
La identificación es la clave: enviar dos veces el mismo participante no lo duplica. La primera vez responde 201 y creado; las siguientes, 200 y actualizado. Si estaba retirado, se reactiva.
curl -X POST https://upass.day/api/v1/eventos/$EVENTO_ID/participantes \
-H "Authorization: Bearer $CLAVE_API" \
-H "Content-Type: application/json" \
-d '{
"identificacion": "1710034065",
"nombres": "Ana",
"apellidos": "Pérez",
"correo": "[email protected]",
"salaId": "cm4sala...",
"modalidad": "PRESENCIAL"
}'HTTP/1.1 201 Created
{ "datos": { "identificacion": "1710034065", "resultado": "creado" } }Para cargar muchos, usa el lote: hasta 500 por solicitud. Se valida todo antes de guardar: si uno tiene un error, no se guarda ninguno y detalles indica la posición y el campo.
POST /api/v1/eventos/{eventoId}/participantes/lote
{
"participantes": [
{ "identificacion": "1710034065", "nombres": "Ana", "apellidos": "Pérez",
"correo": "[email protected]", "salaId": "cm4sala...", "modalidad": "PRESENCIAL" },
{ "identificacion": "PA123456", "nombres": "John", "apellidos": "Smith",
"correo": "[email protected]", "salaId": "cm4sala...", "modalidad": "VIRTUAL" }
]
}HTTP/1.1 422 Unprocessable Entity
{
"error": {
"codigo": "datos_invalidos",
"mensaje": "Revisa los datos ingresados",
"detalles": { "participantes.1.correo": ["Correo inválido"] }
}
}Se aplican las mismas reglas que en la importación desde Excel: validación de la identificación según el país del evento, límite de participantes del plan y, si el evento lo tiene activado, envío de la credencial por correo.
DELETE retira la inscripción sin borrarla: su asistencia se conserva y repetirlo no cambia nada.
Consultar resultados
La asistencia se calcula en el momento, con las mismas reglas del panel y del reporte. alcanzaMinimo indica si la persona cumple el porcentaje mínimo para certificar; estado puede ser ALCANZADO, EN_CAMINO o NO_ALCANZA.
curl "https://upass.day/api/v1/eventos/$EVENTO_ID/asistencia?estado=ALCANZADO&pagina=1" \
-H "Authorization: Bearer $CLAVE_API"{
"evento": { "id": "cm4...", "modoCalculo": "MARCACIONES", "porcentajeMinimo": 80 },
"datos": [
{
"participante": {
"inscripcionId": "cm4ins...", "identificacion": "1710034065",
"nombres": "Ana", "apellidos": "Pérez", "correo": "[email protected]",
"salaId": "cm4sala...", "sala": "Auditorio", "modalidad": "PRESENCIAL",
"activa": true, "inscritoEn": "2026-10-01T15:00:00.000Z", "primerAccesoEn": null,
"telefono": null
},
"asistencia": {
"logrado": 11, "total": 12, "porcentaje": 91.7,
"estado": "ALCANZADO", "alcanzaMinimo": true, "necesario": 9.6
}
}
],
"paginacion": { "pagina": 1, "porPagina": 50, "total": 1, "totalPaginas": 1 }
}Para mantener tu sistema al día con las marcaciones, guarda la mayor actualizadaEn recibida y pide solo lo que cambió desde entonces. Ese instante se incluye, así que puede repetirse alguna marcación: identifícalas por id. Una marcación anulada vuelve a aparecer con anulada: true.
# Primera vez: todas las marcaciones del evento
curl "https://upass.day/api/v1/eventos/$EVENTO_ID/marcaciones?porPagina=200&pagina=1" \
-H "Authorization: Bearer $CLAVE_API"
# Después: solo lo que cambió desde la mayor actualizadaEn que guardaste
curl "https://upass.day/api/v1/eventos/$EVENTO_ID/marcaciones?actualizadasDesde=2026-10-14T15:20:00.000Z" \
-H "Authorization: Bearer $CLAVE_API"Webhooks
Un webhook es una URL de tu sistema a la que avisamos cuando ocurre algo, sin que tengas que preguntar. Se configuran en Administración, Integraciones: URL https pública, eventos que te interesan y un secreto de firma que se muestra una sola vez.
| Evento | Cuándo se envía |
|---|---|
| participante.inscrito | Se inscribió un participante: alta en el panel, importación, API o reactivación. |
| participante.retirado | Se retiró o eliminó una inscripción. |
| marcacion.registrada | Se registró una marcación por cualquier método. En eventos grandes son muchos avisos. |
| marcacion.anulada | Se anuló una marcación por corrección. |
| asistencia.minimo_alcanzado | Un participante alcanzó el mínimo y ya no puede bajar de él. Puede demorar unos minutos. |
| evento.finalizado | Terminó la última jornada del evento. Trae el resumen y el enlace a los resultados completos. Puede demorar unos minutos. |
Cada aviso es un POST con un cuerpo JSON y las cabeceras X-Evento, X-Entrega y X-Firma. Responde con un código 2xx en menos de 10 segundos; si tu proceso tarda más, guarda el aviso y procésalo después.
POST https://tu-sistema.com/webhooks
Content-Type: application/json
X-Evento: evento.finalizado
X-Entrega: cm4ent...
X-Firma: t=1792000000,v1=5f2b...
{
"id": "cm4ent...",
"evento": "evento.finalizado",
"creadoEn": "2026-10-16T23:05:00.000Z",
"datos": {
"evento": { "id": "cm4...", "nombre": "Congreso de Salud", "zonaHoraria": "America/Guayaquil" },
"resumen": { "participantes": 1450, "alcanzaron": 1312, "noAlcanzaron": 138 },
"resultados": { "url": "https://.../api/v1/eventos/cm4.../asistencia" }
}
}Si tu sistema no responde 2xx, reintentamos tras 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas, 12 horas y 24 horas. Después, la entrega queda fallida y se puede reintentar desde el historial del panel. Tras 100 intentos fallidos seguidos, el webhook se desactiva solo.
Un mismo aviso puede llegar más de una vez y no siempre en orden: descarta los repetidos por id y usa creadoEn o la API como referencia.
Con el botón Enviar prueba del panel recibes un aviso webhook.prueba y ves en el momento qué respondió tu sistema.
Verificar la firma
La cabecera X-Firma tiene la forma t=marca,v1=firma. La firma es el HMAC-SHA256, en hexadecimal, del texto marca.cuerpo con el secreto del webhook.
Calcula la firma sobre el cuerpo crudo, tal como llegó (antes de convertirlo a JSON), compárala en tiempo constante y rechaza las marcas de más de 5 minutos. Así nadie puede enviarte avisos falsos ni reenviar uno capturado.
import { createHmac, timingSafeEqual } from "node:crypto"
// cuerpo: el texto crudo de la solicitud, antes de interpretarlo como JSON.
export function firmaValida(cuerpo, cabecera, secreto) {
const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=")))
const marca = Number(partes.t)
if (!marca || Math.abs(Date.now() / 1000 - marca) > 300) return false
const esperada = createHmac("sha256", secreto).update(`${marca}.${cuerpo}`).digest()
const recibida = Buffer.from(partes.v1 ?? "", "hex")
return recibida.length === esperada.length && timingSafeEqual(recibida, esperada)
}import hashlib, hmac, time
def firma_valida(cuerpo: bytes, cabecera: str, secreto: str) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(","))
marca = int(partes.get("t", "0"))
if abs(time.time() - marca) > 300:
return False
mensaje = f"{marca}.".encode() + cuerpo
esperada = hmac.new(secreto.encode(), mensaje, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, partes.get("v1", ""))<?php
function firma_valida(string $cuerpo, string $cabecera, string $secreto): bool {
parse_str(str_replace(',', '&', $cabecera), $partes);
$marca = (int) ($partes['t'] ?? 0);
if (abs(time() - $marca) > 300) return false;
$esperada = hash_hmac('sha256', $marca . '.' . $cuerpo, $secreto);
return hash_equals($esperada, $partes['v1'] ?? '');
}
$cuerpo = file_get_contents('php://input');
if (!firma_valida($cuerpo, $_SERVER['HTTP_X_FIRMA'] ?? '', getenv('SECRETO_WEBHOOK'))) {
http_response_code(401);
exit;
}Casos de uso
Inscripciones desde tu sistema de pagos
Cuando se confirma un pago, tu sistema envía al participante con POST a participantes. Recibe su credencial digital al instante y solo quien pagó tiene acceso.
Resultados para tu herramienta de certificados
Suscríbete a evento.finalizado. Al recibirlo, recorre la asistencia con estado=ALCANZADO y emite los certificados con tu herramienta.
Sin programar: Zapier, Make o n8n
Crea un flujo que reciba el webhook (por ejemplo, Catch Hook en Zapier o Custom webhook en Make) y que llame a la API con una solicitud HTTP. Sirve para llevar inscritos a una hoja de cálculo, avisar por correo o actualizar un CRM.