Autenticación y límites de tasa

Todas las rutas de verificación requieren la cabecera Authorization: Bearer <token>. Un token ausente, inválido, revocado o caducado devuelve 401. Superado el límite de tu plan (30 peticiones/segundo en Starter, 60 en Standard), una petición devuelve 429.

Cuando a tu token le quedan menos de 7 días para caducar, cada respuesta autenticada incluye además la cabecera X-License-Warning (p. ej. license expires in 3 day(s), renew soon): solo un aviso, la petición en curso se completa con normalidad.

Quién decide qué información se divulga

Cada atributo de una presentación SD-JWT VC (la edad, un título, un estado) está protegido por su propio digest salado en el JWT firmado. La cartera solo adjunta a su presentación el valor y el salt de los atributos que el usuario ha aceptado revelar: el resto permanece como una huella irreversible. Todis solo tiene acceso técnicamente a lo que realmente se divulga en la presentación que recibe, nunca más.

Con POST /verify/sessions, el campo claims de tu solicitud se convierte en una consulta DCQL integrada en la solicitud de autorización enviada a la cartera: el usuario ve y aprueba exactamente esa lista antes de que su cartera construya la presentación. Con POST /verify/sd-jwt-vc, ese paso ya ha ocurrido antes de que obtengas la presentación: Todis la verifica y te devuelve su contenido tal cual.

POST /verify/sd-jwt-vc

Verifica una presentación SD-JWT VC. Dos modos en el cuerpo de la petición: issuer_public_key_pem (clave del emisor indicada directamente) o country_code (resolución automática a través del registro oficial de la UE) ; issuer_public_key_pem tiene prioridad si ambos están presentes.

  • 200: presentación verificada, claims incluido.
  • 400: no se indicó ni issuer_public_key_pem ni country_code.
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 422: presentación malformada o firma no verificada (verified: false, detalle en error).
  • 429: cuota de peticiones del plan superada.
  • 502: resolución del registro de confianza del emisor imposible (solo en modo country_code, por ejemplo si el registro europeo no está disponible temporalmente).

POST /verify/mdoc

Verifica una presentación mdoc (ISO/IEC 18013-5, usada especialmente para el permiso de conducir móvil). El ancla de confianza (issuer_trust_anchor_pem) debe indicarse explícitamente en el cuerpo de la petición. Puede ser la IACA que emitió al firmante del documento, que es la opción recomendada: la cadena del firmante, incrustada en la presentación, se valida hasta ella, incluidas la firma y la ventana de validez, lo que te evita seguir las renovaciones de firmantes. También puede ser el certificado del propio firmante, si prefieres fijarlo. Solo se sigue un nivel, tal como prevé ISO/IEC 18013-5. No hay resolución automática por país para este formato (contáctanos si te concierne un emisor mdoc concreto).

  • 200: presentación verificada, doc_type y namespaces incluidos.
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 422: presentación malformada, certificado de confianza incorrecto o firma no verificada.
  • 429: cuota de peticiones del plan superada.

POST /hosted/sessions

El recorrido de verificación alojado: este servicio sirve él mismo la página que ve el usuario (código QR, enlace a la cartera, textos en francés, inglés o español). Es la integración recomendada cuando no quieres construir una pantalla de verificación, y está detallada en la página Integraciones. Rediriges al usuario a la URL devuelta, vuelve a tu success_url, y siempre confirmas el resultado en el servidor con GET /verify/sessions/{id}. success_url significa verificación superada, nunca que el usuario sea mayor de edad: una prueba de edad que vale false también llega allí, como cualquier resultado verificado. Lee el resultado en el servidor (claims.age_over_18 con el atajo age_over_18) y aplícale tu propia regla.

Es el único endpoint que acepta un webhook_url (un POST JSON firmado con HMAC-SHA256 al concluir el recorrido, incluso si el usuario cierra la página) y los atajos check ("age_over_18", "identity"), disponibles en todos los planes. Requiere un despliegue con identidad de firma de solicitudes, que el servicio alojado tiene.

La opción dc_api. Con "dc_api": true, la página alojada propone al visitante abrir su cartera mediante la Digital Credentials API del W3C, cuando su navegador la admite; el código QR sigue visible. El protocolo es el Anexo C de ISO/IEC 18013-7 (org-iso-mdoc): solo las atestaciones mdoc (ISO/IEC 18013-5) pasan por esta vía, y el PID en formato SD-JWT VC pasa por el código QR. Con check: "age_over_18", esta vía pide la atestación de prueba de edad (eu.europa.ec.av.1), sin recurrir al PID. La respuesta de la cartera va cifrada para Todis y vinculada al origen https://verify.todis.eu: no tienes ningún origen que declarar. El resultado, GET /verify/sessions/{id} y el webhook no cambian. Fuera de un navegador, por ejemplo en una aplicación nativa, usa el código QR o el enlace profundo.

  • 201: recorrido creado, con hosted_url, expires_at y el secreto de webhook.
  • 400: URL de retorno inválida, check desconocido, o campos de verificación incoherentes.
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 403: el plan del token no incluye la gramática DCQL completa (un dcql_query indicado por ti).
  • 429: cuota de peticiones del plan superada.
  • 501: despliegue sin identidad de firma, flujo alojado no disponible.

POST /verify/sessions

Crea una sesión de verificación OpenID4VP. El cuerpo de la petición indica el tipo de credential solicitado (vct) y los atributos solicitados (claims, rutas simples, ej. "age_over_18"). La confianza en el emisor viene de country_code (SD-JWT VC, lista oficial de la UE) o de los registros de anclas de este despliegue, listados por GET /trust/registries. La respuesta contiene una solicitud de autorización OpenID4VP para presentar a la cartera del usuario (código QR o enlace directo); cómo construir exactamente la cadena del código QR o del enlace se detalla en nuestro archivo de integración. La sesión caduca 15 minutos después de su creación, haya recibido respuesta o no. Para necesidades avanzadas (varios credentials por sesión, alternativas entre credentials o entre atributos, valores restringidos), el campo dcql_query acepta una consulta DCQL completa en lugar de vct/claims, y puede mezclar SD-JWT VC y mdoc. El campo credential_trust da entonces a un credential SD-JWT VC su propio country_code, para verificar en una misma sesión credentials de emisores de países distintos (cada entrada solo vale para su credential, nunca para otro). Ver el detalle en la especificación OpenAPI.

encrypted_response vale false por defecto en este endpoint, a diferencia del flujo alojado: es aquí donde debes actuar. Algunas carteras nacionales no aceptan otra cosa que una respuesta cifrada (response_mode=direct_post.jwt) y, sin ella, no devuelven ningún error: leen la petición y abandonan, y la sesión se queda en pending indefinidamente. Si apuntas a una cartera de producción, actívalo.

Sin cifrado, redirect_uri es opcional (sirve al flujo same-device); el cifrado lo hace obligatorio (HAIP 1.0 §5.1). Esa dirección debe ser alcanzable desde el dispositivo que ejecuta la cartera, no desde tu servidor: en cross-device, la cartera la recibe tras enviar su respuesta e intenta abrirla en el teléfono. Una dirección de bucle (http://localhost:...) designa allí al propio teléfono. El síntoma despista: la verificación funcionó, el resultado te llega, y es el teléfono el que muestra un error.

curl -X POST https://verify.todis.eu/verify/sessions \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vct": "https://example.eu/credentials/pid",
    "claims": ["age_over_18"],
    "country_code": "FR"
  }'
  • 201: sesión creada, con session_id y authorization_request.
  • 400: claims vacío, o ningún ancla válida cubre el tipo solicitado.
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 403: el plan del token no incluye la gramática DCQL completa (dcql_query, plan Premium).
  • 429: cuota de peticiones del plan superada.
  • 502: no se pudo resolver el registro de confianza (modo country_code).
  • 503: el registro de anclas del que depende la sesión no tiene ninguna base cargada.

GET /verify/sessions/{id}

Consulta el resultado de una sesión creada por POST /verify/sessions, a interrogar después de que el usuario haya respondido con su cartera (o periódicamente mientras se espera).

curl https://verify.todis.eu/verify/sessions/<session_id> \
  -H "Authorization: Bearer TU_TOKEN"
  • 200: tres estados posibles, {"status": "pending"}, {"status": "verified", "claims": {...}} o {"status": "failed", "error": "..."}. Para una sesión mdoc (doctype/mdoc_claims), claims anida los elementos bajo su espacio de nombres. Una sesión verificada lleva además trust, de dónde viene el ancla que la validó.
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 404: sesión desconocida (identificador inválido, o sesión caducada tras 15 minutos).
  • 429: cuota de peticiones del plan superada.

GET /trust/registries

Lista, sin token, las anclas de confianza que acepta este despliegue: para cada registro (mdoc, sd_jwt_vc), sus fuentes y sus anclas, con los tipos de credential que cubre cada una y las fechas de validez de su certificado, y los países que un country_code puede alcanzar. Una fuente leída de un VICAL, la lista firmada de las autoridades emisoras mdoc, indica de qué VICAL procede, y discarded lista las entradas de ese VICAL que no se han convertido en anclas, con su motivo: ahí se entiende por qué falta un ancla esperada. Para señalar un ancla que falta: report_missing_anchor.

curl https://verify.todis.eu/trust/registries
  • 200: anclas de los registros y países de la lista oficial de la UE, respuesta cacheable (Cache-Control, ETag).
  • 304: sin cambios desde el ETag enviado en If-None-Match.

GET /usage

Consulta tu volumen de peticiones directamente con tu token de licencia, sin esperar un panel ni contactar con soporte: útil para comprobar tu consumo antes de que se agote un bloque Starter, o simplemente para seguir tu actividad con el tiempo. Si has recomprado varios bloques Starter, el total suma automáticamente todos los tokens asociados a tu dirección de email, sin que tengas que sumarlos tú mismo.

curl https://verify.todis.eu/usage \
  -H "Authorization: Bearer TU_TOKEN"
  • 200: {"client_ids": [...], "total_authenticated_requests": 42}, o total_authenticated_requests: null con un campo error si las métricas no están disponibles temporalmente (el resto de la respuesta sigue siendo fiable).
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 429: cuota de peticiones del plan superada.

GET /usage/history

El historial diario de tu consumo, a partir del plan Standard: totales por día durante 30 días. El plan Premium añade el desglose por endpoint, las latencias, una profundidad de 400 días y la exportación CSV (?days=30&format=csv). No se conserva ningún dato bruto de verificación, solo agregados.

  • 200: historial JSON, o CSV si format=csv.
  • 401: token de licencia ausente, inválido, revocado o caducado.
  • 403: el plan del token no incluye el historial (Standard en adelante).
  • 429: cuota de peticiones del plan superada.
  • 501: despliegue sin base de datos (autoalojamiento sin DATABASE_URL).

Formato de la respuesta (verificación directa)

Para /verify/sd-jwt-vc y /verify/mdoc: un booleano verified y, según el resultado, la información divulgada (claims para SD-JWT VC, doc_type/namespaces para mdoc) o un campo de texto error. Nunca el documento de identidad completo, solo los atributos realmente divulgados (ver arriba).

Para ir más allá

Para la visión general (autenticación, dos formas de integrar, un ejemplo con curl), consulta Cómo funciona la API. Para el esquema exacto de peticiones/respuestas, todis.eu/es/docs (Swagger). ¿Tienes una pregunta que no está aquí? contact@todis.eu.