Constaia
Integraciones

React

Añade el widget de Constaia a una app React con el componente ConstaiaUpload y el hook useConstaiaUpload, subiendo los archivos a tu propio backend.

@constaia/widget/react es un wrapper fino sobre el web component <constaia-upload>: captura el documento (arrastrar, elegir archivo o cámara), revisa la calidad de la imagen y lo sube a tu backend. Funciona con React 18 y 19, y con renderizado en servidor.

Necesitas un backend

React se ejecuta en el navegador y la clave de API nunca puede estar ahí. El widget sube el archivo a un endpoint tuyo (/api/constaia) y es ese servidor el que llama a Constaia. Si usas Next.js o React Router, sigue Next.js o Remix y React Router; para una SPA con Vite, monta el backend con Express (o cualquier otro de las integraciones).

Instalación

npm i @constaia/widget

Lo que debe hacer tu backend

El widget envía POST multipart/form-data al endpoint con dos campos:

CampoContenido
fileEl documento. Para DNI, NIE y documento de identidad europeo, anverso y reverso unidos en un solo JPEG.
optionsJSON con expect y language tomados de los atributos. Es solo una pista.

Tu backend debe:

  1. Autenticar al usuario y limitar los intentos (cada análisis gasta créditos).
  2. Llamar a Constaia con expect y checks decididos en el servidor, no con los que mande el navegador.
  3. Devolver el análisis tal cual (JSON) con estado 200, o 202 si aún está en cola.
  4. En caso de error, devolver un estado no 2xx con { "error": { "message": "…" } }: el widget muestra ese mensaje.

Un ejemplo mínimo con Express:

server/server.mjs
import express from "express";
import multer from "multer";
import { Constaia, ConstaiaError, InvalidRequestError } from "@constaia/sdk";

const constaia = new Constaia({ apiKey: process.env.CONSTAIA_API_KEY });
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
const app = express();

app.post("/api/constaia", upload.single("file"), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: { message: "Falta el archivo." } });
  try {
    const analysis = await constaia.analyze(req.file.buffer, {
      filename: req.file.originalname,
      expect: "es_dni",
      checks: { notExpired: true, minAgeYears: 18 },
    });
    res.status(analysis.status === "completed" ? 200 : 202).json(analysis);
  } catch (err) {
    if (err instanceof InvalidRequestError) return res.status(err.status ?? 400).json({ error: { message: err.message } });
    console.error(err instanceof ConstaiaError ? err.requestId : "", err);
    res.status(502).json({ error: { message: "No se ha podido verificar el documento. Inténtalo de nuevo." } });
  }
});

app.listen(3000);

La versión completa (sesión, todos los errores del SDK, webhook con firma) está en Express.

En desarrollo, haz que Vite redirija /api al backend para compartir origen y cookies:

vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { "/api": "http://localhost:3000" },
  },
});

El componente ConstaiaUpload

src/VerifyDocument.tsx
import { ConstaiaUpload } from "@constaia/widget/react";
import { useState } from "react";

export function VerifyDocument() {
  const [verdict, setVerdict] = useState<string | null>(null);

  return (
    <section>
      <h1>Verifica tu DNI</h1>
      <ConstaiaUpload
        endpoint="/api/constaia"
        document="es_dni"
        lang="es"
        onResult={(analysis) => setVerdict(analysis.verdict?.status ?? null)}
        onError={(error) => console.warn(error.code, error.message)}
        onStatus={(status) => status === "idle" && setVerdict(null)}
      />
      {verdict === "valid" && <a href="/registro/datos">Continuar</a>}
    </section>
  );
}

Las props son los atributos del widget en camelCase más los manejadores de eventos:

PropDescripción
endpointURL de tu backend.
documentTipo de documento para la interfaz (marco de tarjeta, dos caras). Si falta expect, también se envía como expect.
expectTipo o tipos esperados (string o string[]), enviados en options.
sides, frame1 o 2 caras; marco card o none. Automáticos para DNI, NIE y documento de identidad europeo.
accept, maxSizeMbTipos aceptados y tamaño máximo (20 MB por defecto).
autoSubmitfalse espera a que el usuario (o submit()) envíe tras el control de calidad.
withCredentialsEnvía cookies a un endpoint de otro origen.
lang, theme, cameraIdioma (es, en, pt, fr), tema (light, dark, auto) y botón de cámara (auto, always, never).
headersCabeceras extra, por ejemplo un token CSRF.
messagesSustituye textos de la interfaz.
onFile, onQuality, onProgress, onResult, onError, onStatusEventos del widget. onResult recibe el análisis tal y como lo devuelve tu backend.

La referencia completa de atributos, eventos y estilos está en Widget.

El hook useConstaiaUpload

El hook expone el estado del widget para construir tu propia interfaz alrededor: ref, element, status, progress, result, error, reset() y submit(). Acepta los mismos atributos que el componente, más messages y headers.

src/VerifyWithControls.tsx
import { ConstaiaUpload, useConstaiaUpload } from "@constaia/widget/react";

export function VerifyWithControls({ csrfToken }: { csrfToken: string }) {
  const { ref, status, progress, result, error, reset, submit } = useConstaiaUpload({
    endpoint: "/api/constaia",
    document: "es_dni",
    lang: "es",
    autoSubmit: false,
    headers: { "X-CSRF-Token": csrfToken },
  });
  const verdict = result?.verdict?.status;

  return (
    <section>
      <ConstaiaUpload ref={ref} />

      {status === "ready" && (
        <button type="button" onClick={() => submit()}>
          Enviar documento
        </button>
      )}
      {status === "uploading" && <progress value={progress} max={100} />}
      {status === "analyzing" && <p>Analizando…</p>}

      {verdict === "valid" && <a href="/registro/datos">Continuar</a>}
      {verdict === "review" && <p>No se lee bien. Haz otra foto con buena luz y sin reflejos.</p>}
      {(verdict === "invalid" || error) && (
        <button type="button" onClick={reset}>
          Probar con otro documento
        </button>
      )}
    </section>
  );
}

Los estados son idle, checking, quality, ready, uploading, analyzing, done y error. Si el análisis tarda más de 30 s, tu backend devuelve el análisis en queued o processing y el widget muestra un aviso de "en cola"; el resultado final te llega en el backend por webhook.

No te fíes del resultado en el navegador

result sirve para la interfaz. Cuando el usuario continúe, tu backend debe decidir con el resultado que guardó al llamar a Constaia (o con GET /v1/analyses/{id}), nunca con un veredicto que le reenvíe el navegador.

Probar en modo test

Con una clave ck_test_... en tu backend no se gastan créditos y el resultado depende del nombre del archivo, que debe ser una imagen o PDF real. El widget une las dos caras en un JPEG con el nombre del anverso.

Archivoverdict.statusMotivo principal
dni_valid.jpgVálidonot_expired (info): "Vigente hasta el 12/03/2031."
dni_expired.jpgNo válidonot_expired (error): "Caducado el 15/06/2020."
blurry.jpgRevisarlow_quality (warning); warnings: blurry, low_quality
foto.jpg (otro nombre)No válidotype_mismatch: se detecta generic

Más nombres en Modo test.

Producción

  • La clave vive solo en el backend. Nunca en variables VITE_* ni REACT_APP_*, que acaban en el bundle.
  • El endpoint de subida exige sesión y limita intentos por usuario: cada llamada gasta créditos.
  • Backend y proxy aceptan cuerpos de al menos 20 MB (o baja maxSizeMb al límite de tu plataforma).
  • Timeouts del backend y del proxy de 60 s o más: el análisis síncrono puede tardar hasta 30 s.
  • Si el backend está en otro dominio, configura CORS con credenciales y usa withCredentials, o pasa un token con headers.

Siguientes pasos

En esta página