Constaia
Guías por caso

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

options.json
{
  "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ónPor qué
expectAceptas 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_expiredYa viene activado por defecto en tipos con caducidad; ponerlo explícito deja clara la intención.
checks.min_age_yearsEdad mínima calculada desde la fecha de nacimiento del documento. Quítalo en pruebas sin límite de edad.
checks.holderCompara 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_idTe 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.

inscripcion.html
<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 multer
server.js
import 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

VeredictoQué significa aquíQué hacer
VálidoDocumento aceptado, vigente, del titular y con la edad mínima.Confirmar la inscripción.
No válidoAlgún motivo con severity: "error".No confirmar. Mostrar los mensajes y dejar subir otro documento.
RevisarAlgú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:

codeCon severity: "error"
type_mismatchNo es un DNI, NIE ni pasaporte.
not_expiredCaducado ("Caducado el 15/06/2020.").
holderNombre, número o fecha de nacimiento no coinciden con el formulario.
min_age_yearsMenor que la edad mínima ("El titular tiene 16 años; el mínimo es 18.").
nif_check_digit, mrz_checksums, mrz_matches_visualFalla 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:

reasons.js
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 holder a partir de los datos del tutor en el formulario y min_age_years: 18.
  • Documento del menor: si el menor tiene DNI o pasaporte, valídalo con holder a partir de sus datos y, si la categoría lo exige, max_age_years (por ejemplo, 17 para categorías sub-18). Si su edad supera el máximo, llega el motivo max_age_years con severity: "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.status y la fecha.
  • El número de documento validado (document_number o nie_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.

FicheroResultado
dni_valid.jpgDNI 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.jpgDNI de JUAN PÉREZ SÁNCHEZ caducado el 15/06/2020 → invalid con not_expired.
blurry.jpgMismo DNI que dni_valid con avisos blurry y low_quality → review.
nie.jpgNIE de ANNA KOWALSKA, X1234567L → valid.
passport.jpgPasaporte 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».".

Terminal
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

En esta página