Saltar a contenido

API de check-in y ejemplos

Las rutas están disponibles únicamente con CHECKIN_ENABLED=true. Comparten el origen de la API de firmas y usan el prefijo /v1/checkin/sites/{site}. openapi.yaml en la raíz del repositorio es el contrato HTTP canónico; docs/API_CONTRACT.md explica el contrato funcional. Esta guía muestra cómo consumirlo. Con SWAGGER_ENABLED=true, abre /swagger en el origen de tu API y busca el grupo Check-in; el JSON generado está en /swagger/v1/swagger.json.

Acceso y aislamiento

Operación Credencial
Contexto y registro de recepción Pública, establecimiento autorizado
Contexto y registro de reserva Token privado de la invitación, en el cuerpo JSON
Provisión de reservas y consulta/reintento de entregas Cabecera X-Checkin-Admin-Token
Entrega del servicio a BC OAuth de Entra configurado exclusivamente en backend

BC_BEARER_TOKEN y X-Device-Token pertenecen a las firmas; no habilitan estas operaciones administrativas. El token administrativo de check-in debe tener al menos 32 caracteres y se compara en tiempo constante. Es común a los sitios de esta instalación: no representa un permiso distinto por recepcionista o establecimiento. Protégelo y no lo incluyas en la web.

site debe figurar en CHECKIN_SITE_SLUGS. Admite letras ASCII minúsculas, dígitos y guiones; empieza por letra/dígito y tiene hasta 80 caracteres. Se distingue entre mayúsculas y minúsculas. Consultas, referencias e idempotencia están acotadas al sitio; el token de una reserva de otro sitio no sirve. No existe alta pública de establecimientos.

Se usa JSON con nombres camelCase. Las fechas civiles son YYYY-MM-DD; los instantes incluyen zona horaria y se devuelven en UTC. Cada cuerpo está limitado a 16 KiB. Las respuestas, incluidos errores, deshabilitan caché. El límite es 60 peticiones por minuto e IP, compartido entre las rutas de check-in de cada proceso API; no es un limitador distribuido.

Rutas

Todas las rutas siguientes son relativas a /v1/checkin/sites/{site}:

Método y ruta Entrada Éxito
GET /reception Sin cuerpo 200 {site,referenceDate}
POST /reservation-context {token} 200 {reservationReference,capacity,registered,remaining,expiresAt,referenceDate}
POST /guests {mode,token?,idempotencyKey,guest} 200 {receiptId,remaining,capacity}
POST /reservations Admin + {reservationReference,capacity,expiresAt} 201 {reservationId,token,reservationReference,capacity,expiresAt}
GET /deliveries Admin 200, array de hasta 100 metadatos
GET /deliveries/{receiptId} Admin 200, metadatos de un recibo
POST /deliveries/{receiptId}/retry Admin; sin cuerpo 204, reintento/revisión programado

Los metadatos de entrega y sus estados se explican en Operación. No existe listado público de huéspedes ni descarga pública del contenido de un recibo.

Provisionar una reserva

La integración BC llama a POST /reservations después de crear su reserva de negocio:

POST /v1/checkin/sites/las-torres/reservations
Content-Type: application/json
X-Checkin-Admin-Token: <secreto-administrativo>
{
  "reservationReference": "BC-STABLE-BOOKING-KEY",
  "capacity": 2,
  "expiresAt": "2027-01-31T12:00:00Z"
}

La referencia tiene 1–100 caracteres y debe ser única y estable dentro del establecimiento. Evita números reutilizables entre años: usa una identidad estable de BC. capacity es un entero de 1 a 1000 y expiresAt debe estar en el futuro, como máximo dos años después de la solicitud. La fecha del ejemplo debe ajustarse al momento de uso.

El 201 devuelve el ID de reserva, los tres campos anteriores y un token URL-safe de 43 caracteres. Se generan 32 bytes aleatorios y solo se conserva su hash SHA-256. La respuesta es la única oportunidad de obtener ese token. Guarda respuesta/token en un lugar protegido del lado BC antes de enviar la invitación.

Una segunda llamada con la misma referencia devuelve 409 reservation_exists; no devuelve el token anterior. Esta provisión no usa la idempotencia de /guests. Si se pierde su respuesta, no cambies la referencia para volver a probar: crearías otro cupo. La recuperación de token, revocación, rotación, actualización de plazas y cancelación no tienen endpoint implementado. Deben resolverse antes de automatizar ese ciclo en producción.

Consultar el contexto

Recepción usa GET /reception y recibe el slug y la fecha de negocio del establecimiento. Para reserva envía el token por POST:

{"token":"<token-opaco-devuelto-al-provisionar>"}

Respuesta ilustrativa de /reservation-context:

{
  "reservationReference": "BC-STABLE-BOOKING-KEY",
  "capacity": 2,
  "registered": 0,
  "remaining": 2,
  "expiresAt": "2027-01-31T12:00:00Z",
  "referenceDate": "2026-10-23"
}

referenceDate se calcula con CHECKIN_TIME_ZONE y cambia con el día del establecimiento. No es una fecha que deba copiarse fija a un cliente. El contexto no devuelve datos de otros huéspedes ni reserva una plaza mientras se rellena el formulario.

Registrar un huésped

Ejemplo completo y ficticio de POST /guests:

{
  "mode": "reservation",
  "token": "<token-opaco-devuelto-al-provisionar>",
  "idempotencyKey": "ae7cb5d4-8efb-47ee-a6b2-e342969bcb15",
  "guest": {
    "givenName": "Ana",
    "surname1": "García",
    "surname2": "",
    "birthDate": "1990-05-17",
    "holderRelationship": "",
    "sex": "female",
    "nationality": "ES",
    "documentType": "dni",
    "documentCountry": "ES",
    "documentNumber": "12345678Z",
    "documentSupportNumber": "",
    "documentIssueDate": null,
    "documentExpiryDate": null,
    "address": "Calle Mar 1",
    "city": "Mazarrón",
    "postalCode": "30860",
    "province": "Murcia",
    "country": "ES",
    "phone": "+34 612 345 678",
    "email": "[email protected]"
  }
}

Para recepción usa mode: "reception" y omite token o envíalo como null. Un token no vacío en recepción devuelve 400 invalid_guest. Cada nuevo huésped necesita un UUID no nulo propio. El UUID anterior es ilustrativo: no lo reutilices como constante de integración.

Respuesta 200 de una reserva de dos plazas tras el primer registro:

{"receiptId":"b2fe0703-9e92-4fe8-baf4-e6542b6c2311","remaining":1,"capacity":2}

En recepción remaining y capacity son null. El recibo identifica el envío guardado en el servicio; la creación del huésped y de su entrada de outbox se confirma en la misma transacción. No espera la importación a BC para responder.

Datos del huésped

Los textos se recortan y normalizan a Unicode NFC. Los límites se aplican al contenido normalizado; no se admiten caracteres de control en los campos de texto validados.

Campo Obligatorio Formato y límite
givenName, surname1 Sí 1–100 caracteres cada uno
surname2 No Hasta 100 caracteres
birthDate Sí Fecha civil válida, no futura
holderRelationship Menor de 14 Hasta 100 caracteres; no vacío cuando es obligatorio
sex Sí male, female, unspecified
nationality Sí País/nacionalidad del huésped, ISO 3166-1 alpha-2
documentType Sí dni, passport, nie
documentCountry Sí País emisor, ISO alpha-2
documentNumber Sí 3–40 letras ASCII/dígitos; se eliminan espacios/guiones y se convierte a mayúsculas
documentSupportNumber No Hasta 40 caracteres
documentIssueDate No Fecha ISO o null; desde nacimiento hasta referenceDate
documentExpiryDate No Fecha ISO o null; no anterior a la expedición si se informa
address Sí 1–250 caracteres
city, province Sí 1–100 caracteres cada uno, admiten direcciones internacionales
postalCode Sí 1–20 caracteres, admite letras y espacios
country Sí País de residencia, ISO alpha-2
phone Sí Hasta 40 caracteres; 7–15 dígitos ASCII, + inicial opcional y puntuación habitual
email Sí Hasta 254 caracteres; dirección válida sin nombre de presentación

Los tres países pueden ser diferentes. Las fechas opcionales vacías se representan con null, no con "". La API no recibe edad calculada ni el prefijo telefónico como campos separados.

El umbral de menor de 14 es birthDate > referenceDate.AddYears(-14), incluido el ajuste de años bisiestos. El servidor obtiene su fecha de negocio en cada envío. La relación se exige tanto con reserva como en recepción. No se ha inventado un catálogo legal de parentescos ni una excepción para menores sin documento.

DNI con emisor ES y NIE validan su letra de control. NIE exige emisor ES; DNI con otro país representa un documento nacional extranjero con validación alfanumérica conservadora. Números con forma de DNI/NIE español tampoco pueden eludir la letra cambiando el tipo. No se verifica autenticidad ni se implementan controles específicos para todos los pasaportes extranjeros. Un documento caducado no se rechaza solo por estar caducado.

Idempotencia, cupo y concurrencia

La clave es idempotencyKey dentro del JSON, no la cabecera usada por PAdES. Guarda juntos sitio, modo, token, datos normalizados y UUID para reintentar el mismo envío.

  • Misma clave y mismo contenido normalizado: devuelve el recibo original, incluso cuando la reserva ya esté llena o haya caducado. Los contadores del recibo son los del éxito original; consulta contexto si necesitas disponibilidad actual.
  • Misma clave y contenido distinto: 409 idempotency_conflict. No generes otra clave automáticamente para superar el conflicto.
  • Mismo documento en una reserva con una clave distinta: 409 duplicate_document. La identidad es país emisor + número canónico, independiente del tipo elegido.
  • Recepción no deduplica por documento ni tiene cupo de reserva. Dos claves distintas representan dos registros distintos; concilia resultados inciertos antes de crear otra operación.

PostgreSQL bloquea la fila de reserva con FOR UPDATE durante comprobación e inserción. Las restricciones únicas de sitio/clave y sitio/reserva/documento refuerzan esa protección. La relación compuesta incluye el sitio y evita enlazar una persona a una reserva de otro establecimiento. El navegador no garantiza estas propiedades por sí solo.

Errores y recuperación

Los errores de aplicación de check-in usan {code,message?}. No exigen traceId en el cuerpo como los errores antiguos de firmas; la cabecera compartida X-Trace-Id sirve para diagnóstico. No muestres mensajes arbitrarios de servidor al huésped ni registres cuerpos con datos personales o tokens.

HTTP Código / caso Acción
400 invalid_guest Revisar campos, modo, UUID y, en provisión, referencia/cupo/caducidad
400 JSON o fechas/UUID mal formados Error de enlace del framework; el cuerpo puede no seguir el sobre de check-in
401 checkin_unavailable Falta o no coincide el secreto administrativo; comprobar también longitud configurada
404 unknown_site Revisar slug exacto y allowlist
404 invalid_token Revisar invitación original y establecimiento; no cambiar a recepción automáticamente
404 delivery_not_found No hay ese recibo en el sitio consultado
409 reservation_full El cupo se completó, posiblemente durante otro envío
409 duplicate_document Ya existe ese documento en la reserva; conciliar
409 idempotency_conflict La clave pertenece a otro contenido; conservarla e investigar
409 reservation_exists La referencia ya fue provisionada; no hay devolución del token anterior
409 delivery_not_retryable Recibo desconocido, en procesamiento o ya importado
410 reservation_expired Invitación caducada; contactar con recepción
413 Cuerpo mayor de 16 KiB Reducir al contrato; respuesta del framework, no necesariamente JSON de check-in
429 rate_limited Esperar antes de repetir; conservar el mismo intento si era un registro
503 checkin_unavailable Fallo de persistencia; comprobar servicio y reintentar con la misma clave

El proxy puede producir otros errores o HTML. Una respuesta perdida o no válida es un resultado incierto, no autorización para crear otro registro. Las rutas {receiptId:guid} también pueden devolver un 404 de enrutamiento ante un identificador mal formado.

Prueba reproducible con PowerShell

Después de arrancar la API local, ejecuta en una terminal de operador. Usa únicamente un entorno de prueba y valores ficticios. El ejemplo crea datos; no publica ni envía correos.

$api = 'http://127.0.0.1:8080'
$site = 'las-torres'
# Define CHECKIN_ADMIN_TOKEN en esta terminal con el mismo secreto de la API.
$headers = @{ 'X-Checkin-Admin-Token' = $env:CHECKIN_ADMIN_TOKEN }
$request = @{
    reservationReference = 'LOCAL-' + [guid]::NewGuid().ToString('N')
    capacity = 2
    expiresAt = [DateTimeOffset]::UtcNow.AddDays(2).ToString('O')
} | ConvertTo-Json
$reservation = Invoke-RestMethod -Method Post -Uri "$api/v1/checkin/sites/$site/reservations" `
    -Headers $headers -ContentType 'application/json' -Body $request
$contextBody = @{ token = $reservation.token } | ConvertTo-Json
$context = Invoke-RestMethod -Method Post -Uri "$api/v1/checkin/sites/$site/reservation-context" `
    -ContentType 'application/json' -Body $contextBody
$payload = @{
    mode = 'reservation'
    token = $reservation.token
    idempotencyKey = [guid]::NewGuid().ToString()
    guest = @{
        givenName = 'Ana'; surname1 = 'Garcia'; surname2 = ''
        birthDate = '1990-05-17'; sex = 'female'; nationality = 'ES'
        documentType = 'dni'; documentCountry = 'ES'; documentNumber = '12345678Z'
        address = 'Calle Mar 1'; city = 'Mazarron'; postalCode = '30860'; province = 'Murcia'
        country = 'ES'; phone = '+34 612 345 678'; email = '[email protected]'
    }
} | ConvertTo-Json -Depth 5
$receipt = Invoke-RestMethod -Method Post -Uri "$api/v1/checkin/sites/$site/guests" `
    -ContentType 'application/json' -Body $payload
# Repetir exactamente el mismo texto: mismo recibo y sin consumir otra plaza.
$replay = Invoke-RestMethod -Method Post -Uri "$api/v1/checkin/sites/$site/guests" `
    -ContentType 'application/json' -Body $payload
$receipt.receiptId -eq $replay.receiptId

Conserva $payload sin modificar si una llamada falla con resultado incierto. El ejemplo usa una referencia nueva solo para otra reserva de laboratorio, nunca para recuperar una provisión cuyo resultado se haya perdido. Para abrir esa misma reserva en la web local compón http://127.0.0.1:5174/?site=las-torres#token= seguido de $reservation.token, sin guardar el enlace en registros compartidos.