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_...) | |
|---|---|---|
| Result | Simulated and deterministic, based on the file name | Real AI analysis |
| Credits | Never reserved or consumed (usage.credits: 0) | Reserved at start, charged at the end |
livemode in the response | false | true |
| Option validation, errors, idempotency | Same as live | — |
| Limits | Same requests per second as your tier (2 req/s free, 10 paid), no page or concurrency limit | Your tier's requests/s, concurrent analyses and pages/min |
Deterministic checks (nif_check_digit, MRZ, IBAN…) | Really run on the simulated data | Run on the extracted data |
| Analyses, listings and usage | You only see test ones | You only see live ones |
| Webhooks | Only endpoints created with a test key receive them | Only endpoints created with a live key receive them |
| PDF signature and metadata | Verified for real on the file you send | Same |
| Requirement | None | Verified 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
- The file name is taken without extension, lowercased.
- 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. - 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.jpggives theblurryscenario, anddni_expired_2.png,dni_expired. - 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
| Name | Also matches | Typical expect | Result |
|---|---|---|---|
dni_valid | dni | es_dni | Válido MARÍA GARCÍA LÓPEZ, 12345678Z, born 1990-05-14, expires 2031-03-12. |
dni_expired | expired, caducad | es_dni | No 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) |
nie | nie, tie | es_nie | Válido ANNA KOWALSKA, X1234567L, expires 2029-11-30. |
passport | passport, pasaporte | passport | Válido Passport (TD3) MARIA GARCIA LOPEZ, PAA123456, expires 2032-06-01. |
medical_certificate | medical, medico, médico | medical_certificate_sport | Válido Fit, signed and stamped, issued 2026-09-01. |
sexual_offences_certificate | sexual, delitos, penales | es_sexual_offences_certificate | Válido No records, CSV MJU4-7K2P-9QXA-3ZTR, issued 2026-09-15. |
payment_receipt | receipt, justificante, transfer | payment_receipt | Válido 45 EUR, beneficiary IBAN ES7921000813610123456789, reference INSCRIPCION 123. |
invoice | invoice, factura | invoice | Válido Invoice 20260042, base 100, VAT 21 %, total 121 EUR; invoice_totals passed. |
blurry | blur, borros | es_dni | Revisar 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 thesignature_missingreason withwarningand the verdict becomesreview. With the original PDF downloaded from the e-government site you will seesignature_valid. - A PDF produced with Word, LibreOffice, Canva, iLovePDF or another known editor adds
edited_suspected(warning) and also leads toreview.
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 test | File | Options | Result |
|---|---|---|---|
| Wrong type | passport.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 holder | dni_valid.jpg | {"expect":"es_dni","checks":{"holder":{"full_name":"Juan Pérez"}}} | invalid, reason holder with severity error |
| Too young | dni_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 certificate | medical_certificate.pdf | {"expect":"medical_certificate_sport","checks":{"max_age_days":7}} | invalid, reason max_age_days with severity error (issued 2026-09-01) |
| Different amount | payment_receipt.pdf | {"expect":"payment_receipt","checks":{"expected_amount":50}} | invalid, reason expected_amount with severity error |
| Unrecognised document | other.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"}:
{
"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 exampleCONSTAIA_TEST_API_KEY), and check it starts withck_test_before running anything. - Assert only on
verdict.status, thecodeandseverityofverdict.reasons, andwarnings. Don't compare messages (they depend on the language and, in some cases, on the date), norid, timestamps orsize_bytes. - Keep a single small JPEG and a single small PDF in the repository and send them with different names.
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:
- 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). - Run analyses with
async: trueor batches with the test key: you will receiveanalysis.completed,analysis.review_required(withblurry.jpg),batch.completed… - 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
What to use
When to use analyze, classify, batches, the widget, the MCP server or a no-code tool in Constaia, with the cost, latency and limits of each option.
Use cases
End-to-end use-case guides for sign-up IDs, medical and LOPIVI certificates, receipts, invoices to Excel, batches, human review and US documents.