Constaia
Integrations

Svelte

Use the constaia-upload web component in Svelte 5, with events handled through bind:this and addEventListener and files uploaded to your backend.

Svelte needs no wrapper: <constaia-upload> is a standard web component. You import it once, use it like any tag and listen to its events. This guide is for a Svelte 5 app with Vite; if you use SvelteKit, follow SvelteKit, which also covers the backend.

You need a backend

Svelte runs in the browser and the API key can never be there. The widget uploads the file to an endpoint of yours (/api/constaia) and that server calls Constaia. Build it with Express or any of the integrations.

Install

npm i @constaia/widget

What your backend must do

The widget sends POST multipart/form-data to endpoint with file (for DNI, NIE and EU ID cards, both sides merged into one JPEG) and options (JSON with expect and language, only as a hint). Your backend must authenticate the user and limit attempts, call Constaia with expect and checks decided on the server, return the analysis as-is (200, or 202 if still queued) and, on failure, answer with a non-2xx status and { "error": { "message": "…" } }, which is what the widget shows.

A minimal Express example:

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: "The file is missing." } });
  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: "The document could not be verified. Please try again." } });
  }
});

app.listen(3000);

The full version (session, every SDK error, signed webhook) is in Express. In development, forward /api to the 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" },
  },
});

The component

The widget's event names contain a colon (constaia:result, constaia:error, constaia:status). Svelte 5 event attributes (onclick) and the on: directive are not designed for such names, so the safe approach is to get the element with bind:this and register listeners with addEventListener inside an $effect, which also removes them on unmount.

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>Verify your ID</h1>
<constaia-upload bind:this={uploader} endpoint="/api/constaia" document="es_dni" lang="en"></constaia-upload>

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

{#if verdict === "valid"}
  <a href="/signup/details">Continue</a>
{:else if verdict === "review"}
  <p>We could not read it well. Take another photo in good light, without glare.</p>
{:else if verdict === "invalid"}
  <button type="button" onclick={retry}>Try another document</button>
{/if}
  • import "@constaia/widget" registers the element once for the whole app.
  • Attributes (endpoint, document, expect, lang, theme, camera, max-size-mb…) are written as in HTML. The full reference is in Widget.
  • Properties such as headers (object) or messages are assigned on the element, as in the $effect.
  • With document="es_dni" the widget asks for both sides, checks image quality and merges them into one JPEG before uploading.

Do not trust the result in the browser

The verdict the component receives is for the UI. When the user moves on, your backend must decide with the result it stored when calling Constaia (or with GET /v1/analyses/{id}). If the analysis takes longer than 30 s, the widget shows a "queued" notice and the result reaches your backend by webhook.

Test mode

With a ck_test_... key in your backend no credits are spent and the result depends on the file name, which must be a real image or PDF. The widget merges both sides into a JPEG named after the front.

Fileverdict.statusMain reason
dni_valid.jpgVálidonot_expired (info): "Valid until 12/03/2031."
dni_expired.jpgNo válidonot_expired (error): expired on 15/06/2020
blurry.jpgRevisarlow_quality (warning); warnings: blurry, low_quality
photo.jpg (any other name)No válidotype_mismatch: generic is detected

More names in Test mode.

Production

  • The key only lives on the backend. Never in VITE_* variables, which end up in the bundle.
  • The upload endpoint requires a session and limits attempts per user: every call spends credits.
  • Backend and proxy accept bodies of at least 20 MB (or lower max-size-mb to your platform's limit).
  • Backend and proxy timeouts of 60 s or more: a synchronous analysis can take up to 30 s.
  • If the backend is on another domain, configure CORS with credentials and add the with-credentials attribute, or pass a token through headers.

Next steps

On this page