Constaia
Conceptos

Verificación facial

Módulo opcional: selfie con prueba de vida activa y pasiva comparada 1:1 con la foto del documento de identidad. Modelos abiertos en nuestros servidores de la UE, sin guardar imágenes ni plantillas faciales. Solo cuentas del EEE.

La verificación facial comprueba que quien envía un documento de identidad es su titular: la persona se hace una selfie con dos gestos (prueba de vida) y la comparamos 1:1 con la foto de su DNI, NIE/TIE, pasaporte o permiso de conducir. Es un módulo opcional, desactivado por defecto.

Es biometría (RGPD, art. 9)

Solo se ofrece a cuentas del Espacio Económico Europeo (país de facturación de la UE, Islandia, Liechtenstein o Noruega). No está disponible en Estados Unidos ni para cuentas en dólares o con país EE. UU. (BIPA de Illinois, CUBI de Texas y otras leyes estatales). Lo activa el propietario de la cuenta aceptando el anexo de tratamiento de datos biométricos, que queda en el registro de auditoría.

Qué guardamos (y qué no)

Dato¿Se guarda?
Fotogramas de la selfieNo. Se procesan en memoria y se borran en cuanto termina la comparación (segundos).
Plantilla o vector facialNo. Nunca se guarda ni se devuelve.
Foto del documentoNo se guarda para esto. Usamos el original solo si tu almacenamiento ya lo conserva; si no, la página (o tu integración) reenvía el mismo fichero, que comprobamos por su huella SHA-256.
Resultado (face_match)Sí: estado, similitud, puntuación de prueba de vida, gestos y motivos. Sigue la retención de los resultados (cuenta o plantilla) y se vacía, junto con el consentimiento, con DELETE /v1/analyses?metadata[...] (también en los enlaces y expedientes con esos metadatos), al cancelar el enlace o al borrar el expediente (results_deleted_at).
ConsentimientoSí: fecha, IP, navegador, versión y huella del texto mostrado.

No hacemos identificación 1:N (buscar a alguien entre otras personas) ni usamos las caras para entrenar modelos.

Precio

1 crédito por selfie comparada en modo real (variable FACE_VERIFICATION_CREDITS); gratis en modo test. No se cobra si la persona no hace la selfie (sin cámara o sin consentimiento), si hay que repetirla por calidad (sin cara, poca luz, imagen movida) ni si falla por nuestra parte. Cada intento que llega a compararse cuenta (máximo 3 por enlace).

Modelos

Todos son abiertos, con licencia que permite uso comercial, y se ejecutan en CPU en nuestros servidores de la UE (sin terceros):

PasoModeloLicencia
Detección de caras y 5 puntosYuNet (OpenCV Zoo)MIT
Comparación (vector de 128 dimensiones, coseno)SFace (OpenCV Zoo)Apache-2.0
Prueba de vida pasiva (foto impresa, pantalla, máscara)MiniFASNet V2 + V1SE (Silent-Face-Anti-Spoofing)Apache-2.0
Guía de gestos en el navegadorMediaPipe Face Landmarker (servido desde nuestro dominio)Apache-2.0

Tiempo en servidor: menos de 1,5 s por verificación (unos 150 ms con 6 fotogramas en nuestras pruebas).

En los enlaces de verificación

Añade face_verification al crear el enlace (o a la plantilla):

{
  "documents": [{ "key": "id", "label": "DNI", "expect": ["es_dni", "es_nie", "passport"] }],
  "face_verification": { "enabled": true, "required": true }
}
  1. Después de los documentos, la página muestra el paso Selfie con el texto de consentimiento explícito (finalidad, base legal: consentimiento, borrado inmediato, responsable y encargado).
  2. La persona abre la cámara frontal y hace 2 gestos al azar que elige el servidor: girar la cabeza a un lado, acercarse o parpadear. El navegador solo guía; el servidor vuelve a medirlos sobre los fotogramas.
  3. Si no tiene cámara o no quiere, puede seguir sin selfie: el resultado queda review (no_camera o declined) y lo revisas tú.
  4. Hasta 3 intentos. Los fallos de calidad (sin cara, poca luz, imagen movida) no gastan intento.

El enlace no se completa hasta cerrar el paso de la selfie. required: true hace que el expediente quede incomplete mientras falte, review si queda a revisar e invalid si no coincide. El webhook verification_link.completed y el GET del enlace incluyen:

{
  "face_verification": { "enabled": true, "required": true, "status": "completed", "attempts": 1, "max_attempts": 3, "consent_accepted": true },
  "face_match": {
    "status": "match",
    "similarity": 0.61,
    "liveness": {
      "status": "passed",
      "score": 0.93,
      "challenges": [{ "type": "turn_left", "passed": true }, { "type": "blink", "passed": true }]
    },
    "quality": { "frames": 6, "face_size_px": 88, "sharpness": 142.3, "brightness": 131, "document_face_size_px": 74 },
    "reasons": [],
    "thresholds": { "match": 0.42, "review_band": 0.12, "liveness": 0.6 },
    "document": { "analysis_id": "an_…", "document_key": "id" },
    "checked_at": "2026-09-30T10:12:03Z"
  }
}

La persona nunca ve la similitud ni las puntuaciones (con show_result solo ve el estado).

Cómo decidimos

  • match: similitud ≥ umbral y prueba de vida superada. Nunca hay match sin prueba de vida superada.
  • no_match: similitud por debajo de umbral − franja (different_person).
  • review: todo lo demás: similitud en la franja (similarity_borderline), prueba de vida sin confirmar, sin cara en el documento, sin cámara…

La prueba de vida es passed si los dos gestos se ven en los fotogramas y la puntuación pasiva supera su umbral; failed si falta un gesto, hay más de una cara, la cara cambia durante la captura o los puntos que envía el navegador no cuadran con las imágenes.

Variable (servidor)DefectoQué es
FACE_MATCH_THRESHOLD0.42Similitud coseno desde la que hay coincidencia. OpenCV recomienda 0,363 para SFace en LFW (fotos del mismo tipo); lo subimos porque documento contra selfie es más difícil y preferimos pocos falsos positivos.
FACE_REVIEW_BAND0.12Franja por debajo del umbral que va a revisión (0,30–0,42).
LIVENESS_THRESHOLD0.6Puntuación mínima de MiniFASNet.

Los umbrales se calibran con pnpm --filter @constaia/api face:eval sobre pares con licencia y consentimiento (documento, selfie, misma persona sí/no): el arnés calcula FAR/FRR, EER y el umbral para FAR ≤ 0,1 %.

Con tu propia captura: POST /v1/face-verifications

Si capturas la selfie en tu app, envía los fotogramas y el analysis_id del documento (un análisis completado de un documento de identidad con foto). Solo funciona con el módulo activado; tú recoges el consentimiento explícito.

# 1. Retos al azar (recomendado): sesión de un solo uso, 10 minutos
curl -X POST https://api.constaia.com/v1/face-verifications/sessions \
  -H "Authorization: Bearer $CONSTAIA_API_KEY"
# → { "session_id": "fvs_…", "challenges": ["turn_left", "blink"], "expires_at": "…" }

# 2. 5–8 fotogramas en orden: de frente, los gestos y de frente otra vez
curl https://api.constaia.com/v1/face-verifications \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F analysis_id=an_01J9Z8Q3K4M5N6P7Q8R9S0T1V2 \
  -F frames=@f1.jpg -F frames=@f2.jpg -F frames=@f3.jpg -F frames=@f4.jpg -F frames=@f5.jpg -F frames=@f6.jpg \
  -F document=@dni.jpg \
  -F 'meta={"session_id":"fvs_…","frames":[{"phase":"neutral","t":0},{"phase":"neutral","t":400},{"phase":"turn_left","t":1500},{"phase":"turn_left","t":1600},{"phase":"blink","t":2600},{"phase":"neutral","t":3400}]}'
CampoDescripción
analysis_idAnálisis del documento (DNI, NIE/TIE, pasaporte, permiso). Otro tipo → 422 face_document_not_identity.
frames5–8 JPEG/PNG/WEBP (≤ 2 MB, ~640 px), sin espejo.
metaJSON con session_id (o challenges si no usas sesión), y por fotograma phase (neutral, turn_left, turn_right, move_closer, blink), t (ms) y, opcional, landmarks (nose, left_eye, right_eye, normalizados 0–1) y blink (0–1).
documentEl mismo fichero del documento si ya no lo guardamos (storage: "none" o review sin revisión). Si falta → 422 face_document_unavailable.
dossier_idOpcional: el resultado cuenta en ese expediente.

Responde 201 con { id: "fv_…", object: "face_verification", status, face_match, … }; GET /v1/face-verifications/{id} lo recupera. Sin gestos declarados no hay prueba de vida superada (no_active_challenge), así que el resultado máximo es review.

Errores

CódigoCuándo
403 face_verification_not_enabledLa cuenta no ha activado el módulo.
403 face_verification_unavailableCuenta fuera del EEE, de EE. UU. o en dólares.
503 face_verification_disabledEl servicio no tiene el módulo disponible ahora.
422 face_retryNo se ve bien la cara (sin cara, oscuro, movido): repetir.
422 face_document_unavailable / face_document_mismatchFalta el documento o no es el mismo fichero.
409 face_session_invalidSesión de retos caducada o ya usada.
429 face_attempts_exhaustedSin intentos en el enlace.

En esta página