DNI en un formulario de inscripción
Valida el DNI, NIE o pasaporte en una inscripción deportiva (vigente, del titular y con edad mínima) con el widget, Express o Laravel.
En una inscripción (una carrera, una licencia, un campus) quieres comprobar tres cosas del documento de identidad: que es un DNI, NIE o pasaporte, que está vigente y que es de la persona que se inscribe. Si la prueba es para adultos, además, que tiene la edad mínima.
Esta guía monta el flujo completo: el widget en la página, tu backend (Node/Express o PHP/Laravel) que llama a Constaia, y la decisión según el veredicto.
El flujo
Página de inscripción Tu backend Constaia
───────────────────── ────────── ────────
1. datos del formulario ─────────▶ guarda la inscripción (borrador)
2. <constaia-upload> ──fichero──▶ POST /api/registrations/:id/document
carga nombre, DNI y fecha de nacimiento
de la inscripción ──Bearer ck_…──────▶ POST /v1/analyze
decide según verdict.status ◀────────── analysis
3. veredicto en pantalla ◀──JSON reducido──La clave de API solo existe en tu backend. El navegador envía el fichero a tu endpoint, y es tu backend quien fija
expect y checks a partir de los datos que ya tiene de la inscripción. No te fíes de las opciones que envíe el cliente.
Las opciones del análisis
{
"expect": ["es_dni", "es_nie", "passport"],
"checks": {
"not_expired": true,
"min_age_years": 18,
"holder": {
"full_name": "María García López",
"document_number": "12345678Z",
"birth_date": "1990-05-14"
}
},
"storage": "none",
"language": "es",
"metadata": { "registration_id": "1234" }
}| Opción | Por qué |
|---|---|
expect | Aceptas cualquiera de los tres. Si llega otro tipo reconocido (un carné de conducir, por ejemplo), el veredicto es invalid con type_mismatch; si no se reconoce el documento, review con type_unknown. |
checks.not_expired | Ya viene activado por defecto en tipos con caducidad; ponerlo explícito deja clara la intención. |
checks.min_age_years | Edad mínima calculada desde la fecha de nacimiento del documento. Quítalo en pruebas sin límite de edad. |
checks.holder | Compara con lo que la persona escribió en el formulario. Ignora tildes, admite otro orden de apellidos y pequeñas erratas. |
storage: "none" | El fichero se procesa en memoria y no se guarda. Es el valor por defecto si no has cambiado el de tu cuenta. |
metadata.registration_id | Te permite encontrar el análisis desde el panel o con GET /v1/analyses?metadata[registration_id]=1234. |
Todos los checks están en Checks.
Paso 1: el widget en la página
El widget <constaia-upload> hace de campo de subida: cámara en móvil, control de calidad antes de
enviar y las dos caras del DNI unidas en una sola imagen. Apunta a un endpoint de tu backend que incluye el id de la
inscripción.
<script type="module" src="https://cdn.jsdelivr.net/npm/@constaia/widget@0.1"></script>
<form id="inscripcion">
<!-- nombre, DNI, fecha de nacimiento… ya guardados como borrador con id 1234 -->
<constaia-upload
endpoint="/api/registrations/1234/document"
expect="es_dni,es_nie,passport"
lang="es"
></constaia-upload>
<button type="submit" disabled>Confirmar inscripción</button>
</form>
<script type="module">
const upload = document.querySelector("constaia-upload");
const submit = document.querySelector("#inscripcion button");
upload.addEventListener("constaia:result", (event) => {
const status = event.detail.verdict?.status;
submit.disabled = status === "invalid";
});
</script>El widget muestra el estado del veredicto y los mensajes de verdict.reasons que devuelva tu backend. El atributo
expect es solo una pista para la interfaz: quien decide es tu servidor. Si usas React o Vue, mira la
guía del widget.
Paso 2: tu backend
npm i @constaia/sdk express multerimport express from "express";
import multer from "multer";
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { findRegistration, updateRegistration } from "./db.js";
const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
const constaia = new Constaia(); // lee CONSTAIA_API_KEY
const NEXT_STATUS = { valid: "confirmed", review: "pending_review", invalid: "document_rejected" };
app.post("/api/registrations/:id/document", upload.single("file"), async (req, res) => {
const registration = await findRegistration(req.params.id);
if (!registration) return res.status(404).json({ error: { message: "Inscripción no encontrada." } });
if (!req.file) return res.status(400).json({ error: { message: "Falta el documento." } });
let analysis;
try {
analysis = await constaia.analyze(
{ file: req.file.buffer, filename: req.file.originalname },
{
expect: ["es_dni", "es_nie", "passport"],
checks: {
notExpired: true,
minAgeYears: 18,
holder: {
fullName: registration.fullName,
documentNumber: registration.documentNumber,
birthDate: registration.birthDate, // "YYYY-MM-DD"
},
},
storage: "none",
language: "es",
metadata: { registration_id: String(registration.id) },
},
);
} catch (err) {
if (err instanceof ConstaiaError) {
console.error("Constaia", err.status, err.code, err.requestId);
return res.status(502).json({ error: { message: "No hemos podido analizar el documento. Inténtalo de nuevo." } });
}
throw err;
}
// Si el análisis tarda más de 30 s llega en queued/processing y sin veredicto: el resultado irá por webhook.
const status = analysis.status === "completed" ? (analysis.verdict?.status ?? "review") : "review";
const documentNumber =
analysis.fields?.document_number?.value ?? analysis.fields?.nie_number?.value ?? null;
await updateRegistration(registration.id, {
status: NEXT_STATUS[status],
documentAnalysisId: analysis.id,
documentVerdict: status,
documentNumber,
});
res.json({
id: analysis.id,
status: analysis.status,
verdict: analysis.verdict,
warnings: analysis.warnings,
});
});
app.listen(3000);El NIE devuelve el número en nie_number; el DNI y el pasaporte, en document_number. Por eso el código lee los dos.
Los campos de cada tipo están en el catálogo.
Si prefieres enviar el fichero con tu propio formulario en vez del widget, el backend es el mismo: recibe el campo
file en multipart. Tienes más ejemplos en las guías de Express y
Laravel.
Paso 3: decidir según el veredicto
| Veredicto | Qué significa aquí | Qué hacer |
|---|---|---|
| Válido | Documento aceptado, vigente, del titular y con la edad mínima. | Confirmar la inscripción. |
| No válido | Algún motivo con severity: "error". | No confirmar. Mostrar los mensajes y dejar subir otro documento. |
| Revisar | Algún motivo con severity: "warning": foto borrosa, confianza baja, un dato del titular ilegible… | Aceptar de forma provisional y mandar a una cola de revisión humana. |
Los motivos que verás en este caso:
code | Con severity: "error" |
|---|---|
type_mismatch | No es un DNI, NIE ni pasaporte. |
not_expired | Caducado ("Caducado el 15/06/2020."). |
holder | Nombre, número o fecha de nacimiento no coinciden con el formulario. |
min_age_years | Menor que la edad mínima ("El titular tiene 16 años; el mínimo es 18."). |
nif_check_digit, mrz_checksums, mrz_matches_visual | Falla una validación determinista: letra del DNI, dígitos de control o MRZ distinta de lo impreso. |
Para mostrar al usuario qué ha fallado, usa los mensajes de los motivos que no son informativos. Ya vienen en el idioma
que pediste con language:
const problems = analysis.verdict.reasons
.filter((reason) => reason.severity !== "info")
.map((reason) => reason.message);En review no rechaces automáticamente: la mayoría son fotos mejorables de documentos correctos. Cómo montar la cola
está en Revisión humana, y el detalle de cada estado en
Veredictos.
Menores
En una inscripción de menores no uses min_age_years. Tienes dos opciones:
- Documento del tutor: valida el DNI o NIE de la madre, padre o tutor con
holdera partir de los datos del tutor en el formulario ymin_age_years: 18. - Documento del menor: si el menor tiene DNI o pasaporte, valídalo con
holdera partir de sus datos y, si la categoría lo exige,max_age_years(por ejemplo,17para categorías sub-18). Si su edad supera el máximo, llega el motivomax_age_yearsconseverity: "error".
Para calcular la edad en una fecha distinta de hoy (por ejemplo, el 31 de diciembre de la temporada), usa
checks.reference_date: "2026-12-31".
Qué guardar
Guarda solo lo que necesitas para justificar la decisión:
analysis.id(para consultarlo después si guardas resultados; ver abajo).verdict.statusy la fecha.- El número de documento validado (
document_numberonie_number).
No hace falta guardar la imagen ni el resto de campos. Con storage: "none" Constaia no guarda el fichero; los
resultados extraídos sí se conservan mientras no borres el análisis. Si tampoco quieres eso, añade
keep_results: false: recibes la respuesta una sola vez y después GET /v1/analyses/{id} devuelve 404. Lo explica
Solo analizar sin guardar.
Probarlo en modo test
Con una clave ck_test_… el resultado depende del nombre del fichero (debe ser una imagen o PDF real). El widget
conserva el nombre del fichero de la cara delantera, así que puedes probar desde la propia página. Detalle en
Modo test.
| Fichero | Resultado |
|---|---|
dni_valid.jpg | DNI de MARÍA GARCÍA LÓPEZ, 12345678Z, nacida el 1990-05-14, vigente hasta el 12/03/2031 → valid (si holder coincide). |
dni_expired.jpg | DNI de JUAN PÉREZ SÁNCHEZ caducado el 15/06/2020 → invalid con not_expired. |
blurry.jpg | Mismo DNI que dni_valid con avisos blurry y low_quality → review. |
nie.jpg | NIE de ANNA KOWALSKA, X1234567L → valid. |
passport.jpg | Pasaporte de MARIA GARCIA LOPEZ, PAA123456 → valid. |
Para probar holder en valid, crea la inscripción de prueba con los datos de dni_valid: María García López,
12345678Z, 1990-05-14. Con otro nombre recibirás invalid y el motivo
"El titular no coincide: full_name es «MARÍA GARCÍA LÓPEZ» y se esperaba «Juan Pérez».".
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@dni_expired.jpg \
-F 'options={"expect":["es_dni","es_nie","passport"],"checks":{"not_expired":true}}'Siguientes pasos
Casos de uso
Guías por caso de uso con DNI en inscripciones, certificados médicos y LOPIVI, justificantes, facturas a Excel, lotes, revisión humana y EE. UU.
Certificado médico deportivo
Comprueba que un certificado médico deportivo declara apto, está firmado y sellado, es del deportista y no supera la antigüedad máxima que fijes.