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_byCUSTOMER_EXECUTOR | CUSTOMER_API | PROVIDER_WEBHOOK | MANDATE_RECONCILIATION. verification.methodCUSTOMER_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: ISSUEDCLAIMEDEXECUTED, 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_statusCUSTOMER_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": {}
}

typeSIMULATED | 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=true403
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.

30 min read · Need something that isn't here? Browse all documentation