JavaScript and TypeScript SDK
Reference for @constaia/sdk on Node.js, Bun, Deno and edge runtimes. Installation, options, methods, errors, retries and webhook verification.
@constaia/sdk is the official SDK for JavaScript and TypeScript. It has no dependencies, uses native fetch and
runs on Node.js ≥ 18, Bun, Deno and edge runtimes (Vercel Edge, Netlify Edge). Current version: 0.2.0.
Server only
The key (ck_live_… or ck_test_…) is secret. If the SDK detects a browser with a Constaia key, the
constructor throws an error with code secret_key_in_browser. In the browser, use the
widget, which sends the file to your backend.
Installation
npm install @constaia/sdkThe package ships ESM and CommonJS builds with types included.
Configuration
import { Constaia } from "@constaia/sdk";
export const constaia = new Constaia({
apiKey: process.env.CONSTAIA_API_KEY, // default: process.env.CONSTAIA_API_KEY
baseUrl: "https://api.constaia.com", // default; or CONSTAIA_BASE_URL
timeout: 60_000, // ms per attempt
maxRetries: 2, // on 429, 408, 409 idempotency_in_progress, 5xx and network errors
maxConcurrency: 4, // requests in flight at once; the rest wait in a queue
});| Option | Default | Description |
|---|---|---|
apiKey | process.env.CONSTAIA_API_KEY | Secret key. Without one, the constructor throws. |
baseUrl | https://api.constaia.com (or CONSTAIA_BASE_URL) | Useful to target a local environment. |
timeout | 60000 | Maximum time per attempt, in milliseconds. |
maxRetries | 2 | Automatic retries on 429, 408, 409 idempotency_in_progress, 5xx (except 501) and network failures. 0 disables them. |
maxConcurrency | 4 | Requests in flight at once per client; the rest wait in a FIFO queue. |
fetch | globalThis.fetch | Custom fetch implementation (tests, proxies). |
defaultHeaders | {} | Headers added to every request. |
dangerouslyAllowBrowser | false | Allows using the key in a browser. Local experiments only. |
constaia.livemode tells you whether the key is ck_live_ (true) or ck_test_ (false).
Accepted files
analyze() and classify() accept:
| Input | Example |
|---|---|
File / Blob | await constaia.analyze(form.get("file") as File) |
Buffer / Uint8Array / ArrayBuffer | await constaia.analyze(buffer, { filename: "id.jpg" }) |
ReadableStream | await constaia.analyze({ file: stream, filename: "id.jpg" }) |
| Local path (Node, Bun, Deno) | await constaia.analyze(await fromPath("./id.jpg")) |
| Remote URL | await constaia.analyze({ fileUrl: "https://example.com/id.jpg" }) |
| Base64 | await constaia.analyze({ base64, filename: "id.jpg" }) |
fromPath is imported from @constaia/sdk/node and only exists where there is a file system. A bare string
throws: use { fileUrl } for URLs and fromPath() for paths.
Supported files: JPEG, PNG, WEBP, HEIC and PDF, up to 20 MB and 30 pages per synchronous analysis (200 with
async: true). In test mode the file name picks the response: pass filename when the input has no name. See
Test mode.
Options
In the SDK, options are camelCase and converted to the API's snake_case. Responses arrive exactly as the API
sends them (snake_case), like in the reference and in webhooks.
| SDK | API | Description |
|---|---|---|
expect | expect | Expected type or list of types. Without it there is no verdict. |
extract | extract | true (type template) or your own JSON Schema. |
checks.notExpired | checks.not_expired | Expiry. On by default for types with an expiry date. |
checks.referenceDate | checks.reference_date | Reference date YYYY-MM-DD. |
checks.maxAgeDays | checks.max_age_days | Maximum age of the issue date. |
checks.minAgeYears / maxAgeYears | min_age_years / max_age_years | Holder's age. |
checks.holder.fullName… | checks.holder.full_name… | Expected holder (fullName, firstName, lastName, documentNumber, birthDate). |
checks.requireFields | checks.require_fields | Required fields. |
checks.requireSignature / requireStamp | require_signature / require_stamp | Signature and stamp. |
checks.expectedAmount / expectedIban / expectedReference | expected_amount / expected_iban / expected_reference | Receipts and invoices. |
storage | storage | none, temporary or persistent. |
ttlHours | ttl_hours | Retention hours with temporary. |
keepResults | keep_results | false to not store the results. |
async | async | true to get a 202 and a webhook later. |
export | export | ["xlsx", "csv", …]. |
metadata | metadata | Sent as is (keys are not converted). |
language | language | Message language: es, en, pt, fr. |
filename | (file name) | Name when the input has none. |
Every API option is explained in POST /v1/analyze and Checks.
Analyze a document
import { Constaia } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia();
const analysis = await constaia.analyze(await fromPath("./dni_valid.jpg"), {
expect: ["es_dni", "es_nie", "passport"],
checks: {
notExpired: true,
minAgeYears: 18,
holder: { fullName: "María García López", documentNumber: "12345678Z" },
},
storage: "none",
metadata: { registration_id: "123" },
language: "en",
});
switch (analysis.verdict?.status) {
case "valid":
console.log("OK", analysis.fields.document_number?.value);
break;
case "invalid":
console.log(analysis.verdict.reasons.filter((r) => r.severity === "error").map((r) => r.message));
break;
case "review":
console.log("Manual review", analysis.warnings);
break;
}If the analysis takes longer than 30 seconds, the API answers 202 with status: "queued" or "processing" and
the result arrives by webhook. Check analysis.status === "completed" before reading the verdict.
Classify
classify() only detects the type (0.2 credits). It accepts expect, language, metadata and filename.
const c = await constaia.classify(await fromPath("./invoice.pdf"), { expect: "invoice" });
console.log(c.document?.type, c.candidates, c.verdict?.status);Stored analyses
const one = await constaia.analyses.get("an_01J…");
// One page (await) …
const page = await constaia.analyses.list({ limit: 50, status: "completed", metadata: { event: "42" } });
console.log(page.data.length, page.has_more);
// … or every page (for await)
for await (const a of constaia.analyses.list({ type: "es_dni" })) {
console.log(a.id, a.verdict?.status);
}
// The 200 most recent
const recent = await constaia.analyses.list().toArray(200);
await constaia.analyses.delete("an_01J…");list() returns a PagedList: await gives you the first page { object, data, has_more, url }; for await
walks every item (the SDK passes starting_after for you). See Pagination.
Download an export
import { writeFile } from "node:fs/promises";
const res = await constaia.analyses.export("an_01J…", "xlsx"); // Response
await writeFile("analysis.xlsx", Buffer.from(await res.arrayBuffer()));Batches
const batch = await constaia.batches.create({
items: [
{ fileUrl: "https://example.com/receipt-1.pdf" },
{ fileUrl: "https://example.com/receipt-2.pdf", options: { checks: { expectedAmount: 45 } } },
],
options: { expect: "payment_receipt", export: ["xlsx"] },
});
// Or uploading files (multipart files[])
const batch2 = await constaia.batches.create({
files: [await fromPath("./a.pdf"), await fromPath("./b.pdf")],
options: { expect: "invoice" },
});
const current = await constaia.batches.get(batch.id);Batches are always asynchronous: wait for the batch.completed webhook. Each items entry is
{ fileUrl, options? } or { base64, filename, options? }. See Batches.
Catalogue, balance and usage
const types = await constaia.documentTypes.list({ language: "en" }); // array
const dni = await constaia.documentTypes.get("es_dni", { language: "en" });
const balance = await constaia.balance();
console.log(balance.credits_available + balance.free_tier_remaining);
const usage = await constaia.usage({ from: "2026-09-01", to: "2026-09-30" });Webhook endpoints
const endpoint = await constaia.webhookEndpoints.create({
url: "https://example.com/webhooks/constaia",
events: ["analysis.completed", "analysis.review_required", "batch.completed"],
});
console.log(endpoint.secret); // whsec_… — only shown now: store it
const endpoints = await constaia.webhookEndpoints.list(); // array
const same = await constaia.webhookEndpoints.get(endpoint.id);
await constaia.webhookEndpoints.delete(endpoint.id);Verify webhooks
constaia.webhooks.verify() checks the Standard Webhooks signature (HMAC-SHA256, 5-minute tolerance) and returns
the parsed event. It is asynchronous (uses WebCrypto). Pass the raw body.
import { Constaia, WebhookVerificationError } from "@constaia/sdk";
const constaia = new Constaia();
export async function POST(request: Request) {
const raw = await request.text();
try {
const event = await constaia.webhooks.verify(raw, request.headers, process.env.CONSTAIA_WEBHOOK_SECRET!);
if (event.type === "analysis.completed") {
console.log(event.data.id, event.data.verdict?.status);
}
return new Response(null, { status: 204 });
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response("invalid signature", { status: 400 });
throw err;
}
}The standalone functions are exported too:
import { signWebhook, verifyWebhook } from "@constaia/sdk";
const event = await verifyWebhook(raw, headers, secret, { tolerance: 300 });
// In your tests: build signed headers like Constaia's
const body = JSON.stringify({ type: "analysis.completed", created_at: new Date().toISOString(), data: {} });
const signed = await signWebhook(body, "whsec_…");headers can be a Headers object, a plain object (like Express's req.headers) or anything with get(). More in
Webhooks.
Errors
Every class extends ConstaiaError, with status, type, code, param, requestId, headers and raw.
| Class | When |
|---|---|
InvalidRequestError | 400, 409, 413, 415, 422: any invalid_request error (unsupported_file_type, file_too_large, invalid_parameter…). |
AuthenticationError | 401: missing or invalid key. |
InsufficientCreditsError | 402: out of credits. |
PermissionError | 403. |
NotFoundError | 404: the analysis doesn't exist, was deleted or was created with keep_results: false. |
RateLimitError | 429, after retries are exhausted. retryAfter in seconds. |
APIError | 5xx, e.g. 503 live_mode_unavailable. |
APIConnectionError / APITimeoutError | Could not connect, or timed out. |
WebhookVerificationError | Invalid webhook signature. |
import {
ConstaiaError,
InsufficientCreditsError,
InvalidRequestError,
RateLimitError,
} from "@constaia/sdk";
try {
await constaia.analyze(file, { expect: "es_dni" });
} catch (err) {
if (err instanceof InvalidRequestError) console.log(err.code, err.param);
else if (err instanceof InsufficientCreditsError) alertBilling();
else if (err instanceof RateLimitError) console.log(`retry in ${err.retryAfter} s`);
else if (err instanceof ConstaiaError) console.log(err.status, err.type, err.requestId);
else throw err;
}Every code is listed in Errors.
Retries, timeouts and idempotency
- Retries automatically on 429, 408, 409
idempotency_in_progress, 5xx (never 501) and network errors, up tomaxRetriestimes. HonoursRetry-After(up to 60 s); otherwise waits with exponential backoff (0.5 s to 8 s, with jitter). - Every
POSTcarries a randomIdempotency-Key, the same across its retries, so a retry never charges twice. - Every method accepts a last
RequestOptionsargument:
const controller = new AbortController();
await constaia.analyze(file, { expect: "es_dni" }, {
idempotencyKey: "registration-123-id", // your own, to dedupe across processes
timeout: 90_000,
maxRetries: 4,
signal: controller.signal,
headers: { "X-Trace": "abc" },
});With maxConcurrency (default 4) the client never has more requests in flight than that; the rest wait in a queue.
After each response, constaia.lastRateLimit holds the limit headers (limit, remaining, reset, policy,
retryAfter) and constaia.lastRequestId the X-Request-Id. With several processes, the per-key limit is shared.
See Rate limits.
TypeScript types
The package exports response types (Analysis, Classification, Batch, Balance, Usage, WebhookEvent…)
and option types (AnalyzeOptions, Checks). DocumentType, ReasonCode and WarningCode autocomplete known
values and accept any string, so your code keeps working when new types or codes are added. Treat
reasons[].code as a string and compare it with the stable codes in Verdicts.
Next steps
API reference
Interactive reference and OpenAPI 3.1 specification of the Constaia API, /v1 conventions, client generation and import into Postman, Insomnia or Bruno.
PHP SDK
Reference for constaia/constaia-php on PHP 8.1+: Composer install, inputs, options, methods, pagination, exceptions, retries and webhooks.