Constaia
SDKs

JavaScript and TypeScript SDK

Reference for @constaia/sdk on Node.js, Bun, Deno and edge runtimes. Installation, options, methods, errors, retries and webhook verification.

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

@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/sdk

The package ships ESM and CommonJS builds with types included.

Configuration

constaia.ts
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
});
OptionDefaultDescription
apiKeyprocess.env.CONSTAIA_API_KEYSecret key. Without one, the constructor throws.
baseUrlhttps://api.constaia.com (or CONSTAIA_BASE_URL)Useful to target a local environment.
timeout60000Maximum time per attempt, in milliseconds.
maxRetries2Automatic retries on 429, 408, 409 idempotency_in_progress, 5xx (except 501) and network failures. 0 disables them.
maxConcurrency4Requests in flight at once per client; the rest wait in a FIFO queue.
fetchglobalThis.fetchCustom fetch implementation (tests, proxies).
defaultHeaders{}Headers added to every request.
dangerouslyAllowBrowserfalseAllows 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:

InputExample
File / Blobawait constaia.analyze(form.get("file") as File)
Buffer / Uint8Array / ArrayBufferawait constaia.analyze(buffer, { filename: "id.jpg" })
ReadableStreamawait constaia.analyze({ file: stream, filename: "id.jpg" })
Local path (Node, Bun, Deno)await constaia.analyze(await fromPath("./id.jpg"))
Remote URLawait constaia.analyze({ fileUrl: "https://example.com/id.jpg" })
Base64await 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.

SDKAPIDescription
expectexpectExpected type or list of types. Without it there is no verdict.
extractextracttrue (type template) or your own JSON Schema.
checks.notExpiredchecks.not_expiredExpiry. On by default for types with an expiry date.
checks.referenceDatechecks.reference_dateReference date YYYY-MM-DD.
checks.maxAgeDayschecks.max_age_daysMaximum age of the issue date.
checks.minAgeYears / maxAgeYearsmin_age_years / max_age_yearsHolder's age.
checks.holder.fullName…checks.holder.full_name…Expected holder (fullName, firstName, lastName, documentNumber, birthDate).
checks.requireFieldschecks.require_fieldsRequired fields.
checks.requireSignature / requireStamprequire_signature / require_stampSignature and stamp.
checks.expectedAmount / expectedIban / expectedReferenceexpected_amount / expected_iban / expected_referenceReceipts and invoices.
storagestoragenone, temporary or persistent.
ttlHoursttl_hoursRetention hours with temporary.
keepResultskeep_resultsfalse to not store the results.
asyncasynctrue to get a 202 and a webhook later.
exportexport["xlsx", "csv", …].
metadatametadataSent as is (keys are not converted).
languagelanguageMessage 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

verify-id.ts
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.

app/api/webhooks/constaia/route.ts
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.

ClassWhen
InvalidRequestError400, 409, 413, 415, 422: any invalid_request error (unsupported_file_type, file_too_large, invalid_parameter…).
AuthenticationError401: missing or invalid key.
InsufficientCreditsError402: out of credits.
PermissionError403.
NotFoundError404: the analysis doesn't exist, was deleted or was created with keep_results: false.
RateLimitError429, after retries are exhausted. retryAfter in seconds.
APIError5xx, e.g. 503 live_mode_unavailable.
APIConnectionError / APITimeoutErrorCould not connect, or timed out.
WebhookVerificationErrorInvalid 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 to maxRetries times. Honours Retry-After (up to 60 s); otherwise waits with exponential backoff (0.5 s to 8 s, with jitter).
  • Every POST carries a random Idempotency-Key, the same across its retries, so a retry never charges twice.
  • Every method accepts a last RequestOptions argument:
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

Nesta página