Constaia

Quickstart

Validate your first Spanish ID card with the Constaia API in 5 minutes using a free test key, with curl, JavaScript, PHP or Python.

In five minutes you will create an account, get a test key and run real analyses against the API without spending credits. Then you will see how to go live.

1. First analysis

Create an account

Sign up at app.constaia.com/signup with your email or your Google or GitHub account (then your email is verified straight away). The free plan includes 150 credits per month and no card is required. Credits renew on the 1st (UTC). More in Pricing.

Just want to see what it returns? The website demo analyses a document with no account or key (5 a day, without storing the file).

Copy your test key

In the dashboard, go to API keys and copy the key that starts with ck_test_. Keys are shown only once: store it in your secrets manager.

Test keys never spend credits and return deterministic results based on the file name. Everything about this mode in Test mode.

Export the key as an environment variable

Terminal
export CONSTAIA_API_KEY="ck_test_..."

The official SDKs read CONSTAIA_API_KEY automatically. The key always stays on your server: never put it in browser code or a mobile app.

Prepare a test file

In test mode the result is chosen by the file name, but the content must be a real JPEG, PNG, WEBP, HEIC or PDF (the type is detected from the first bytes). Copy any photo and any PDF you have at hand with these names:

Terminal
cp ~/Pictures/any-photo.jpg dni_valid.jpg
cp dni_valid.jpg dni_expired.jpg
cp dni_valid.jpg blurry.jpg
cp dni_valid.jpg nie.jpg
cp dni_valid.jpg passport.jpg
cp ~/Documents/any-document.pdf medical_certificate.pdf

A PDF's signature and metadata are analysed for real even in test mode: if your PDF was produced by Word, Canva or another editor, you'll see the edited_suspected warning and a review verdict. If that happens, use cp dni_valid.jpg medical_certificate.jpg. See Test mode.

Analyse an ID card

Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@dni_valid.jpg \
  -F 'options={"expect":"es_dni"}'

Response (fields trimmed; size_bytes and timestamps depend on your file and the moment):

200 OK
{
  "id": "an_01J...",
  "object": "analysis",
  "status": "completed",
  "livemode": false,
  "created_at": "2026-09-29T10:00:00Z",
  "completed_at": "2026-09-29T10:00:02Z",
  "file": { "name": "dni_valid.jpg", "mime_type": "image/jpeg", "pages": 1, "size_bytes": 482133 },
  "document": { "type": "es_dni", "label": "DNI (España)", "confidence": 0.97, "side": "both", "country": "ESP" },
  "verdict": {
    "expected": ["es_dni"],
    "match": true,
    "status": "valid",
    "reasons": [
      { "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
      { "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." }
    ]
  },
  "fields": {
    "document_number": {
      "value": "12345678Z",
      "confidence": 0.99,
      "validated": true,
      "source": { "page": 1, "bbox": [0.61, 0.12, 0.83, 0.16] }
    }
  },
  "checks": [
    { "code": "nif_check_digit", "passed": true, "message": "La letra del documento 12345678Z es correcta." },
    { "code": "mrz_checksums", "passed": true, "message": "Los dígitos de control de la MRZ son correctos." },
    { "code": "mrz_matches_visual", "passed": true, "message": "La MRZ coincide con los datos impresos." }
  ],
  "warnings": [],
  "signature": null,
  "exports": {},
  "storage": { "mode": "none", "file_deleted_at": "2026-09-29T10:00:02Z", "expires_at": null },
  "processing": {
    "profile": "sovereign",
    "region": "eu",
    "mode": "vlm",
    "providers": [{ "name": "mock", "region": "local", "role": "llm", "model": "mock-llm-1" }]
  },
  "usage": { "credits": 0, "pages": 1 },
  "metadata": {}
}

Messages default to Spanish. Add "language":"en" to the options to get "The document is Spanish ID card (DNI)." and "Valid until 12/03/2031.". Besides document_number, fields includes first_name (MARÍA), last_name_1 (GARCÍA), last_name_2 (LÓPEZ), sex, nationality, birth_date (1990-05-14), issue_date, expiry_date (2031-03-12), support_number, address, birth_place, parents and mrz, all with the same structure.

Note usage.credits: 0 (test mode) and storage.mode: "none": the file was not stored. signature is null because it is not a PDF, and processing tells you which providers handled it (in test mode, the simulated mock).

2. Try other outcomes

Change only the file name to see each verdict. We show verdict and warnings; the rest of the response has the same shape.

Expired ID card: invalid

Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@dni_expired.jpg \
  -F 'options={"expect":"es_dni"}'
verdict
{
  "expected": ["es_dni"],
  "match": true,
  "status": "invalid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
    { "code": "not_expired", "severity": "error", "message": "Caducado el 15/06/2020." }
  ]
}

The code is the same (not_expired) as for the valid card; what changes is the severity. A reason with severity error makes the verdict No válido. With "language":"en" the message is "Expired on 15/06/2020.".

Blurry photo: review

Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@blurry.jpg \
  -F 'options={"expect":"es_dni"}'
verdict and warnings
{
  "verdict": {
    "expected": ["es_dni"],
    "match": true,
    "status": "review",
    "reasons": [
      { "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
      { "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." },
      { "code": "low_quality", "severity": "warning", "message": "La calidad de la imagen es insuficiente (blurry, low_quality)." }
    ]
  },
  "warnings": ["blurry", "low_quality"]
}

A warning reason gives Revisar: ask the user for another photo or send it to manual review.

Several accepted types: NIE

expect accepts a list. Useful when you accept a DNI, NIE or passport in the same field:

Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@nie.jpg \
  -F 'options={"expect":["es_dni","es_nie","passport"]}'
document and verdict
{
  "document": { "type": "es_nie", "label": "NIE / TIE (España)", "confidence": 0.97, "side": "both", "country": "ESP" },
  "verdict": {
    "expected": ["es_dni", "es_nie", "passport"],
    "match": true,
    "status": "valid",
    "reasons": [
      { "code": "type_match", "severity": "info", "message": "El documento es NIE / TIE (España)." },
      { "code": "not_expired", "severity": "info", "message": "Vigente hasta el 30/11/2029." }
    ]
  }
}

The number is in fields.nie_number (X1234567L). Each type has its own fields: look them up in the catalogue or with GET /v1/document-types/es_nie.

Passport

Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@passport.jpg \
  -F 'options={"expect":"passport"}'

Returns valid with document.type: "passport", fields.document_number PAA123456, expiry 2032-06-01 and the mrz_checksums and mrz_matches_visual checks passed.

Medical certificate with rules

checks add business rules. Here we require the certificate to be at most one year old and signed:

Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@medical_certificate.pdf \
  -F 'options={"expect":"medical_certificate_sport","checks":{"max_age_days":365,"require_signature":true}}'
verdict
{
  "expected": ["medical_certificate_sport"],
  "match": true,
  "status": "valid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "El documento es Certificado médico deportivo." },
    { "code": "max_age_days", "severity": "info", "message": "Emitido hace 28 días (máximo 365)." },
    { "code": "require_signature", "severity": "info", "message": "El documento está firmado." }
  ]
}

The test certificate is issued on 2026-09-01, so the number of days in the message depends on the date you run it. All available rules are in Checks.

3. From your code

The same call with the SDKs or plain HTTP. Always handle errors: the API returns { "error": { "type", "code", "message", "request_id" } } (see Errors).

analyze.sh
#!/usr/bin/env bash
set -euo pipefail

curl --fail-with-body -sS https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@dni_valid.jpg \
  -F 'options={"expect":"es_dni","checks":{"min_age_years":18},"language":"en"}'

A synchronous analysis waits up to 30 seconds. If it has not finished, the API responds 202 with status: "queued" or "processing" and the result arrives by webhook (or by polling GET /v1/analyses/{id}). With short documents in test mode you will always get 200.

4. Go live

Verify your email

ck_live_ keys can only be created with a verified email, and free credits in live mode require it too (otherwise 402 email_not_verified). If you signed up with Google or GitHub, it already is.

Create a ck_live_ key

In the dashboard → API keys, create a production key. It is shown only once. Live analyses consume credits: first the month's 150 free credits and then those from the packs you buy. See Credits and billing.

Mind the free plan limits: 2 requests per second, 2 concurrent synchronous analyses and 60 pages per minute (10, 10 and 600 once you buy a pack). If you exceed them you get 429 with Retry-After; the SDKs already retry. See Rate limits.

Swap the environment variable

No code changes needed: replace the value of CONSTAIA_API_KEY in your production environment with the ck_live_ key. Each mode's analyses are separate: listing with a live key only shows live analyses.

Set up production webhooks

Webhook endpoints belong to the mode of the key you create them with. Create your endpoints again with the live key; see Webhooks.

live_mode_unavailable

Live keys never get simulated results. If the environment has no real AI providers available, live calls fail with 503 live_mode_unavailable. Meanwhile, keep developing with your ck_test_ key.

Next steps

On this page