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/widgetWhat 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:
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:
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.
<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) ormessagesare 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.
| File | verdict.status | Main reason |
|---|---|---|
dni_valid.jpg | Válido | not_expired (info): "Valid until 12/03/2031." |
dni_expired.jpg | No válido | not_expired (error): expired on 15/06/2020 |
blurry.jpg | Revisar | low_quality (warning); warnings: blurry, low_quality |
photo.jpg (any other name) | No válido | type_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-mbto 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-credentialsattribute, or pass a token throughheaders.
Next steps
Vue
Add the Constaia widget to a Vue 3 app with the ConstaiaUpload component, the ConstaiaPlugin plugin or the raw tag, uploading to your backend.
Expo and React Native
Photograph documents in an Expo or React Native app with expo-image-picker, upload them to your backend with FormData and let the server call Constaia.