Agents
Integrating an AI agent directly against the Aletheia API
This page describes stable protocol behaviour for an AI agent integrating programmatically against Aletheia, without the MCP server (see MCP server if you want typed tool calls instead). The authoritative machine-readable contract is the published OpenAPI document; request/response schemas, security schemes, and error bodies are defined there.
Authentication
Two auth modes exist; which one applies is a server-side deployment setting.
Mode A, production multi-tenant. Tenant API key as bearer token:
Authorization: Bearer fsk_<8-char-prefix>_<secret>. The tenant identity is derived from the
key, not from a header, X-Tenant-ID is ignored in this mode. Keys are issued by IDCanopy
(no self-service). Optional mTLS is available at the connection level; see
mTLS.
Mode B, local/compat single-tenant. A shared service bearer token paired with
X-Tenant-ID: <tenant-slug>, honoured only in this mode. Provided by IDCanopy for the relevant
environment; never commit or log it.
On failure the middleware returns 401 with {"detail": "authentication required"} and no
enumeration of which check failed.
Ingestion paths
See Submitting a case for the complete
overview. The stateless POST /analyze path is the simplest for one-off checks: 1 to 15 files as
multipart/form-data, an optional applicant_id label, an optional case_meta JSON string.
Synchronous ForensicResult on 200. No applicant state is persisted and no outbound webhook
fires for this path.
The stateful /v1/ lifecycle persists an applicant and its documents across three calls
(create, upload, analyze), then lets you fetch findings later. A duplicate applicant_id within
a tenant returns 409; analyzing before any document is uploaded also returns 409.
ForensicResult shape
{
"applicant_id": "app-001",
"verdict": {
"verdict": "REJECT", // REJECT | ESCALATE | PENDING | CONDITIONAL | PASS
"reason_codes": ["MRZ_CHECKSUM_FAIL"],
"override_rules_fired": ["I1"],
"verdict_explanation": ""
},
"findings": [
{
"check_id": "mrz_checksum_failure",
"check_category": "identity",
"severity": "HIGH",
"summary": "MRZ check digit mismatch on birth date",
"evidence_quote": "expected=3, computed=7",
"source_document_id": "doc-uuid"
}
],
"advisory_score": 12, // 0-100, informational only, does not drive verdict
"advisory_band": "HIGH_RISK", // HIGH_RISK | REVIEW | LOW_RISK
"checker_version": "0.4.0",
"analysis_ts": "2026-05-31T10:00:00Z",
"recommended_action": "Request current payslip"
}Verdict severity order: REJECT > ESCALATE > PENDING > CONDITIONAL > PASS. Stable
check_id/reason_code/override_rules_fired values are catalogued and forward-compatible -
tolerate unknown codes.
Limits
| Limit | Value |
|---|---|
| Max files per request | 15 |
| Max size per file | 20 MB |
| Max total body | 50 MB |
| Accepted extensions | .pdf .jpg .jpeg .heic .heif .png .tif .tiff |
| Rate limit | 60 requests/minute per tenant (default), 429 with Retry-After on breach |
Non-goals
There is no applicant-list endpoint. POST /analyze and POST /v1/applicants/{id}/analyze are
synchronous, no polling needed. Polling applies only to /v1/batch/* async jobs.