Upload widget
Reference for @constaia/widget, the <constaia-upload> web component that captures documents in the browser without exposing your key. React, Vue and any framework.
@constaia/widget is a web component, <constaia-upload>, that captures a document in the browser, checks the
photo quality and sends it to your backend. Your backend calls Constaia with the key and returns the result,
which the widget shows as Valid, Invalid or
Review with its reasons. Current version: 0.1.0.
- No dependencies, about 13 kB compressed. Works in any framework, with React and Vue wrappers.
- Drag and drop, file picker and camera on phones. JPEG, PNG, WEBP, HEIC and PDF up to 20 MB.
- ID-1 card framing guide, client-side quality check and front + back capture.
- Real upload progress, accessible (keyboard,
aria-live, verdicts with icon and text) and ines,en,pt,fr.
The widget never holds the key
If any attribute contains something that looks like a key (ck_live_… or ck_test_…), the component refuses to
work, shows an error and fires constaia:error with code secret_key_rejected.
Installation
npm install @constaia/widgetimport "@constaia/widget"; // registers <constaia-upload>How it works
- The user picks or photographs the document. For Spanish DNI, NIE and EU ID cards, the widget asks for front and back and merges them into a single JPEG.
- The widget checks quality (resolution, blur, brightness) and warns before uploading.
- It sends
POST multipart/form-datato yourendpointwith two fields:fileandoptions. - Your backend calls
POST /v1/analyze, deciding itselfexpectandchecks, and returns the analysis as JSON. - The widget shows the verdict, reasons and warnings, and fires
constaia:result.
Minimal example
<script type="module" src="https://cdn.jsdelivr.net/npm/@constaia/widget@0.1"></script>
<constaia-upload endpoint="/api/constaia" document="es_dni" lang="en"></constaia-upload>
<script type="module">
const el = document.querySelector("constaia-upload");
el.addEventListener("constaia:result", (e) => {
console.log(e.detail.id, e.detail.verdict?.status); // "valid" | "invalid" | "review"
});
el.addEventListener("constaia:error", (e) => console.warn(e.detail.code, e.detail.message));
</script>And the backend (Node, with the JavaScript SDK):
import express from "express";
import multer from "multer";
import { Constaia, ConstaiaError } from "@constaia/sdk";
const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
const constaia = new Constaia();
app.post("/api/constaia", upload.single("file"), async (req, res) => {
if (!req.file) return res.status(400).json({ error: { message: "The document is missing." } });
try {
const analysis = await constaia.analyze(req.file.buffer, {
filename: req.file.originalname,
expect: ["es_dni", "es_nie", "passport"], // decided by the server, not the browser
checks: { notExpired: true },
language: "en",
});
res.json(analysis);
} catch (err) {
const status = err instanceof ConstaiaError && err.status && err.status < 500 ? 422 : 502;
res.status(status).json({ error: { message: "We couldn't analyze the document." } });
}
});
app.listen(3000);Full framework guides in Integrations.
Contract with your backend
| Request | POST to endpoint, multipart/form-data, same origin with cookies (with-credentials for another origin). |
file field | The document. For two-sided cards, one JPEG with the front on top and the back below. |
options field | JSON with expect and language from the attributes. It is a hint: anyone can edit it. |
| Success response | 2xx with the analysis object as is (the widget uses verdict.status, verdict.reasons[].message, warnings and document.label). |
| Error response | Non-2xx status and { "error": { "message": "…" } }: the widget shows that message. |
Don't trust the browser
Your backend must set expect and checks (for example the holder from the authenticated user). Protect the
route with authentication and rate limiting, because every call spends credits. And don't trust a verdict that
comes back from the browser: keep the result your server received, or re-read it with GET /v1/analyses/{id}.
Attributes
| Attribute | Default | Description |
|---|---|---|
endpoint | — | Your backend URL. |
expect | — | Expected type(s), comma separated: es_dni or es_dni,passport. Sent in options. |
document | — | Type for the UI (frame, two sides). If there is no expect, it is also sent as expect. |
sides | auto | 2 asks for front and back (automatic for es_dni, es_nie, eu_id_card); 1 forces a single file. |
frame | auto | card shows the ID-1 frame (85.6 × 54 mm); none hides it. |
accept | JPEG, PNG, WEBP, HEIC, PDF | Accepted types (MIME, image/* or extensions). |
max-size-mb | 20 | Maximum size. |
auto-submit | true | false waits for the user (or submit()) after the quality check. |
with-credentials | off | Sends cookies to a cross-origin endpoint. |
lang | page lang or es | es, en, pt, fr. Also sent as options.language. |
theme | light | dark or auto (follows prefers-color-scheme). |
camera | auto | "Take photo" button: auto (touch screens), always, never. |
headers | — | Extra headers as JSON, for example a CSRF token. |
upload-url, session-token | — | Verification-links mode. Coming soon: the API does not issue these tokens yet. |
Properties and methods
Properties: the attributes in camelCase (maxSizeMb, autoSubmit…), plus headers (object), messages (texts),
and read-only status, result and error.
Methods: submit(), reset(), selectFile(file, side?) and open(camera?).
const el = document.querySelector("constaia-upload");
el.headers = { "X-CSRF-TOKEN": token };
el.reset();Events
All are CustomEvents with bubbles and composed.
| Event | detail | |
|---|---|---|
constaia:file | { file, side } | Cancelable: preventDefault() stops the widget from using the file. side: front, back or single. |
constaia:quality | { file, side, metrics, issues } | After the quality check. issues[].code: low_resolution, blurry, too_dark, too_bright. |
constaia:progress | { loaded, total, percent } | Upload progress. |
constaia:result | the analysis | Your backend's response, as is. |
constaia:error | { code, message, status? } | Validation, network, HTTP or configuration error (file_too_large, unsupported_file_type, network_error, http_error, invalid_response, missing_endpoint, secret_key_rejected, combine_failed). |
constaia:status | { status } | idle, checking, quality, ready, uploading, analyzing, done, error. |
Front and back
For two-sided cards the widget asks for the front, then the back, and merges them on the client into one JPEG
(front on top, back below, quality 0.9, max. 2000 px wide), because the API takes a single file.
- If the front is a PDF, the back is not requested: the PDF is sent as is (it should contain both sides).
- If the back is a PDF or the browser cannot decode an image (e.g. HEIC outside Safari), only the front is sent and a
notice is shown. The API may then return the
side_missingwarning.
Quality check
Before uploading an image, the widget downsizes it to 1024 px and measures the original resolution (short side under
600 px → low_resolution), blur (variance of the Laplacian), brightness and, for cards, glare. If there are issues,
the user sees Upload anyway and Retake. PDFs skip this step; the API runs its own checks.
Styling
constaia-upload {
--constaia-accent: #12b76a;
--constaia-accent-fg: #0b1220;
--constaia-bg: #fafaf7;
--constaia-fg: #0b1220;
--constaia-surface: #ffffff;
--constaia-muted: #5b6474;
--constaia-border: #d5d9df;
--constaia-radius: 12px;
--constaia-font: Inter, system-ui, sans-serif;
--constaia-valid: #12b76a;
--constaia-invalid: #e5484d;
--constaia-review: #f5a524;
--constaia-focus: #12b76a;
}
constaia-upload::part(dropzone) { border-style: solid; }Parts: container, dropzone, frame, button, button-primary, camera-button, preview, thumbnail,
quality, progress, progress-bar, result, verdict, reasons, warnings, note, error.
Texts
document.querySelector("constaia-upload").messages = {
dropTitle: "Upload your medical certificate",
verdictValid: "All good",
};Keys are those of LOCALES.es, exported by the package (dropTitle, dropHint, chooseFile, takePhoto,
verdictValid, verdictInvalid, verdictReview, tryAgain…).
React
"use client";
import { ConstaiaUpload, useConstaiaUpload } from "@constaia/widget/react";
export function VerifyId() {
return (
<ConstaiaUpload
endpoint="/api/constaia"
document="es_dni"
lang="en"
onResult={(analysis) => console.log(analysis.verdict?.status)}
onError={(error) => console.warn(error.code)}
/>
);
}
export function VerifyWithHook() {
const { ref, status, result, error, reset } = useConstaiaUpload({ endpoint: "/api/constaia", document: "es_dni" });
return (
<>
<ConstaiaUpload ref={ref} />
<p>{status} {result?.verdict?.status} {error?.message}</p>
<button type="button" onClick={reset}>Try again</button>
</>
);
}Props: the attributes in camelCase (endpoint, expect, document, sides, frame, accept, maxSizeMb,
autoSubmit, withCredentials, lang, theme, camera, messages, headers) and the callbacks onFile,
onQuality, onProgress, onResult, onError, onStatus. Works with React 18 and 19 and with server rendering
(the element is only registered in the browser).
Vue
<script setup lang="ts">
import { ConstaiaUpload } from "@constaia/widget/vue";
function onResult(analysis: { verdict?: { status: string } | null }) {
console.log(analysis.verdict?.status);
}
</script>
<template>
<ConstaiaUpload endpoint="/api/constaia" expect="medical_certificate_sport" lang="en" @result="onResult" />
</template>Events: @file, @quality, @progress, @result, @error, @status. To use the <constaia-upload> tag
directly, install ConstaiaPlugin and tell the compiler it is a custom element:
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("constaia-") } } });Svelte, Angular and others
Import @constaia/widget once (it registers the element) and use the tag. Listen to events with
addEventListener, because the names contain a colon (constaia:result). In Angular, add CUSTOM_ELEMENTS_SCHEMA
to the component. Examples in Svelte and Angular.
Next steps
Python SDK
Reference for the official constaia SDK for Python 3.9+: sync and async clients, inputs, options, pagination, errors, concurrency and webhooks.
MCP server for AI agents
Connect Constaia to Claude Desktop, Claude Code, Cursor, VS Code or Windsurf with @constaia/mcp to validate documents and extract data from an agent.