Constaia

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.

Esta página ainda não está traduzida para o seu idioma. Mostramos a versão em inglês.

@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 in es, 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/widget
import "@constaia/widget"; // registers <constaia-upload>

How it works

  1. 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.
  2. The widget checks quality (resolution, blur, brightness) and warns before uploading.
  3. It sends POST multipart/form-data to your endpoint with two fields: file and options.
  4. Your backend calls POST /v1/analyze, deciding itself expect and checks, and returns the analysis as JSON.
  5. The widget shows the verdict, reasons and warnings, and fires constaia:result.

Minimal example

index.html
<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):

server.ts
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

RequestPOST to endpoint, multipart/form-data, same origin with cookies (with-credentials for another origin).
file fieldThe document. For two-sided cards, one JPEG with the front on top and the back below.
options fieldJSON with expect and language from the attributes. It is a hint: anyone can edit it.
Success response2xx with the analysis object as is (the widget uses verdict.status, verdict.reasons[].message, warnings and document.label).
Error responseNon-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

AttributeDefaultDescription
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.
sidesauto2 asks for front and back (automatic for es_dni, es_nie, eu_id_card); 1 forces a single file.
frameautocard shows the ID-1 frame (85.6 × 54 mm); none hides it.
acceptJPEG, PNG, WEBP, HEIC, PDFAccepted types (MIME, image/* or extensions).
max-size-mb20Maximum size.
auto-submittruefalse waits for the user (or submit()) after the quality check.
with-credentialsoffSends cookies to a cross-origin endpoint.
langpage lang or eses, en, pt, fr. Also sent as options.language.
themelightdark or auto (follows prefers-color-scheme).
cameraauto"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.

Eventdetail
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:resultthe analysisYour 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_missing warning.

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

VerifyId.tsx
"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

VerifyId.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:

vite.config.ts
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

Nesta página