React
Add the Constaia widget to a React app with the ConstaiaUpload component and the useConstaiaUpload hook, uploading files to your own backend.
@constaia/widget/react is a thin wrapper over the <constaia-upload> web component: it captures the document (drag and drop, file picker or camera), checks image quality and uploads it to your backend. It works with React 18 and 19, and with server rendering.
You need a backend
React 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. If you use Next.js or React Router, follow Next.js or Remix and React Router; for a Vite SPA, build the backend with Express (or any of the other integrations).
Install
npm i @constaia/widgetWhat your backend must do
The widget sends POST multipart/form-data to endpoint with two fields:
| Field | Content |
|---|---|
file | The document. For DNI, NIE and EU ID cards, front and back merged into a single JPEG. |
options | JSON with expect and language taken from the attributes. Only a hint. |
Your backend must:
- Authenticate the user and limit attempts (every analysis spends credits).
- Call Constaia with
expectandchecksdecided on the server, not the ones the browser sends. - Return the analysis as-is (JSON) with status 200, or 202 if it is still queued.
- On error, return a non-2xx status with
{ "error": { "message": "…" } }: the widget shows that message.
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, have Vite forward /api to the backend to share origin and cookies:
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
server: {
proxy: { "/api": "http://localhost:3000" },
},
});The ConstaiaUpload component
import { ConstaiaUpload } from "@constaia/widget/react";
import { useState } from "react";
export function VerifyDocument() {
const [verdict, setVerdict] = useState<string | null>(null);
return (
<section>
<h1>Verify your ID</h1>
<ConstaiaUpload
endpoint="/api/constaia"
document="es_dni"
lang="en"
onResult={(analysis) => setVerdict(analysis.verdict?.status ?? null)}
onError={(error) => console.warn(error.code, error.message)}
onStatus={(status) => status === "idle" && setVerdict(null)}
/>
{verdict === "valid" && <a href="/signup/details">Continue</a>}
</section>
);
}Props are the widget attributes in camelCase plus event handlers:
| Prop | Description |
|---|---|
endpoint | Your backend URL. |
document | Document type for the UI (card frame, two sides). If expect is missing, it is also sent as expect. |
expect | Expected type or types (string or string[]), sent in options. |
sides, frame | 1 or 2 sides; card or none frame. Automatic for DNI, NIE and EU ID cards. |
accept, maxSizeMb | Accepted types and maximum size (20 MB by default). |
autoSubmit | false waits for the user (or submit()) after the quality check. |
withCredentials | Sends cookies to a cross-origin endpoint. |
lang, theme, camera | Language (es, en, pt, fr), theme (light, dark, auto) and camera button (auto, always, never). |
headers | Extra headers, for example a CSRF token. |
messages | Overrides UI texts. |
onFile, onQuality, onProgress, onResult, onError, onStatus | Widget events. onResult receives the analysis exactly as your backend returns it. |
The full reference of attributes, events and styling is in Widget.
The useConstaiaUpload hook
The hook exposes the widget state so you can build your own UI around it: ref, element, status, progress, result, error, reset() and submit(). It accepts the same attributes as the component, plus messages and headers.
import { ConstaiaUpload, useConstaiaUpload } from "@constaia/widget/react";
export function VerifyWithControls({ csrfToken }: { csrfToken: string }) {
const { ref, status, progress, result, error, reset, submit } = useConstaiaUpload({
endpoint: "/api/constaia",
document: "es_dni",
lang: "en",
autoSubmit: false,
headers: { "X-CSRF-Token": csrfToken },
});
const verdict = result?.verdict?.status;
return (
<section>
<ConstaiaUpload ref={ref} />
{status === "ready" && (
<button type="button" onClick={() => submit()}>
Send document
</button>
)}
{status === "uploading" && <progress value={progress} max={100} />}
{status === "analyzing" && <p>Analysing…</p>}
{verdict === "valid" && <a href="/signup/details">Continue</a>}
{verdict === "review" && <p>We could not read it well. Take another photo in good light, without glare.</p>}
{(verdict === "invalid" || error) && (
<button type="button" onClick={reset}>
Try another document
</button>
)}
</section>
);
}States are idle, checking, quality, ready, uploading, analyzing, done and error. If the analysis takes longer than 30 s, your backend returns the analysis as queued or processing and the widget shows a "queued" notice; the final result reaches your backend by webhook.
Do not trust the result in the browser
result 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}), never with a verdict the browser sends back.
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_*orREACT_APP_*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
maxSizeMbto 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 use
withCredentials, or pass a token throughheaders.
Next steps
Angular
Integrate Constaia in Angular 18+ with the custom element and CUSTOM_ELEMENTS_SCHEMA, or with HttpClient, always uploading to your backend (Express example).
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.