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

  1. 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.
  2. Prueba la conexión: la respuesta indica la organización y el permiso de la clave.

    bash
    curl https://upass.day/api/v1/yo \
      -H "Authorization: Bearer $CLAVE_API"
    json
    {
      "datos": {
        "organizacion": { "id": "cm4...", "nombre": "Mi organización" },
        "permiso": "ESCRITURA"
      }
    }
  3. Lista los eventos con GET /api/v1/eventos y las salas de cada uno con GET /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.

HTTPcodigoCuándo
401no_autenticadoFalta la clave, no existe o fue revocada
403sin_permisoEscritura con una clave de solo lectura
404no_encontradoEl recurso no existe o es de otra organización
422datos_invalidosDatos inválidos; detalles trae los mensajes por campo
422regla_de_negocioUna regla lo impide, por ejemplo, el límite de participantes del plan
413cuerpo_demasiado_grandeEl cuerpo de la solicitud supera 1 MB
429limite_excedidoSe superó el límite de solicitudes por minuto, o hubo demasiados intentos con una clave inválida desde la misma dirección
500error_internoError 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étodoRutaPermisoDescripción
GET/yoLecturaOrganización y permiso de la clave
GET/eventosLecturaEventos de la organización
GET/eventos/{eventoId}LecturaUn evento
GET/eventos/{eventoId}/salasLecturaSalas con sus jornadas
GET/eventos/{eventoId}/participantesLecturaParticipantes; filtros identificacion, salaId y activa
POST/eventos/{eventoId}/participantesEscrituraCrea o actualiza un participante
POST/eventos/{eventoId}/participantes/loteEscrituraCrea o actualiza hasta 500 participantes
GET/eventos/{eventoId}/participantes/{identificacion}LecturaUn participante por su identificación
DELETE/eventos/{eventoId}/participantes/{identificacion}EscrituraRetira la inscripción
GET/eventos/{eventoId}/asistenciaLecturaAsistencia calculada; filtros identificacion, salaId y estado
GET/eventos/{eventoId}/marcacionesLecturaMarcaciones 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.

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

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

bash
curl "https://upass.day/api/v1/eventos/$EVENTO_ID/asistencia?estado=ALCANZADO&pagina=1" \
  -H "Authorization: Bearer $CLAVE_API"
json
{
  "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.

bash
# 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.

EventoCuándo se envía
participante.inscritoSe inscribió un participante: alta en el panel, importación, API o reactivación.
participante.retiradoSe retiró o eliminó una inscripción.
marcacion.registradaSe registró una marcación por cualquier método. En eventos grandes son muchos avisos.
marcacion.anuladaSe anuló una marcación por corrección.
asistencia.minimo_alcanzadoUn participante alcanzó el mínimo y ya no puede bajar de él. Puede demorar unos minutos.
evento.finalizadoTerminó 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.

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

Node.js
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)
}
Python
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
<?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.