This page documents the endpoints an ANAX Standard subscriber calls directly. It describes the API as deployed. Where a capability is not implemented, this page says so rather than omitting it.
| Service | Base URL | Authentication |
|---|---|---|
| X-Layer | provided at on-boarding | API key |
| Canal | provided at on-boarding | API key; two endpoints are public |
Every X-Layer endpoint except GET /health and GET / requires an API key. Send it in either header:
X-API-Key: sk-canal-...
Authorization: Bearer sk-canal-...
Keys are issued against a Canal subscription. X-Layer is an add-on: a key must already be an active Canal key before X-Layer can be activated on it.
The responses below are X-Layer's ({"status": "<word>"}). Canal returns {"ok": false, "reason": "…"}; a named Canal fault also carries an error word beside reason — see the Canal section.
| Status | status | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | No key supplied, or the key is unknown or inactive. These are deliberately indistinguishable. |
| 403 | SUSPENDED | The key exists but is suspended. |
| 403 | SUBSCRIPTION_EXPIRED | The licence period has ended. The response carries expired_at. |
| 403 | UPGRADE_REQUIRED | The key is valid but lacks the entitlement for this endpoint. The response carries current_tier and upgrade_url. |
| 403 | FORBIDDEN | Owner scope: the caller may only read its own records. |
Key lookups are cached for five minutes. A newly-activated entitlement can therefore take up to five minutes to take effect on a key that was used immediately before activation.
| Endpoint | Auth | Returns |
|---|---|---|
GET /health | none | {"status":"OK"} — liveness only. The engine roster is deliberately not exposed here. |
GET / | none | Service name, version and endpoint index. |
| Endpoint | Entitlement | Notes |
|---|---|---|
POST /sentinel/baseline | X-Layer | Creates a behavioural baseline for a client_id / system_id / model_id triple. |
POST /sentinel/evaluate | X-Layer | Evaluates one decision against the baseline. Returns 403 when the result is BLOCKED. |
GET /sentinel/drift/:source | X-Layer | Window-based drift for one system: 7-day against 30-day clearance, computed from sealed Canal crossings. |
GET /client/drift | X-Layer | Drift across every active system owned by the caller, plus a worst-case aggregate. Systems with no scan return INSUFFICIENT_DATA, never a passing state. |
GET /intelligence/:source (X-Layer) runs the full sequence for one system and returns a single object: Sentinel-X drift, the Predictive-X forecast, the Prism-X narrative, and the Fusion-X unified state, with the underlying drift windows attached.
| Field | Values |
|---|---|
prediction | DRIFT_IMMINENT (0–7 days) · DRIFT_LIKELY (7–21 days) · WATCH_CLOSELY · STABLE |
trend | SEVERE_RISK · HIGH_RISK · WATCH · STABLE |
risk_level | CRITICAL · AT_RISK · STABLE |
Cannot-assess is an explicit state, not a passing one. Where there is too little governed decision history, or the history is too noisy for a confident reading, the response carries INSUFFICIENT_DATA or INSUFFICIENT_CONFIDENCE with confidence: 0 and assessable: false. These propagate unchanged through the forecast and the narrative. They must not be read as an all-clear.
No endpoint returns a calibrated probability. Forecasts are weighted bands with a stated horizon.
POST /anassa/brief (X-Layer) generates a board-level strategic brief from the caller's own governance record. Rate-limited independently of the other endpoints.
| Endpoint | Entitlement | Notes |
|---|---|---|
POST /chain/append | X-Layer + Chain | Runs the engine sequence, seals the result as a report with a SHA-256 content hash, and appends it to the caller's chain. Returns report_id, link_hash, previous_link_hash and chain_position. This endpoint seals a link; it does not verify the chain. |
GET /chain/verify/:client_id | X-Layer + Chain | Recomputes every link's hash from its stored basis and checks that each link points at its predecessor. |
GET /chain/public-verify/:chr_id | public | Verifies one Chronicle Chain link by its CHR- reference. No key required; mounted before the auth middleware, by design. |
Verification returns one of four states, and never reports intact without having checked:
status | Meaning |
|---|---|
CHAIN_INTACT | Every link recomputed and every linkage matched. |
CHAIN_BROKEN | At least one HASH_MISMATCH or BROKEN_LINKAGE, reported with its exact chain position. |
UNVERIFIABLE_LEGACY | Linkage checked, but one or more links predate content-basis storage and cannot be hash-recomputed. |
EMPTY | No links on record. |
If the registry is unreachable the endpoint returns 503 with verified: false and UNVERIFIABLE. It does not assume intact.
⚠ GET /chain/verify/:client_id may also return 403 REFUSED or 503 UNAVAILABLE for a reason concerning ANAX's own authority to perform the read — not the caller's entitlement. A well-entitled client can see either if ANAX's own Mandate is exhausted or unconfigured; it is not a statement about your access.
Scope. GET /chain/verify/:client_id is owner-scoped: a caller may verify only its own chain. X-Layer chain links verify publicly, no key: GET /chain/public-verify/:chr_id (mounted before auth, by design). Five states, not three: 200 valid:true (sealed) · 200 valid:false (well-formed, not found — a verdict, not an error) · 400 MALFORMED_ID · 429 RATE_LIMITED · 503 UNAVAILABLE (a lookup failure, never valid:false). The rate limiter runs before the id-format check, so a malformed id from an over-limit caller gets 429, not 400. Window: 60 seconds, 60 requests per IP per instance (the real ceiling is 60 × instances) — X-RateLimit-Limit / X-RateLimit-Window / X-RateLimit-Scope are set on every response, 200 through 503, so a caller who never sees a 429 can still read what the limit is. A 429 carries Cache-Control: no-store, and no Retry-After header is sent on any path. The response's attestation block (fixed, compiled-in facts about the verifier, not row data — marked "source": "constant") appears only on 200, on both valid:true and valid:false; 400, 429 and 503 attest nothing. Public verification also exists for Canal crossing seals (below).
POST /agc/assess returns 501 NOT_IMPLEMENTED. The AGC certification engine does not exist, and this endpoint issues nothing and writes nothing.
It previously derived a certification level from fields supplied in the request body and returned a SHA-256-sealed certificate. A certificate minted from the caller's own inputs is not an assessment, and the endpoint was disabled rather than left to look like one. It will return a certificate when the engine exists and every input is derived server-side.
GET /agc/status/:client_id is owner-scoped: a caller may read only its own status (master keys excepted). It answers in one of three states, and an absence is never reported as a grade.
status | HTTP | Meaning |
|---|---|---|
| — (the certificate row) | 200 | A certificate is on record. The stored row is returned as held. |
NO_GRADE_ON_RECORD | 200 | The registry was read and holds no certificate for this client. agc_level is null. |
UNAVAILABLE | 503 | The registry could not be read. agc_level is null. This is a read failure, not a finding about the client. |
Absence is null, never 0. A grade of 0 is a measured verdict; “we could not look” and “we looked and found nothing” are not verdicts at all, and the two are kept distinct so a caller knows whether to retry or to act. No level name is returned beside a null level.
Canal evaluates a set of declared governance attributes about one AI decision — sector, risk class, personal-data flag, criticality, automation level, jurisdiction, and the regulation to cross it against — not the decision's actual content. The decision itself (what it decided, on what input) is never sent to Canal and never sealed; every attribute is caller-asserted, taken as declared and not independently verified by Canal. Canal seals its verdict on those attributes into a hash-chained, independently verifiable record: the declared attributes and the verdict are covered by the same seal — neither can be changed without invalidating verification. This is detectable tampering, not physical immutability: GET /canal/verify/:seal_id recomputes the hash from the stored basis, so an altered record fails verification rather than silently passing. The crossing is chained when it carries a clearance envelope (system_clearance_id), sealed but unchained otherwise — meta.chain_linked in the response reflects which actually happened, derived from the write itself, not asserted. A subscriber calls Canal directly with an API key. Two endpoints are public and need no account: GET /canal/frameworks (the framework catalogue) and GET /canal/verify/:seal_id (independent verification of any seal).
| Endpoint | Auth | Notes |
|---|---|---|
POST /canal/envelope | API key | Declare an AI system and mint its clearance envelope — the first call, prerequisite for a chained/clearing crossing. Returns clearance_id (env_...). Sending system_id / clearance_id is refused 400 (server-derived). |
GET /canal/frameworks | public | The framework catalogue with the tier each one requires. |
GET /canal/verify/:seal_id | public | Independent verification of a crossing seal. Returns the sealed governance-attribute record and verdict with secrets removed — never the decision's own content, which Canal was never sent. |
POST /canal/evaluate | API key | Evaluate and seal one crossing from six simple fields — self-serve, and gates on the base tier. For a chained, clearing crossing, first POST /canal/envelope. |
POST /canal/evaluate/full | API key | Evaluate and seal one crossing from a complete canal-crossing-transaction-v2.1 contract object. |
GET /canal/chain/verify/:clearance_id | API key | Verify an envelope's crossing chain. |
GET /canal/chain/export/:clearance_id | API key | Export the full chain. |
POST /fusion | API key + X-Layer sub | Unified intelligence (Sentinel + Predictive + Prism + Fusion). 403 UPGRADE_REQUIRED without X-Layer. Pure computation — nothing sealed. |
GET /canal/evidence/:seal_id | API key, owner-scoped | Full evidence bundle for a seal you own. 403 on someone else's seal. |
GET /canal/crossings | API key, owner-scoped | List your own crossings (owner scope). |
POST /canal/attestation | API key, owner-scoped | Record a self-declared compliance attestation (SOC 2 v1; other frameworks pre-provisioned, not yet selectable). Immutable once written. Not verified by ANAX. |
GET /canal/attestation | API key, owner-scoped | List your own attestations, optionally filtered by subject_ref. |
An active Canal API key, and a framework your tier licenses, is all you need. Send the key in the X-API-Key header and post two fields — the decision's governance attributes and the framework to cross them against. A framework outside your tier is refused 403 not_licensed (see below), not silently accepted. This minimal call is unchained by design; an envelope (POST /canal/envelope) is the setup step for a chained crossing, not required for this one:
curl -X POST https://<your-canal-host>/canal/evaluate \
-H "X-API-Key: sk-canal-..." \
-H "Content-Type: application/json" \
-d '{"input":{"source":"my-system","type":"loan_decision"},"regulation":"eu_ai_act_2024"}'
The crossing is sealed and returns an outcome, a seal_id and a seal hash. This minimal call is sealed but not chained and returns CLEARED_WITH_VICCA_REQUIRED — the onboarding gate. To get a chained, clearing crossing, add a system_clearance_id from an envelope you created with POST /canal/envelope.
What a first call returns. An ungoverned first call on the six-field path — no Mandate presented — returns a CLEARED-family outcome:
CLEARED_WITH_CONDITIONSinside an envelope,CLEARED_WITH_VICCA_REQUIREDwithout one; never bareCLEARED, which requires an authority-sealed VICCA assessment.
BLOCKEDis itself a verdict — sealed and returned 200 like any other outcome, never an error status — and it has two independent gates. The prohibited-practice gate is not reachable from this six-field path:prohibited_practiceis fixedfalsehere and can only be set true viaPOST /canal/evaluate/full. The Mandate authority gate is reachable from both paths: present amandatetogether with acommitment_class, and Canal judges the declared class against the act's floor and the Mandate's ceiling on the same reversible…binding ladder used throughout this API. A class below the floor, above the ceiling, or off the ladder entirely is refused asclass_below_floor/class_exceeded/malformed_action— sealed asBLOCKED, not returned as an error. Nomandateat all is the ungoverned path: not a refusal, and every existing caller's default. Amandatethat is itself invalid or expired never reaches this gate — it is refused earlier, customer-actionable, 403mandate_invalid/mandate_expired(below).ESCALATE_TO_VICCAis a retired alias, kept only for backward compatibility —CLEARED_WITH_VICCA_RECOMMENDEDis what current code returns in its place. Do not expect bareCLEAREDfrom a first call, and do not assumeBLOCKEDis unreachable just because you skipped/full.
POST /canal/evaluateHeader: X-API-Key: <key> (the key is read from the header only). Body:
| Field | Type | Required | Default / effect |
|---|---|---|---|
input | object | yes | Governance attributes about the decision being crossed — not the decision's content itself, which is never sent. Missing → 400 {"ok":false,"reason":"Missing input"}. |
input.source | string | in practice yes | Becomes the AI system's system_id and provider — the identity the seal is chained under. |
input.type | string | recommended | The decision type, matched against the envelope's permitted decision types. |
input.payload.sector | string | optional | Default "general". (.domain is accepted as an alias.) |
input.payload.risk_class | enum | optional | Default "limited_risk". One of prohibited · high_risk · limited_risk · minimal_risk · gpai. |
input.payload.personal_data | boolean | optional | Default false. |
input.payload.criticality | string | optional | Default "medium". |
regulation | string | yes | One framework id from GET /canal/frameworks (e.g. eu_ai_act_2024, gdpr_2016_679, dora_2022, iso27001_2022). Must be in the key's licensed set, else 403 not_licensed. Missing → 400 {"ok":false,"reason":"Missing regulation"}. |
automation_level | enum | optional | Default "ai_assisted". One of fully_automated · ai_assisted · ai_informed · human_primary. |
jurisdictions | string[] | optional | Default ["EU"]. Only the first entry is used — for origin, destination and processing location alike. |
system_clearance_id | string | optional | A clearance envelope you own (from POST /canal/envelope). Absent or unknown → the onboarding gate: sealed as CLEARED_WITH_VICCA_REQUIRED, not chained. An envelope you do not own is treated as absent. |
commitment_class | enum | optional | Default null. One of observe · reversible · irreversible · financial · binding. Judged only when a mandate is presented. |
mandate | string | optional | An SD-JWT Open Mandate. When present it is verified, judged and spent (one unit). Absent = the ungoverned path every existing caller takes. |
phase_flags | string[] | optional | Default [] (regulatory-phase flags). |
Do not send these — they are server-assigned and a sent value is either ignored or refused:
commitment_act — the act is server-assigned (canal_evaluate); a body value is ignored.system_id, clearance_id, envelope_owner — never read here; identity is input.source plus the key's owner. On POST /canal/envelope, sending system_id or clearance_id is refused: 400 {"ok":false,"reason":"system_id / clearance_id are server-derived and must not be sent"}.api_key in the body — not read (header only).Posted against a MedTriage AI envelope created one step earlier:
{
"system_clearance_id": "<clearance_id from POST /canal/envelope>",
"input": {
"source": "medtriage-triage",
"type": "symptom_triage",
"payload": { "sector": "general", "risk_class": "limited_risk", "personal_data": true, "criticality": "medium" }
},
"regulation": "eu_ai_act_2024",
"automation_level": "ai_assisted",
"jurisdictions": ["EU"]
}
→ 200, outcome: "CLEARED_WITH_CONDITIONS", reason: "Transaction within acceptable parameters. Cleared subject to standard oversight conditions." — the default branch when no VICCA assessment is on record. Not bare CLEARED, by design.
One body serves every sealed verdict. A governance block (a prohibited practice, or a Mandate declaring a class below the act's floor or above its ceiling) is this same body with outcome: "BLOCKED" — sealed and chained like any other verdict. There is no error key on any 200.
{
"ok": true,
"seal_id": "canal-<ms>-<12hex>",
"seal": "<64-hex sha256 of the sealed basis>",
"timestamp": "<ISO-8601>",
"outcome": "<CANAL_OUTCOMES word>",
"reason": "<determination basis>",
"flags": ["<conflict flag>", "..."],
"triggers": ["..."],
"conditions": ["..."],
"vicca_recommendation": <object | null>,
"regulation": "<regulation>",
"automation_level": "<automation_level>",
"jurisdictions": ["<...>"],
"canal_intelligence": <object | null>,
"temporal_assessment": <object>,
"crossings_remaining": <int>,
"meta": {
"engine": "CANAL-API", "version": "2.1", "mode": "sovereign-port",
"evidence_stored": <bool>,
"chain_linked": <bool>,
"chain_seq": <int | null>
}
}
meta.chain_linked is derived from the actual write and is false only for a crossing with no clearance envelope (nothing to chain to).
CANAL_OUTCOMES — the only values outcome can take:
BLOCKED · CLEARED · CLEARED_WITH_CONDITIONS · CLEARED_WITH_VICCA_RECOMMENDED · CLEARED_WITH_VICCA_REQUIRED · ESCALATE_TO_VICCA · REGISTERED
The single rule: a governance-result field exists only when a gate evaluation ran and was sealed — /canal/evaluate returns it as outcome, /canal/evaluate/full as canal_evaluation.result. A pre-evaluation 4xx/5xx on either endpoint carries neither field and no seal. A refusal before evaluation — commercial, credential, or ours — carries an error word and seals nothing. A client that branches on outcome therefore never mistakes “your plan ran out” or “our service had a fault” for a governance verdict.
| Class | Who acts | HTTP | Wire marker | Sealed? |
|---|---|---|---|---|
| Governance verdict — a gate ruled on the crossing | nobody: it is the answer | 200 | outcome + seal_id + seal | yes |
| Customer-actionable — licence, plan, credential | you remediate | 401 / 403 / 429 | an error word; never outcome | no |
| ANAX-side — a dependency we could not read, or our own credential | we repair; you retry | 503 | an error word, or a reason ending “no verdict issued. Retry.”; never outcome | no — the allowance unit is refunded where one was taken |
No Retry-After header is sent on any path. A 503 is a service-side condition an operator repairs, not a fixed wait; your normal backoff is the right pace.
| Condition | Response |
|---|---|
| no header | 401 {"ok":false,"reason":"X-API-Key header required","docs":"https://anaxstandard.ai/docs/api"} |
| unknown or inactive key | 401 {"ok":false,"reason":"Invalid or inactive API key"} |
| key suspended | 403 {"ok":false,"reason":"API key suspended"} |
| licence period ended | 403 {"ok":false,"reason":"License expired. Renew at app.anaxstandard.ai"} |
Only a sealed verdict carries a governance result, and only at 200 — BLOCKED included: it is itself a verdict, sealed and returned 200 like any other outcome, never an error status. On /canal/evaluate, outcome and seal appear together at 200; on /canal/evaluate/full, the equivalent verdict field is canal_evaluation.result, with the same seal fields alongside it. A failure before evaluation carries neither a governance-result field nor a seal. Within that pre-verdict space, a named licence, allowance or Mandate fault carries an error word beside reason ({"ok": false, "error": "...", "reason": "..."}) — not_licensed, allowance_exhausted, mandate_invalid / mandate_expired / mandate_exhausted / mandate_refused, allowance_counter_unavailable, mandate_unavailable, chain_write_unavailable. All other failure classes are reason-only unless explicitly documented with an error code.
403 {"ok":false,"error":"not_licensed",
"reason":"Regulation '<regulation>' not in your <license_tier> license. Upgrade at app.anaxstandard.ai",
"your_frameworks":["<framework>","..."]}
your_frameworks is your own licensed set (never another tenant's). A key with no frameworks at all carries "reason":"No frameworks are licensed on this key. Contact app.anaxstandard.ai" and "your_frameworks":[].
Emitted only on positive proof from a successful counter read (used >= limit); every other counter state is a 503, never a 429.
429 {"ok":false,"error":"allowance_exhausted",
"reason":"Monthly crossing limit of <N,NNN> reached. Upgrade at app.anaxstandard.ai",
"crossings_remaining":0}
The ceiling is the tier's: entry 5,000 · professional 20,000 · institutional 100,000 crossings per calendar month (UTC).
When you present a mandate that is yours to fix. Two moments, one shape — at load (verification: signature, expiry, audience) and at spend (the Registration Authority refused the unit):
403 {"ok":false,"error":"mandate_invalid" | "mandate_expired" | "mandate_exhausted",
"reason":"<the same word>","allowance_refunded":<true | false>}
A customer-attributable RA refusal outside the named customer vocabulary returns "error":"mandate_refused". ANAX-side RA failures are normalised to 503 mandate_unavailable and are never exposed as customer Mandate faults. allowance_refunded is the measured result of the refund (true only when the crossing unit was actually returned), never “attempted”. Nothing is sealed on any of these.
A 503 here means our service could not do its part: a dependency we could not read, a counter we could not trust, or our own credential refused by the Registration Authority. It is not “the network is down”, and it is never your Mandate or plan. The body carries one normalised word — no raw database, RA or library text ever reaches the wire. Nothing is sealed; the allowance unit is refunded wherever one had been taken.
| Condition | Response |
|---|---|
| allowance counter unreadable | 503 {"ok":false,"error":"allowance_counter_unavailable","reason":"Crossing counter temporarily unavailable - crossing not recorded. Retry shortly."} — no crossings_remaining: unknown is not zero. |
| RA unreachable / our credential refused | 503 {"ok":false,"error":"mandate_unavailable","reason":"mandate_unavailable","allowance_refunded":<bool>} |
| verifier unreachable (before any spend) | 503 {"ok":false,"reason":"Mandate could not be verified - no verdict issued. Retry."} |
| chain head could not be taken | 503 {"ok":false,"error":"chain_write_unavailable","reason":"chain_write_unavailable","allowance_refunded":<bool>} |
| a read we could not make (clearance envelope, VICCA assessment, chain position, trust weight) | 503 {"ok":false,"reason":"<dependency> unavailable - no verdict issued. Retry."} |
Raised before any route runs; the key is not read.
| Condition | Response |
|---|---|
| malformed JSON body | 400 {"ok":false,"reason":"Malformed JSON body"} |
| body over 10 MB | 413 {"ok":false,"reason":"Request body exceeds 10mb"} |
| malformed request URI | 400 {"ok":false,"reason":"Malformed request URI"} |
POST /canal/evaluate/full — the full contractWhere /canal/evaluate builds the transaction object for you from six simple fields, /canal/evaluate/full takes the whole canal-crossing-transaction-v2.1 object — exposing the decision_impact, oversight_controls and richer data_profile blocks the evaluation reads directly. Admission requires contract === "canal-crossing-transaction-v2.1" and a caller-supplied transaction_id (a second seal of the same id for the same account → 409). Self-declared trust is neutralised server-side — a caller cannot assert its own evidence level.
Schema, not a proven example. The object below is the contract builder's output with its defaults, verbatim — but no
/fullfixture exists in the repository, so unlike the/evaluateexample above it has not been measured against a live call. Derive your object from it; do not treat it as a tested request.
{
"contract": "canal-crossing-transaction-v2.1",
"transaction_id": "<caller-supplied, unique per account>",
"timestamp": "<ISO-8601>",
"ai_system": {
"system_id": null, "provider": null, "provider_jurisdiction": null, "model_type": null,
"deployment_environment": null, "hosting_jurisdiction": null, "system_clearance_id": null
},
"transaction_context": {
"regulation": null, "decision_type": null, "sector": null, "use_case": null,
"criticality": null, "automation_level": null, "human_review": null
},
"jurisdiction": {
"origin": null, "processing_location": null, "destination": null,
"affected_subject_jurisdiction": null, "additional_hops": []
},
"data_profile": {
"personal_data": false, "special_category_data": false, "biometric_data": false,
"financial_data": false, "health_data": false, "children_data": false
},
"ai_risk_profile": {
"risk_class": null, "ai_act_domain": null, "prohibited_practice": false, "gpai_component": false
},
"decision_impact": {
"affects_individual_rights": false, "economic_impact": false, "legal_effect": false,
"service_access": false, "reversible": true
},
"oversight_controls": {
"human_in_the_loop": false, "explainability_available": false,
"appeal_mechanism": false, "decision_logging": false
}
}
On /canal/evaluate/full the verdict is canal_evaluation.result, not a top-level outcome; the seal fields are the same.
POST /v1/crossing:seal is the governed bilateral crossing path. It requires Closed Mandates held by both parties — issued by the Registration Authority against each party's Open Mandate and bound to one agreed Terms Document — and it is not self-serve: it is not callable with an API key alone (the credential is the Mandate). Its error vocabulary is a JSON-RPC extension documented in the Governed Crossing specification, Addendum A-3.5. For governed bilateral crossings, contact us.
What a third party can do without an account is verify a bilateral seal: GET /canal/verify/:seal_id and the public verification portal need no credential.
/canal/attestationPOST /canal/attestation records that a subject holds, or is pursuing, a named compliance framework — today, v1, only soc2 is selectable (the other three framework ids in the schema are pre-provisioned for future intake). Two shapes: status: "held" requires cert_number, issuing_body and a future-dated expiry_date; status: "in_progress" requires auditor_name, engagement_start and expected_completion. The source of every field is the caller, always — a constant, not a variable: Canal stores what you declare and stamps the row's sealed_label with “NOT verified by ANAX” verbatim. Rows are immutable once written. GET /canal/attestation lists your own rows (optionally filtered by subject_ref), each carrying a server-derived expired and superseded flag — never accepted from the caller.
A passport or certificate holder can direct anyone — an auditor, a governor, a counterparty — to the public verification portals. No account is required.
What the Canal API issues. A Decision Passport is issued by the VICCA ONE governance path, not by Canal — nothing documented in the Canal section above produces one. Every Canal call that reaches a verdict issues a seal_id and a seal hash only, independently verified with GET /canal/verify/:seal_id. If your integration needs a Decision Passport, that is a separate, VICCA ONE-issued artifact, not a Canal API response field.
Certificates resolve at .ai. Both certificate families carry an ANAX-<year>- reference, and the .ai portal resolves both: an earned-era reference (-EG) is answered by re-deriving its Evidence Grade from the sealed crossing, and a legacy -T1 reference is shown as a registry record with no grade. The .com registry holds only the legacy family and answers NOT FOUND for an -EG reference.
| Portal | Verifies |
|---|---|
| verify.anaxstandard.ai | Decision Passports, Canal crossing seals, certificates, and Chronicle Chain links (CHR-) |
| verify.anaxstandard.com | Legacy VICCA ONE Governance Defensibility Certificates (-T1) only |
anaxstandard.ai and anaxstandard.com subdomains. Server-to-server calls are unrestricted and gated by the API key.{"status": "...", "message": "..."}. Canal errors return {"ok": false, "reason": "..."} (or {"ok": false, "error": "...", "reason": "..."} for a named fault) — see the Canal section. Authentication failures do not distinguish an unknown key from an inactive one.