Constaia
Integraciones

Svelte

Usa el web component constaia-upload en Svelte 5, con eventos escuchados vía bind:this y addEventListener y los archivos subidos a tu backend.

En Svelte no hace falta wrapper: <constaia-upload> es un web component estándar. Lo importas una vez, lo usas como cualquier etiqueta y escuchas sus eventos. Esta guía es para una app de Svelte 5 con Vite; si usas SvelteKit, sigue SvelteKit, que incluye también el backend.

Necesitas un backend

Svelte 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 ese servidor llama a Constaia. Móntalo con Express o con cualquiera 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 file (para DNI, NIE y documento de identidad europeo, las dos caras unidas en un JPEG) y options (JSON con expect y language, solo como pista). Tu backend debe autenticar al usuario y limitar intentos, llamar a Constaia con expect y checks decididos en el servidor, devolver el análisis tal cual (200, o 202 si sigue en cola) y, si falla, responder con un estado no 2xx y { "error": { "message": "…" } }, que es lo que el widget muestra.

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, redirige /api al backend:

vite.config.ts
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { defineConfig } from "vite";

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

El componente

Los eventos del widget llevan dos puntos en el nombre (constaia:result, constaia:error, constaia:status). Los atributos de evento de Svelte 5 (onclick) y la directiva on: no están pensados para nombres así, de modo que lo seguro es obtener el elemento con bind:this y registrar los listeners con addEventListener dentro de un $effect, que además los retira al desmontar.

src/lib/VerifyDocument.svelte
<script lang="ts">
  import "@constaia/widget";
  import type { Analysis, ConstaiaUploadElement, WidgetErrorDetail, WidgetStatus } from "@constaia/widget";

  let { csrfToken }: { csrfToken?: string } = $props();

  let uploader: ConstaiaUploadElement | undefined = $state();
  let verdict = $state<string | null>(null);
  let status = $state<WidgetStatus>("idle");

  $effect(() => {
    const el = uploader;
    if (!el) return;
    if (csrfToken) el.headers = { "X-CSRF-Token": csrfToken };

    const onResult = (e: Event) => {
      verdict = (e as CustomEvent<Analysis>).detail.verdict?.status ?? null;
    };
    const onError = (e: Event) => {
      const { code, message } = (e as CustomEvent<WidgetErrorDetail>).detail;
      console.warn(code, message);
    };
    const onStatus = (e: Event) => {
      status = (e as CustomEvent<{ status: WidgetStatus }>).detail.status;
    };

    el.addEventListener("constaia:result", onResult);
    el.addEventListener("constaia:error", onError);
    el.addEventListener("constaia:status", onStatus);
    return () => {
      el.removeEventListener("constaia:result", onResult);
      el.removeEventListener("constaia:error", onError);
      el.removeEventListener("constaia:status", onStatus);
    };
  });

  function retry() {
    uploader?.reset();
    verdict = null;
  }
</script>

<h1>Verifica tu DNI</h1>
<constaia-upload bind:this={uploader} endpoint="/api/constaia" document="es_dni" lang="es"></constaia-upload>

{#if status === "analyzing"}
  <p>Analizando…</p>
{/if}

{#if verdict === "valid"}
  <a href="/registro/datos">Continuar</a>
{:else if verdict === "review"}
  <p>No se lee bien. Haz otra foto con buena luz y sin reflejos.</p>
{:else if verdict === "invalid"}
  <button type="button" onclick={retry}>Probar con otro documento</button>
{/if}
  • import "@constaia/widget" registra el elemento una sola vez para toda la app.
  • Los atributos (endpoint, document, expect, lang, theme, camera, max-size-mb…) se escriben como en HTML. La referencia completa está en Widget.
  • Propiedades como headers (objeto) o messages se asignan sobre el elemento, como en el $effect.
  • Con document="es_dni" el widget pide las dos caras, revisa la calidad de la imagen y las une en un JPEG antes de subirlo.

No te fíes del resultado en el navegador

El veredicto que recibe el componente 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}). Si el análisis tarda más de 30 s, el widget muestra un aviso de "en cola" y el resultado llega a tu backend por webhook.

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_*, 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 max-size-mb 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 añade el atributo with-credentials, o pasa un token con headers.

Siguientes pasos

En esta página