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.
@constaia/widget/vue 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, which is the one that calls Constaia.
You need a backend
Vue runs in the browser and the API key can never be there. The widget uploads the file to an endpoint of yours (/api/constaia). If you use Nuxt, follow Nuxt; for a Vite SPA, build the backend 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.
Vite configuration
The proxy shares origin and cookies with the backend in development. isCustomElement is only needed if you use the <constaia-upload> tag directly in templates (not required with the wrapper, but harmless).
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: { isCustomElement: (tag) => tag.startsWith("constaia-") },
},
}),
],
server: {
proxy: { "/api": "http://localhost:3000" },
},
});The ConstaiaUpload component
<script setup lang="ts">
import { ConstaiaUpload } from "@constaia/widget/vue";
import type { Analysis, WidgetErrorDetail, WidgetStatus } from "@constaia/widget/vue";
import { ref } from "vue";
const upload = ref<{ reset: () => void } | null>(null);
const verdict = ref<string | null>(null);
const status = ref<WidgetStatus>("idle");
function onResult(analysis: Analysis) {
verdict.value = analysis.verdict?.status ?? null;
}
function onError(error: WidgetErrorDetail) {
console.warn(error.code, error.message);
}
function retry() {
upload.value?.reset();
verdict.value = null;
}
</script>
<template>
<h1>Verify your ID</h1>
<ConstaiaUpload
ref="upload"
endpoint="/api/constaia"
document="es_dni"
lang="en"
@result="onResult"
@error="onError"
@status="(s: WidgetStatus) => (status = s)"
/>
<p v-if="status === 'analyzing'">Analysing…</p>
<a v-if="verdict === 'valid'" href="/signup/details">Continue</a>
<p v-else-if="verdict === 'review'">We could not read it well. Take another photo in good light, without glare.</p>
<button v-else-if="verdict === 'invalid'" type="button" @click="retry">Try another document</button>
</template>Wrapper props: endpoint, expect (string or string[]), document, sides, frame, accept, maxSizeMb, autoSubmit (true by default), withCredentials, lang, theme, camera, messages and headers. Events: @file, @quality, @progress, @result, @error and @status (the latter receives the state directly: idle, checking, quality, ready, uploading, analyzing, done or error). Through ref it exposes reset(), submit() and element.
To send a CSRF or session token, use the headers prop, for example :headers="{ 'X-CSRF-Token': csrfToken }".
The plugin and the raw tag
If you prefer <constaia-upload> in your templates without the wrapper, install ConstaiaPlugin (it registers the element) and keep isCustomElement in the Vite config.
import { ConstaiaPlugin } from "@constaia/widget/vue";
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).use(ConstaiaPlugin).mount("#app");<script setup lang="ts">
import type { Analysis } from "@constaia/widget/vue";
import { ref } from "vue";
const verdict = ref<string | null>(null);
function onResult(event: Event) {
verdict.value = (event as CustomEvent<Analysis>).detail.verdict?.status ?? null;
}
</script>
<template>
<constaia-upload endpoint="/api/constaia" document="es_dni" lang="en" @constaia:result="onResult" />
<p v-if="verdict">Result: {{ verdict }}</p>
</template>With the raw tag the events are the web component's (constaia:result, constaia:error…) and the data arrives in event.detail.
Do not trust the result in the browser
The verdict Vue 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
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.