Constaia

Test mode

How Constaia ck_test_ keys work, what each test file returns and how to write automated tests with Vitest, PHPUnit or pytest without spending credits.

ck_test_... keys call the same API, with the same routes, option validation and response format, but instead of analysing the document with AI they return a deterministic result chosen by the file name. Use them for development, CI and demos without spending credits.

Test vs live

Test mode (ck_test_...)Live mode (ck_live_...)
ResultSimulated and deterministic, based on the file nameReal AI analysis
CreditsNever reserved or consumed (usage.credits: 0)Reserved at start, charged at the end
livemode in the responsefalsetrue
Option validation, errors, idempotencySame as live—
LimitsSame requests per second as your tier (2 req/s free, 10 paid), no page or concurrency limitYour tier's requests/s, concurrent analyses and pages/min
Deterministic checks (nif_check_digit, MRZ, IBAN…)Really run on the simulated dataRun on the extracted data
Analyses, listings and usageYou only see test onesYou only see live ones
WebhooksOnly endpoints created with a test key receive themOnly endpoints created with a live key receive them
PDF signature and metadataVerified for real on the file you sendSame
RequirementNoneVerified email

The checks you ask for (holder, min_age_years, max_age_days, expected_amount…) are evaluated on the simulated data exactly as in live, so you can test error cases too.

How the result is chosen

  1. The file name is taken without extension, lowercased.
  2. If it matches a scenario exactly (dni_valid, dni_expired, nie, passport, medical_certificate, sexual_offences_certificate, payment_receipt, invoice, blurry), that one is used.
  3. Otherwise keywords are searched in this order: blur/borros, expired/caducad, nie/tie, passport/pasaporte, medical/medico/médico, sexual/delitos/penales, receipt/justificante/transfer, invoice/factura, dni. The first one found wins: dni_blurry.jpg gives the blurry scenario, and dni_expired_2.png, dni_expired.
  4. If none matches, the document is generic.

The content must be a real JPEG, PNG, WEBP, HEIC or PDF file: the type is detected from the first bytes, not from the extension. Any photo or PDF will do. You don't need one file per scenario: the name that counts is the one you send in the request (the multipart filename), so you can reuse the same file with different names.

Scenarios

NameAlso matchesTypical expectResult
dni_validdnies_dniVálido MARÍA GARCÍA LÓPEZ, 12345678Z, born 1990-05-14, expires 2031-03-12.
dni_expiredexpired, caducades_dniNo válido JUAN PÉREZ SÁNCHEZ, 87654321X, expired 2020-06-15: not_expired with severity error, "Caducado el 15/06/2020." ("Expired on 15/06/2020." in English)
nienie, tiees_nieVálido ANNA KOWALSKA, X1234567L, expires 2029-11-30.
passportpassport, pasaportepassportVálido Passport (TD3) MARIA GARCIA LOPEZ, PAA123456, expires 2032-06-01.
medical_certificatemedical, medico, médicomedical_certificate_sportVálido Fit, signed and stamped, issued 2026-09-01.
sexual_offences_certificatesexual, delitos, penaleses_sexual_offences_certificateVálido No records, CSV MJU4-7K2P-9QXA-3ZTR, issued 2026-09-15.
payment_receiptreceipt, justificante, transferpayment_receiptVálido 45 EUR, beneficiary IBAN ES7921000813610123456789, reference INSCRIPCION 123.
invoiceinvoice, facturainvoiceVálido Invoice 20260042, base 100, VAT 21 %, total 121 EUR; invoice_totals passed.
blurryblur, borroses_dniRevisar Same ID card as dni_valid with warnings: ["blurry", "low_quality"] and the low_quality reason with severity warning.
anything else——Type generic with no fields. Without expect, verdict is null. With expect, reason type_unknown (warning) and Revisar.

There are no scenarios for US types or the rest of the catalogue: a driver's license or a W-9 in test mode returns generic and, with expect, review. To see those types, use a live key (the month's 150 free credits are enough).

PDFs are analysed for real

The electronic signature and metadata of a PDF are not simulated: they are read from the file you send. So:

  • An unsigned PDF with expect: "es_sexual_offences_certificate" (a type that is downloaded signed) adds the signature_missing reason with warning and the verdict becomes review. With the original PDF downloaded from the e-government site you will see signature_valid.
  • A PDF produced with Word, LibreOffice, Canva, iLovePDF or another known editor adds edited_suspected (warning) and also leads to review.

To get exactly the verdicts in this table, use an image (JPEG or PNG) with the scenario name, or a PDF exported by a tool that is not an editor. See Digital signatures in PDF.

Messages with relative dates, such as "Emitido hace 28 días (máximo 365)." ("Issued 28 days ago (maximum 365)."), depend on the day you run the test.

Response examples

Forcing error cases

Besides dni_expired and blurry, you can trigger other outcomes by combining a file with options:

What you want to testFileOptionsResult
Wrong typepassport.jpg{"expect":"es_dni"}invalid, reason type_mismatch with severity error ("Expected Spanish ID card (DNI), but the document looks like Passport." with language: "en")
Different holderdni_valid.jpg{"expect":"es_dni","checks":{"holder":{"full_name":"Juan Pérez"}}}invalid, reason holder with severity error
Too youngdni_valid.jpg{"expect":"es_dni","checks":{"min_age_years":40}}invalid, reason min_age_years with severity error (the holder was born in 1990)
Old certificatemedical_certificate.pdf{"expect":"medical_certificate_sport","checks":{"max_age_days":7}}invalid, reason max_age_days with severity error (issued 2026-09-01)
Different amountpayment_receipt.pdf{"expect":"payment_receipt","checks":{"expected_amount":50}}invalid, reason expected_amount with severity error
Unrecognised documentother.jpg{"expect":"es_dni"}review, reason type_unknown with severity warning

Messages in another language

language changes the language of reasons[].message, checks messages and document.label. For example, dni_valid.jpg with {"expect":"es_dni","checks":{"holder":{"full_name":"Juan Pérez"},"min_age_years":18},"language":"en"}:

verdict
{
  "expected": ["es_dni"],
  "match": true,
  "status": "invalid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "The document is Spanish ID card (DNI)." },
    { "code": "not_expired", "severity": "info", "message": "Valid until 12/03/2031." },
    { "code": "age", "severity": "info", "message": "The holder is 36 years old." },
    { "code": "holder", "severity": "error", "message": "Holder mismatch: full_name is “MARÍA GARCÍA LÓPEZ”, expected “Juan Pérez”." }
  ]
}

Codes don't change with the language: always code against code and severity.

Automated tests

Recommendations so your tests don't break:

  • Use a ck_test_ key dedicated to CI, in its own environment variable (for example CONSTAIA_TEST_API_KEY), and check it starts with ck_test_ before running anything.
  • Assert only on verdict.status, the code and severity of verdict.reasons, and warnings. Don't compare messages (they depend on the language and, in some cases, on the date), nor id, timestamps or size_bytes.
  • Keep a single small JPEG and a single small PDF in the repository and send them with different names.
test/constaia.test.ts
import { beforeAll, describe, expect, it } from "vitest";
import { Constaia, type Analysis } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const apiKey = process.env.CONSTAIA_TEST_API_KEY ?? "";
const constaia = new Constaia({ apiKey });

// Same real file, different name: the name picks the scenario.
const jpeg = (name: string) => fromPath("test/fixtures/sample.jpg", name);

const codes = (a: Analysis, severity?: string): string[] =>
  (a.verdict?.reasons ?? []).filter((r) => !severity || r.severity === severity).map((r) => r.code);

describe("ID card validation", () => {
  beforeAll(() => {
    if (!apiKey.startsWith("ck_test_")) throw new Error("CONSTAIA_TEST_API_KEY must be a ck_test_ key");
  });

  it("accepts a valid ID card", async () => {
    const analysis = await constaia.analyze(await jpeg("dni_valid.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("valid");
    expect(codes(analysis)).toContain("type_match");
    expect(analysis.warnings).toEqual([]);
  });

  it("rejects an expired ID card", async () => {
    const analysis = await constaia.analyze(await jpeg("dni_expired.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("invalid");
    expect(codes(analysis, "error")).toContain("not_expired");
  });

  it("sends a blurry photo to review", async () => {
    const analysis = await constaia.analyze(await jpeg("blurry.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("review");
    expect(analysis.warnings).toEqual(expect.arrayContaining(["blurry", "low_quality"]));
    expect(codes(analysis, "warning")).toContain("low_quality");
  });

  it("rejects a passport when an ID card is expected", async () => {
    const analysis = await constaia.analyze(await jpeg("passport.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("invalid");
    expect(codes(analysis, "error")).toContain("type_mismatch");
  });
});

If you run many tests in parallel, remember test mode has the same requests-per-second limit as your tier (2 req/s on the free plan): the SDKs retry 429s honouring Retry-After, but you should limit concurrency. See Rate limits.

Webhooks in test mode

Each webhook endpoint has the mode of the key it was created with (mode: "test" or "live") and only receives events of that mode. To test webhooks during development:

  1. Create the endpoint with your ck_test_ key (POST /v1/webhook-endpoints) pointing at a public HTTPS URL (for example, a tunnel to your machine).
  2. Run analyses with async: true or batches with the test key: you will receive analysis.completed, analysis.review_required (with blurry.jpg), batch.completed…
  3. From the dashboard you can also send a test event (type: "test") and see the last 100 deliveries.

credits.low only exists in live mode. For unit tests of your receiver without network, generate signed headers with signWebhook (JavaScript SDK), Webhook::headers (PHP SDK) or constaia.webhooks.sign (Python SDK). See Webhooks.

When you go live

Live keys never get simulated results. If the environment has no real AI providers available, ck_live_ calls fail with 503 live_mode_unavailable (APIError in the JavaScript SDK). Keep using the test key meanwhile. The steps to go live are in the quickstart.

Next steps

On this page