API reference
Complete endpoint reference for the mnd8t authority API. For a narrative
introduction, read the developer guide first.
Interactive OpenAPI — generated from the route schemas, always in sync with
the running build — is served at /docs on your API host.
Base path: /v1 · Content type: application/json ·
All money: integer minor units · All timestamps: ISO 8601 UTC
Conventions
Authentication
Authorization: Bearer mdt_test_...
API keys are scoped. A key may do exactly what its scopes permit and nothing else, within its own organisation and environment.
| Scope | Grants |
|---|---|
decisions:read / decisions:write |
read decisions; submit, confirm, cancel |
artifacts:read / artifacts:write |
read artifacts; claim, fail, reissue |
receipts:read |
read evidence receipts |
registry:read |
read agents, counterparties, mandates, accounts |
agents:write |
create and modify agents |
counterparties:write |
create and modify counterparties |
accounts:write |
create and modify financial accounts and bindings |
mandates:write |
create and edit draft mandates |
mandates:publish |
publish and revoke — the act that grants authority |
webhooks:write |
manage webhook endpoints |
valuations:assert |
state what an action is worth — not in the AGENT preset |
evidence:submit |
report what actually happened after execution — not in the AGENT preset |
keys:write |
create and revoke API keys |
approvals:act |
approve or reject escalations |
Two presets: AGENT — the first six, everything needed to transact and
nothing that can change what is permitted — and ADMIN, all fourteen.
POST /v1/api-keys defaults to AGENT.
Give an agent an AGENT key. An ADMIN key can add a counterparty and
publish a mandate, so an agent holding one can rewrite the authority it is
about to be evaluated against. A key also cannot mint a key wider than
itself.
Dashboard requests authenticate with a signed session cookie instead. People
carry an organisation role (ADMIN / APPROVER / VIEWER), not scopes; the
role each endpoint needs is noted below, and is unchanged by scoping.
Public exceptions (no credential): the approval-link endpoints, the key discovery document, and the health endpoints.
Idempotency
All write endpoints accept Idempotency-Key. POST /v1/decision-intents
requires it (8–200 characters).
| Situation | Result |
|---|---|
| Same key, identical payload | Original response replayed |
| Same key, different payload | 409 IDEMPOTENCY_CONFLICT |
| Same key, first still in flight | 409 CONFLICT |
Records are retained 7 days.
Errors
{ "error": { "code": "VALIDATION_FAILED", "message": "…", "details": null } }
UNAUTHENTICATED 401 · FORBIDDEN 403 · NOT_FOUND 404 ·
IDEMPOTENCY_CONFLICT / CONFLICT 409 · VALIDATION_FAILED /
UNSUPPORTED_ACTION_TYPE 422 · RATE_LIMITED 429 · INTERNAL 500.
Resources belonging to another organisation return 404, never 403.
Rate limits
300 requests/minute globally; 120/min on the decision endpoint; 10–60/min on
authentication, approval actions, and artifact claims. Exceeding a limit
returns 429.
Decisions
POST /v1/decision-intents
Submit a proposed financial action for an authority decision. This is the endpoint. Call it before executing, never after.
Headers: Authorization, Idempotency-Key (required)
Body
| Field | Type | Notes |
|---|---|---|
agent_external_id |
string | Required. The agent's external_id. |
action_type |
enum | PAYMENT | TRANSFER. Default PAYMENT. Anything else → 422 UNSUPPORTED_ACTION_TYPE. |
amount |
object | { asset, asset_amount_minor }. Preferred form. |
valuation |
object | { policy_amount_minor, policy_currency, source?, rate_reference?, valued_at? }. The value the engine evaluates. |
amount_minor, policy_amount_minor, policy_currency, asset |
— | Deprecated flat form; still accepted. |
counterparty_external_id |
string | Resolved against your counterparty registry. |
destination |
string | Wallet address or provider reference. |
purpose |
string | Checked against allowed_purposes. |
financial_account_id |
string | The account the action would draw on. |
external_reference |
string | Your invoice/order id. |
occurred_at |
string | ISO 8601, informational only: recorded on the intent and its receipts. Policy evaluation — schedules and budget periods — always uses server time, so a caller-supplied timestamp cannot move a decision into a different schedule window or budget period. Defaults to server time. |
jurisdiction_context |
object | { principal_country?, provider_country?, counterparty_country?, customer_classification? }. Evidence only — never affects the decision. |
metadata |
object | Free-form; stored and echoed in evidence. |
mnd8t never fetches exchange rates. Supply policy_amount_minor already
normalised into the mandate's currency, with provenance.
201 — enforce mode, approved
{
"id": "int_9k2m4x8q1z7c4b6n0p5v",
"mode": "ENFORCE",
"decision": "APPROVE",
"effective_decision": "APPROVE",
"would_decide": "APPROVE",
"reason_codes": ["APPROVED_WITHIN_MANDATE"],
"mandate_id": "mnd_3f8k2m9x",
"mandate_version": 3,
"approval": null,
"authorisation_artifact": {
"id": "art_7bd910442f8e",
"status": "ISSUED",
"expires_at": "2026-08-02T15:00:00.000Z",
"download_url": "/v1/authorisation-artifacts/art_7bd910442f8e"
},
"evidence_receipt_id": "evd_2m8q4x9k1z7c"
}
201 — escalated. decision: "ESCALATE", authorisation_artifact: null,
and an approval block carrying { id, status: "PENDING", expires_at, approval_url }. The artifact is issued only once a human approves.
201 — shadow mode. effective_decision: "OBSERVE", would_decide set
to what enforcement would have done, SHADOW_ONLY appended to
reason_codes, no approval, no artifact, no budget consumed.
An artifact is issued for approved enforce-mode decisions only — never for rejections, unresolved escalations, or shadow decisions.
GET /v1/decision-intents
List decisions, newest first.
Query: status, decision, agent_external_id, limit (1–200,
default 50), offset.
GET /v1/decision-intents/{id}
One decision with everything attached: the rule-by-rule evaluations trace,
approvals, executions, artifacts, receipts, and the agent.
Use the trace to show a human exactly which rule fired:
{
"evaluations": [
{ "rule_order": 12, "rule": "absolute_limit", "passed": true, "reason_code": null },
{ "rule_order": 16, "rule": "autonomous_threshold", "passed": false, "reason_code": "HUMAN_APPROVAL_REQUIRED" }
]
}
POST /v1/decision-intents/{id}/confirm
Record execution reported by your executor or provider. mnd8t does not execute; this endpoint evidences what you tell it.
{
"provider": "SAFE_TESTNET",
"external_reference": "0xabc…",
"authorisation_artifact_id": "art_7bd910442f8e",
"financial_account_id": "fac_1m8q4x9k",
"execution_status": "CONFIRMED",
"executed_at": "2026-08-01T14:06:00Z",
"reported_by": "CUSTOMER_EXECUTOR",
"verification": { "method": "PROVIDER_LOOKUP", "verified": true, "verified_at": "2026-08-01T14:06:05Z" }
}
authorisation_artifact_id is required for enforce-mode intents; the
artifact must belong to this intent and be ISSUED, CLAIMED or
RECONCILIATION_REQUIRED. ISSUED and CLAIMED artifacts must also be
unexpired; a RECONCILIATION_REQUIRED one is past its expiry by definition
and is accepted so a late execution can still be reported. reported_by ∈ CUSTOMER_EXECUTOR | CUSTOMER_API |
PROVIDER_WEBHOOK | MANDATE_RECONCILIATION. verification.method ∈
CUSTOMER_ASSERTED | PROVIDER_LOOKUP | PROVIDER_WEBHOOK |
NETWORK_LOOKUP.
Consumes the budget reservation exactly once and moves the artifact to
EXECUTED. Repeating with the same external_reference returns 200 with
duplicate: true.
POST /v1/decision-intents/{id}/cancel
{ "reason": "Supplier invoice withdrawn" } — releases the reservation,
expires any pending approval, and cancels live artifacts. Valid from
APPROVED, PENDING_APPROVAL, or OBSERVED.
Authorisation artifacts
The signed, single-use token that lets your system execute an approved action. See developer guide §6.
GET /v1/authorisation-artifacts/{id}
{
"id": "art_7bd910442f8e",
"decision_intent_id": "int_9k2m4x8q1z7c4b6n0p5v",
"financial_account_id": "fac_1m8q4x9k",
"status": "ISSUED",
"artifact": {
"artifact_version": "1.0",
"artifact_id": "art_7bd910442f8e",
"decision_intent_id": "int_9k2m4x8q1z7c4b6n0p5v",
"organisation_id": "org_2f8c1b7d9e4a",
"agent_id": "agt_5h3n8s2w7q1m",
"mandate_id": "mnd_8w2r5t9y4u1o",
"mandate_version": 3,
"financial_account_id": "fac_1m8q4x9k",
"action_type": "PAYMENT",
"asset": "USDC",
"amount_minor": 84000000,
"policy_amount_minor": 50000,
"policy_currency": "GBP",
"destination": "0x…",
"counterparty_reference": "supplier_acme_cloud",
"external_client_reference": "invoice_9834",
"nonce": "…",
"issued_at": "…", "not_before": "…", "expires_at": "…",
"signature": { "algorithm": "Ed25519", "key_id": "key_2026_01", "value": "base64…" }
},
"artifact_hash": "sha256:…",
"key_id": "key_2026_01",
"claimed_at": null,
"executed_at": null
}
The signature covers every field of artifact except signature itself, in
canonical form. An executor can therefore verify the complete execution
context — organisation, agent, mandate version, account, amounts, destination,
counterparty and validity window — offline, before acting.
Verify artifact locally before acting on it. The signature covers account,
action type, asset, amount, destination, counterparty, nonce and expiry.
Status: ISSUED → CLAIMED → EXECUTED, or EXPIRED / REVOKED /
CANCELLED / FAILED.
A CLAIMED artifact that passes its expiry with no confirm or fail becomes
RECONCILIATION_REQUIRED rather than EXPIRED — an executor took it, so
whether the money moved is unknown rather than known not to have. Its budget
reservation stays held (UNRECONCILED) until /confirm or /fail resolves
it; /cancel returns 409 until then. See
the policy model for the full table.
POST /v1/authorisation-artifacts/{id}/claim
{ "executor_reference": "executor-instance-1" }
Atomic single claim. Concurrent claimants: exactly one gets 200; the rest
get 409. Already-claimed, expired, revoked and cancelled artifacts all
409. Claim before executing.
POST /v1/authorisation-artifacts/{id}/fail
{ "reason_code": "PROVIDER_REJECTED", "external_reference": null, "reported_by": "CUSTOMER_EXECUTOR" }
Marks the artifact FAILED — the call that reports the money did not move,
and the one failure path an executor needs. An UNRECONCILED hold is released
here; an active reservation is deliberately left in place so you can
reissue-artifact and retry. Pair it with …/cancel only when you are
abandoning the action, which is terminal.
POST /v1/decision-intents/{id}/reissue-artifact
Issues a fresh artifact after a failure or expiry. Re-checks live state
first — revoked mandate, expired mandate, or released reservation all 409,
and an already-active artifact 409. The decision is not re-evaluated
against budgets; if the reservation is gone, submit a new intent.
Outcomes
An approved payment and a fulfilled mandate are different facts. Everything above records the first: that an action was within the delegated authority at the moment it was taken. These endpoints record the second — whether the thing the authority was delegated for actually happened.
There is no execution resource here because there already is one: an authorisation artifact is claimed, then confirmed or failed, and that is what execution means in this API. Evidence and outcomes nest under the decision and the mandate they belong to.
Outcome evidence is customer-asserted, and is a different thing from the
signed evidence receipts further down: a receipt is mnd8t's own proof of a
decision it made, while this is your record of what happened next. mnd8t does
not fetch it from a carrier, a merchant, a bank or an ERP, and does not
independently verify it; every response says
"evidence_source": "CUSTOMER_ASSERTED" rather than implying a verification
that does not happen.
To verify an outcome, the mandate's policy must carry an objective — what
the delegation was for, alongside the limits that bound it. It is frozen into
the published version like everything else in the policy, so the question
cannot be rewritten once the evidence is in, and evaluate() never reads it:
no payment is ever approved or refused because of an objective.
{
"objective": {
"description": "100 units of SKU-4410 before the Q4 launch",
"expected_quantity": 100,
"quantity_unit": "units",
"max_total_minor": 1000000,
"deadline": "2026-09-30T23:59:59Z",
"counterparty_id": "supplier_acme_cloud",
"required_evidence": ["merchant_receipt"]
}
}
An expectation implies the evidence that could test it, so expected_quantity
and deadline require delivery evidence and max_total_minor requires
settlement evidence whether or not required_evidence names them. A limit
nobody can ever check is not a limit.
POST /v1/decision-intents/{id}/evidence
Requires evidence:submit, which the AGENT preset does not carry. An
agent that can report a hundred units arrived can declare its own mandate
fulfilled, which is the same circularity scoping exists to break.
{
"type": "goods_receipt",
"observed_at": "2026-09-28T09:00:00Z",
"quantity": 80,
"counterparty_external_id": "supplier_acme_cloud",
"reference": "GRN-4410-02"
}
type is one of payment_settlement (what left the account),
merchant_receipt (what the counterparty says it sold), goods_receipt
(what was actually received) or service_attestation. They are not
interchangeable: quantity is counted from delivery evidence only, because a
supplier's own invoice is its account of the transaction, not proof that
anything arrived.
observed_at is when the reported fact occurred, not when you filed it —
a delivery note submitted late still evidences an on-time delivery.
reference is your identifier for the fact. Re-posting the same type and
reference against the same decision returns the existing record rather than
creating a second one, so retries are safe. 409 if the decision was
rejected: there is no execution to evidence.
GET /v1/decision-intents/{id}/evidence
Everything recorded against one authorised action, oldest observation first.
GET /v1/mandates/{id}/outcome
The objective on the mandate's published version, evaluated against every piece of evidence recorded under it.
{
"mandate_id": "mnd_...",
"mandate_version": 1,
"outcome": "NOT_FULFILLED",
"reasons": ["QUANTITY_SHORT"],
"checks": [
{ "check": "quantity", "expected": ">= 100 units", "observed": "80 units", "passed": false },
{ "check": "total_amount", "expected": "<= 1000000 minor units", "observed": "970000 minor units", "passed": true }
],
"required_evidence": ["payment_settlement", "merchant_receipt", "goods_receipt"],
"missing_evidence": [],
"evidence_source": "CUSTOMER_ASSERTED"
}
FULFILLED and NOT_FULFILLED are conclusions. PENDING_EVIDENCE is the
absence of one — the evidence that would settle the question has not been
supplied, and the objective is not failed merely because nothing has been
reported yet. A failure already visible in the evidence submitted is decisive
even while other evidence is outstanding: no later delivery note turns eighty
units into a hundred.
The example above is the case worth understanding. Every payment under that mandate was correctly approved — inside the per-transaction limit, inside the budget, to an approved counterparty — and the mandate was still not fulfilled. That is why the question is asked separately.
409 if the mandate has no published version, or if its policy declares no
objective.
Mandates
GET /v1/mandate-templates
Starter policies: procurement, treasury, paid_api, blank.
POST /v1/mandates · ADMIN
{ "agent_id": "agt_…", "name": "Cloud procurement mandate", "mode": "SHADOW", "policy": { } }
Creates a draft at version 1. Invalid policies are rejected 422 with
per-field details. Policy schema: policy-model.md.
POST /v1/mandates/{id}/publish · ADMIN
Freezes the draft and makes it live. Requires a delegator attestation:
{
"change_note": "Initial cloud procurement authority",
"delegation": {
"delegator_role": "Finance Director",
"authority_source": "CORPORATE_POLICY",
"source_reference": "DoA-2026-v4-section-8",
"attested": true
}
}
mnd8t records this assertion in the evidence chain. It does not verify
that the delegator legally held the authority. Returns { mandate_id, version, policy_hash, published_at }.
PATCH /v1/mandates/{id} · ADMIN
Edits name, mode, or policy. A policy edit creates or replaces the unpublished draft — published versions are immutable.
POST /v1/mandates/{id}/revoke · ADMIN
Immediate kill switch. New decisions reject with MANDATE_REVOKED, pending
approvals expire, unconsumed reservations release, and live artifacts are
revoked. Returns counts of each.
POST /v1/mandates/{id}/simulate
Dry run. Nothing is persisted, no budget moves.
{ "amount_minor": 120000, "asset": "USDC", "counterparty_external_id": "supplier_acme_cloud", "purpose": "software", "at": "2026-08-04T14:00:00Z", "version": 2 }
Returns { would_decide, reason_codes, trace }. at lets you test schedule
rules; version targets a specific version, including an unpublished draft.
GET /v1/mandates · GET /v1/mandates/{id}
List, or fetch one with its full versions array (policy, policy_hash,
published_at, change_note).
Agents
POST /v1/agents · ADMIN
{
"name": "Procurement Agent",
"external_id": "procurement_agent_12",
"purpose": "Purchase approved software and cloud services",
"principal_type": "ORGANISATION",
"principal_reference": "acme_ltd",
"metadata": {}
}
external_id matches [a-zA-Z0-9_.-]+, unique within the organisation
(duplicate → 409), and is what you pass on every decision.
GET /v1/agents · GET /v1/agents/{id}
The detail view adds mandates and the ten most recent decisions.
PATCH /v1/agents/{id} · ADMIN
Update name, purpose, metadata, or status
(ACTIVE | SUSPENDED | REVOKED). A non-active agent cannot receive an
approved decision — this is the per-agent kill switch.
Counterparties
POST /v1/counterparties · ADMIN
{
"external_id": "supplier_acme_cloud",
"display_name": "Acme Cloud",
"authority_status": "CUSTOMER_APPROVED",
"destination": "0x…",
"type": "supplier",
"category": "cloud_infrastructure",
"external_checks": [
{ "type": "SANCTIONS", "provider": "customer_supplied", "status": "PASSED", "checked_at": "…", "reference": "check_123" }
]
}
authority_status ∈ CUSTOMER_APPROVED | CUSTOMER_PENDING |
CUSTOMER_BLOCKED. The naming is deliberate: this is your assertion of
who you authorise, not a compliance verdict. external_checks results are
stored as supplied — mnd8t performs no screening and certifies none.
The legacy status field remains accepted and is returned as a deprecated
alias.
GET /v1/counterparties · PATCH /v1/counterparties/{id} (ADMIN)
Financial accounts
Provider-neutral references to accounts you control.
POST /v1/financial-accounts · ADMIN
{
"type": "SIMULATED",
"provider": "SIMULATED",
"external_reference": "sim_account_demo_1",
"network": null,
"asset_capabilities": ["USDC"],
"metadata": {}
}
type ∈ SIMULATED | SELF_HOSTED_WALLET | CUSTODIAL_WALLET |
SMART_ACCOUNT | TOKENISED_DEPOSIT_ACCOUNT | BANK_ACCOUNT | OTHER.
mnd8t stores the reference only — never credentials or keys.
GET /v1/financial-accounts · PATCH /v1/financial-accounts/{id} (ADMIN)
POST /v1/mandates/{id}/account-bindings · ADMIN · GET …
{ "financial_account_id": "fac_…", "expires_at": "…" } — one mandate may
govern several accounts.
Approvals
GET /v1/approval-actions/{token} · public
Context for the approval page, reachable without an account: agent,
principal, amount, counterparty, purpose, triggered reasons, remaining
budget per period, mandate expiry, and recent related decisions. Also
reports expired.
POST /v1/approval-actions/{token}/approve · public
POST /v1/approval-actions/{token}/reject · public
{ "comment": "Approved — invoice checked against PO 4471" }
Single use and expiring. Reuse → 409; expired → 409. Approving issues the
authorisation artifact (returned as authorisation_artifact), confirms the
budget reservation, and writes a new receipt. Rejecting releases the
reservation.
An approval authorises one action. It never changes the mandate.
GET /v1/approvals · APPROVER
Organisation-wide list; filter by status (PENDING | APPROVED |
REJECTED | EXPIRED).
Evidence receipts
Not to be confused with the outcome evidence above. A receipt is mnd8t's own signed record of a decision it made; outcome evidence is the customer's record of what happened afterwards. One is signed by us and provable without us; the other is asserted by you.
GET /v1/evidence-receipts/{id}
Returns the signed receipt, its hash, key_id, and the previous receipt
link. Prefer verifying locally — that is the point of the signature.
GET /v1/evidence-receipts/{id}/verify
Server-side check: { id, valid, reason, key_id }. Convenience only.
GET /v1/decision-intents/{id}/receipts
The full hash-chained sequence for one decision, oldest first.
GET /.well-known/mandate-keys.json · public
{ "keys": [ { "key_id": "key_2026_01", "algorithm": "Ed25519", "public_key_pem": "-----BEGIN PUBLIC KEY-----…" } ] }
Match signature.key_id. Format details:
evidence-receipts.md.
Authentication and account
| Endpoint | Notes |
|---|---|
GET /v1/auth/methods |
Which sign-in methods are enabled: { providers: ["google","github"], password_fallback } |
GET /v1/auth/oauth/{provider} |
Starts OAuth (google | github); 302 to the provider |
GET /v1/auth/oauth/{provider}/callback |
Completes sign-in; creates the account and organisation on first use |
POST /v1/auth/signup · POST /v1/auth/login |
Password auth. Disabled in production unless AUTH_PASSWORD_ENABLED=true → 403 |
POST /v1/auth/logout · GET /v1/auth/me |
Session endpoints |
Developers
| Endpoint | Role | Notes |
|---|---|---|
POST /v1/api-keys |
ADMIN | { name, environment }. Secret returned once. |
GET /v1/api-keys |
ADMIN | Metadata only — never the secret |
DELETE /v1/api-keys/{id} |
ADMIN | Immediate; subsequent calls get 401 |
POST /v1/webhook-endpoints |
ADMIN | { url }. Signing secret returned once. |
GET /v1/webhook-endpoints · DELETE /v1/webhook-endpoints/{id} |
ADMIN | List / deactivate |
POST /v1/webhook-endpoints/{id}/test |
ADMIN | Queues a test event |
GET /v1/webhook-deliveries |
ADMIN | History; filter by status |
POST /v1/webhook-deliveries/{id}/retry |
ADMIN | Requeue a failed delivery |
Webhook events
decision.approved · decision.rejected · decision.escalated ·
decision.expired · approval.approved · approval.rejected ·
execution.confirmed · execution.failed · mandate.published ·
mandate.revoked
Headers: mandate-signature: t=<unix>,v1=<hmac-sha256-hex>,
mandate-event-id, mandate-event-type.
Verify HMAC-SHA256 over "<t>.<raw body>" with your endpoint secret; reject
if |now − t| > 300s. At-least-once delivery with exponential backoff (8
attempts, 30s → 6h) — deduplicate on mandate-event-id. Verification code:
developer guide §9.
System
| Endpoint | Notes |
|---|---|
GET /livez |
Liveness. Use this on Cloud Run — Google's frontend intercepts /healthz on run.app domains. |
GET /healthz |
Liveness (direct hosts) |
GET /readyz |
Readiness; checks the database. 503 when down. |
GET /v1/metrics |
Decision counts, approval stats, daily series, funnel |
POST /v1/product-events |
Records quickstart_copied | design_partner_flagged |
Reason codes
Returned in reason_codes on every decision — all triggered codes, not just
the first.
Hard failures → REJECT
AGENT_INACTIVE · MANDATE_NOT_FOUND · MANDATE_NOT_PUBLISHED ·
MANDATE_NOT_EFFECTIVE · MANDATE_EXPIRED · MANDATE_REVOKED ·
OUTSIDE_PERMITTED_SCHEDULE · ASSET_NOT_ALLOWED · COUNTERPARTY_BLOCKED ·
DESTINATION_NOT_AUTHORISED ·
PURPOSE_NOT_ALLOWED · ABSOLUTE_LIMIT_EXCEEDED · DAILY_BUDGET_EXCEEDED ·
MONTHLY_BUDGET_EXCEEDED · ASSET_ABSOLUTE_LIMIT_EXCEEDED ·
ASSET_DAILY_BUDGET_EXCEEDED · ASSET_MONTHLY_BUDGET_EXCEEDED ·
VALUATION_NOT_ASSERTED
Approval triggers → ESCALATE
COUNTERPARTY_NOT_APPROVED · DESTINATION_UNVERIFIED · HUMAN_APPROVAL_REQUIRED
Informational
APPROVED_WITHIN_MANDATE · SHADOW_ONLY
Precedence: any hard failure → REJECT; otherwise any approval trigger →
one ESCALATE carrying every triggered reason; otherwise APPROVE. Full
evaluation order: policy-model.md.