IDCanopy Developers
Aletheia

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

LimitValue
Max files per request15
Max size per file20 MB
Max total body50 MB
Accepted extensions.pdf .jpg .jpeg .heic .heif .png .tif .tiff
Rate limit60 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.

On this page