Widget de subida
<constaia-upload>: componente web para que tus usuarios suban documentos desde el navegador o la cámara del móvil, sin exponer tu clave secreta.
@constaia/widget es un componente web (<constaia-upload>) que muestra una zona para arrastrar o elegir un documento, hacer una foto con el móvil, comprobar la calidad de la imagen antes de subirla y enseñar el veredicto. Funciona en HTML plano y trae envoltorios para React y Vue.
API preliminar
El widget está en desarrollo. Los atributos, eventos y métodos de esta página pueden cambiar antes de la versión 1.0.
Cómo funciona (patrón seguro)
Tu clave ck_live_… o ck_test_… nunca debe llegar al navegador: cualquiera podría leerla y gastar tus créditos. Por eso el widget no llama a Constaia directamente:
El usuario elige o fotografía el documento en <constaia-upload>.
El widget envía el fichero a tu endpoint (atributo endpoint) como multipart/form-data: un campo file y un campo options con { "expect": …, "language": … }.
Tu servidor comprueba que el usuario tiene permiso, decide expect y checks, y llama a POST /v1/analyze con tu clave usando el SDK.
Tu servidor guarda lo que necesite y devuelve al navegador un JSON con el resultado. El widget lo pinta y emite el evento constaia:result.
Si pones una clave secreta en cualquier atributo, el widget se bloquea, emite constaia:error con el código secret_key_rejected y muestra un error en la consola.
Instalación
npm i @constaia/widgetAl importar el paquete se registra el elemento <constaia-upload>:
import "@constaia/widget";Sin herramientas de build, cárgalo desde un CDN como módulo:
<script type="module" src="https://cdn.jsdelivr.net/npm/@constaia/widget/dist/cdn/constaia-widget.min.js"></script>HTML plano
<constaia-upload endpoint="/api/constaia" expect="es_dni,es_nie" lang="es"></constaia-upload>
<script type="module">
import "https://cdn.jsdelivr.net/npm/@constaia/widget/dist/cdn/constaia-widget.min.js";
const upload = document.querySelector("constaia-upload");
upload.addEventListener("constaia:result", (event) => {
const analysis = event.detail; // lo que devolvió tu endpoint
if (analysis.verdict?.status === "valid") {
document.querySelector("#continue").disabled = false;
}
});
upload.addEventListener("constaia:error", (event) => {
console.warn(event.detail.code, event.detail.message);
});
</script>Atributos
| Atributo | Por defecto | Descripción |
|---|---|---|
endpoint | — | Obligatorio. URL de tu servidor que recibe el fichero. |
expect | — | Tipo o tipos esperados, separados por comas: es_dni,es_nie,passport. Se envía a tu endpoint como pista. |
document | primer expect | Tipo que decide la interfaz: marco de tarjeta y si se piden dos caras. |
sides | según el tipo | 2 pide anverso y reverso; 1, una sola imagen. Por defecto se piden dos caras con es_dni, es_nie y eu_id_card. |
frame | según el tipo | card muestra un marco de tarjeta al encuadrar; none lo quita. |
accept | JPEG, PNG, WEBP, HEIC, PDF | Tipos de fichero admitidos, con la sintaxis del atributo accept de HTML. |
max-size-mb | 20 | Tamaño máximo por fichero. |
auto-submit | true | Con "false" el usuario pulsa un botón para enviar. |
lang | lang de la página | Idioma de la interfaz: es, en, pt o fr. También se envía como language. |
camera | auto | Botón Hacer foto: auto lo muestra solo en pantallas táctiles, always siempre, never nunca. |
headers | — | Cabeceras extra en JSON, p. ej. {"X-CSRF-TOKEN":"…"}. |
with-credentials | — | Envía cookies en peticiones a otro origen. |
Lo que llega de expect desde el navegador es solo una pista: el usuario puede cambiarlo. Decide siempre en tu servidor qué tipos y qué checks aplicas.
Cámara en el móvil
En pantallas táctiles, el widget muestra un botón Hacer foto que abre directamente la cámara trasera (capture="environment"), además del selector de ficheros normal. Controla ese botón con el atributo camera. Con documentos de dos caras guía al usuario para fotografiar anverso y reverso y los une en una sola imagen JPEG antes de enviarla.
Antes de subir, comprueba en el propio navegador la resolución, el desenfoque y la exposición. Si la foto es mala, propone repetirla (o subirla igualmente). Así se evitan análisis que acabarían en review por blurry o glare.
Eventos
Todos burbujean y atraviesan el shadow DOM (bubbles y composed).
| Evento | detail | Cuándo |
|---|---|---|
constaia:file | { file, side } | El usuario ha elegido un fichero. Cancelable con preventDefault(). |
constaia:quality | { file, side, metrics, issues } | Resultado de la comprobación de calidad en el navegador. |
constaia:progress | { loaded, total, percent } | Progreso de la subida. |
constaia:result | respuesta de tu endpoint | Tu endpoint ha respondido con éxito. |
constaia:error | { code, message, status? } | Error de validación, de red o respuesta no 2xx de tu endpoint. |
constaia:status | { status } | Cambio de estado: idle, checking, quality, ready, uploading, analyzing, done, error. |
Si tu endpoint responde con un error en el formato { "error": { "code": "…", "message": "…" } }, el widget muestra ese message al usuario.
Métodos y propiedades
| Miembro | Descripción |
|---|---|
open(camera?) | Abre el selector de ficheros o, con true, la cámara. |
selectFile(file, side?) | Equivale a elegir un fichero desde código. |
submit() | Envía los ficheros elegidos (útil con auto-submit="false"). |
reset() | Vuelve al estado inicial. |
status, result, error | Estado actual, último resultado y último error. |
messages | Sustituye textos de la interfaz, p. ej. { verdictValid: "DNI correcto" }. |
React
"use client";
import { ConstaiaUpload } from "@constaia/widget/react";
export function DniUpload({ onVerified }: { onVerified: (analysisId: string) => void }) {
return (
<ConstaiaUpload
endpoint="/api/constaia"
expect={["es_dni", "es_nie"]}
lang="es"
onResult={(analysis) => {
if (analysis.verdict?.status === "valid") onVerified(analysis.id);
}}
onError={(error) => console.warn(error.code, error.message)}
/>
);
}También hay un hook, useConstaiaUpload, que expone status, progress, result, error, submit y reset para montar tu propia interfaz alrededor del componente.
Vue
<script setup lang="ts">
import { ConstaiaUpload } from "@constaia/widget/vue";
function onResult(analysis: { id: string; verdict?: { status: string } | null }) {
if (analysis.verdict?.status === "valid") {
// continúa el formulario
}
}
</script>
<template>
<ConstaiaUpload endpoint="/api/constaia" expect="es_dni" lang="es" @result="onResult" @error="(e) => console.warn(e)" />
</template>Si prefieres usar la etiqueta <constaia-upload> directamente en tus plantillas, instala el plugin ConstaiaPlugin y dile al compilador de Vue que es un elemento personalizado: compilerOptions: { isCustomElement: (tag) => tag.startsWith("constaia-") }.
Tu endpoint de servidor
El endpoint recibe file y options, llama a Constaia y devuelve JSON. Devuelve solo lo que el widget necesita pintar (verdict, warnings, document): los campos extraídos, mejor guárdalos en tu servidor.
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { getSession } from "@/lib/auth"; // tu autenticación
const constaia = new Constaia(); // CONSTAIA_API_KEY solo en el servidor
const LANGUAGES = ["es", "en", "pt", "fr"] as const;
export async function POST(request: Request) {
const session = await getSession();
if (!session) {
return Response.json({ error: { code: "unauthorized", message: "Inicia sesión." } }, { status: 401 });
}
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return Response.json({ error: { code: "missing_file", message: "Falta el documento." } }, { status: 400 });
}
// Del navegador solo aceptamos el idioma; expect y checks los decide el servidor.
const sent = JSON.parse(String(form.get("options") ?? "{}"));
const language = LANGUAGES.includes(sent.language) ? sent.language : "es";
try {
const analysis = await constaia.analyze(file, {
expect: ["es_dni", "es_nie"],
checks: { notExpired: true, holder: { fullName: session.user.name } },
language,
metadata: { user_id: session.user.id },
});
await saveVerification(session.user.id, analysis); // tu base de datos
return Response.json({
id: analysis.id,
object: analysis.object,
status: analysis.status,
document: analysis.document,
verdict: analysis.verdict,
warnings: analysis.warnings,
});
} catch (err) {
if (err instanceof ConstaiaError && err.status && err.status < 500) {
return Response.json({ error: { code: err.code ?? err.type, message: err.message } }, { status: 400 });
}
return Response.json({ error: { code: "upstream_error", message: "Inténtalo de nuevo." } }, { status: 502 });
}
}Accesibilidad
- La zona de subida es un botón accesible por teclado: se activa con Intro o Espacio.
- Los cambios de estado (subiendo, analizando, resultado) se anuncian a lectores de pantalla mediante una región
aria-live. - La barra de progreso usa
role="progressbar"con su valor actual. - Al cambiar de paso, el foco pasa al siguiente control útil (repetir foto, enviar, analizar otro documento).
- El veredicto se muestra siempre con icono y texto, nunca solo con color.
- Textos en español, inglés, portugués y francés; puedes sustituir cualquiera con la propiedad
messages.
Estilos
El componente usa shadow DOM. Puedes ajustar su aspecto con ::part():
constaia-upload::part(container) { border-radius: 12px; }
constaia-upload::part(button-primary) { background: #0b1220; }
constaia-upload::part(result) { font-size: 0.95rem; }Partes disponibles: container, dropzone, frame, button, button-primary, camera-button, progress, progress-bar, result, verdict.