Docs Concepts
Mandate
LiveUpdated
A mandate is the proof that a specific payment was authorized: by which policy, by which person if one was needed, for which exact request. It is immutable once signed; what moves is the ledger Enruta keeps for it.
Properties
- Verified at issuance. All checks run when the mandate is created. The payment path only looks up and counts, so it adds under 10 ms inside a rail’s authorization window.
- Single use. A
consumedstate moves fromissuedtoconsumed,expiredorvoidexactly once. One consent cannot pay twice. - Bound to the request. The mandate carries the hash of the canonical request: payee, amount, currency, purpose, checkout reference. A different request needs a different mandate.
- Short lived. Default expiry 15 minutes; configurable per policy down to seconds.
- Idempotent. Every request carries an idempotency key. A retried tool call returns the same decision and the same mandate; it never mints a second one.
The signed claims
The mandate is an ES256 compact JWS, header { alg, kid: "key_enruta_mandate_2026-09", typ: "enruta-mandate+jwt" }. It is the ap2.checkout_mandate a UCP merchant receives in complete_checkout; the merchant verifies it with the keys of Enruta’s platform profile.
| Claim | Example | Meaning |
|---|---|---|
iss | https://enruta.ai | Issuer |
sub | org_halvern | The principal |
mandate_id | mnd_01M2512M8FQ7ZK4T1V9W3N6BXC | |
agent | agt_procurement_02 | |
binds | sha256:9f1c…2e4a | The request hash; same value as approval.binds and request.hash |
amount, currency | "24.98", "USD" | The exact amount |
payee | reamandrule.com | The payee’s domain |
checkout | { protocol: "ucp", reference: "chk_01M2511XQ4N7B8T2R6H9K3W5MD" } | |
checkout_hash | sha256:… | JCS digest of the merchant-signed checkout, so the merchant can tell it was not altered |
policy | { id: "pol_office_supplies", version: 4 } | New in this release |
mode, refill_policy, finality_class | bounded_stepup, never, reversible | New in this release; the cap itself is not in the JWS |
principal_login_id | halvern/dana.reyes@halvernrobotics.com | New in this release: plain text, stable, recognizable by the cardholder |
retry_policy | { max_attempts: 3, window_seconds: 86400, same_amount_only: true } | How a failed payment may be tried again, when the policy sets retry_policy |
hidden_constraints_digest | sha256:5b0e…41a7 | Present when the policy’s visibility_scope keeps constraints the mandate carries from the merchant: a commitment to their values, which stay in the record bundle |
iat, exp | 1789048800, 1789049700 | Default 15 minutes |
The JWS carries what the merchant may see. Without a visibility_scope a mandate is issued as the table shows, and the ceilings (per_payment_cap, budget, the tolerated deviation, a recurrence’s caps) are not in it. With one, every constraint the scope keeps from the merchant leaves the JWS and is committed to by hidden_constraints_digest, and a ceiling the scope names under merchant joins the claims (per_payment_cap: { amount, currency }, budget: { amount, currency, window }).
Mode, refill policy, finality class, deviation
| Field | Values | Set from |
|---|---|---|
mode | specified · bounded · bounded_stepup | The policy’s mode; bounded_stepup when absent |
refill_policy | never · on_void_only · on_settled_refund · on_any_reversal | The policy; decides the mandate_effect of a reversal |
finality_class | reversible · conditionally_reversible · irreversible | The rail at issuance: sandbox and stripe_issuing are reversible, x402 is irreversible |
deviation{tolerance, action} | { "0.05", "pause_and_notify" } | The policy |
policy{id, version} | The policy version the decision was made under | |
retry_policy | { max_attempts, window_seconds, same_amount_only } or null | The policy’s retry_policy, as signed into the mandate |
visibility_scope | { merchant[], issuer_only[] } or null | The policy’s visibility_scope; null when it declares none and the mandate is issued as before |
hidden_constraints_digest | sha256: digest or null | Computed at issuance over the constraints kept from the merchant and a random salt |
Retry policy and what the merchant sees
A policy with a $500 cap, a $1,000 monthly budget, retry_policy: { max_attempts: 3, window_seconds: 86400, same_amount_only: true } and visibility_scope: { merchant: ["retry_policy"], issuer_only: ["per_payment_cap", "budget", "policy"] } issues this mandate to the merchant:
{
"iss": "https://enruta.ai", "sub": "org_halvern", "mandate_id": "mnd_01M3F2…", "agent": "agt_procurement_02", "binds": "sha256:9f1c…2e4a",
"amount": "77.04", "currency": "USD", "payee": "reamandrule.com",
"checkout": { "protocol": "ucp", "reference": "chk_01M3F1…" }, "checkout_hash": "sha256:…",
"mode": "bounded_stepup", "refill_policy": "never", "finality_class": "reversible",
"principal_login_id": "halvern/dana.reyes@halvernrobotics.com",
"retry_policy": { "max_attempts": 3, "window_seconds": 86400, "same_amount_only": true },
"hidden_constraints_digest": "sha256:5b0e…41a7",
"iat": 1789048800, "exp": 1789049700
}The policy reference, the cap and the budget are not in it. Their values, with a random salt, stay with the principal in the record’s bundle; the API’s mandate object (GET /v0/mandates/{id}) shows retry_policy, visibility_scope and hidden_constraints_digest, never the salt or the hidden values:
"artifacts": {
"hidden_constraints": {
"salt": "h3Xq…Zp0w",
"constraints": {
"policy": { "id": "pol_office_supplies", "version": 4 },
"per_payment_cap": { "amount": "500.00", "currency": "USD" },
"budget": { "amount": "1000.00", "currency": "USD", "window": "calendar_month" }
}
}
}
// hidden_constraints_digest = sha256 over the RFC 8785 form of { constraints, salt }The merchant checks its charge against the amount, the payee and the checkout, which it always sees, and verifies the mandate as before: POST /v0/sellers/verify and the Accept middleware accept it. The sealed record’s mandate carries retry_policy, visibility_scope and hidden_constraints_digest. enruta-verify recomputes the digest from the bundle and checks that no hidden constraint also travels as a claim and that the record’s terms are the ones signed, so a changed cap fails the keys check (hidden constraints do not match the mandate’s hidden_constraints_digest); a bundle without the hidden values, as a merchant would hold it, is reported rather than failed. Constraints the policy does not set are not committed to, and a scope that keeps nothing the mandate carries adds no digest. What counts as a retry, and what retryable means on a decision: Decision.
The mandate account
The mandate does not change after signing. Enruta keeps a ledger for it (mandate.ledger in the record, GET /v0/mandates/{id}), in decimal strings:
| Field | Meaning | Moves when |
|---|---|---|
ceiling | The amount at issuance | Never |
reserved | Credential minted, rail not yet authorized | Issue: = amount. Rail authorization, void, expiry or hand-off: = 0 |
authorized | Rail authorized, not yet captured | Authorization approved: = amount. Capture: = 0 |
captured | Captured or settled by the rail | Settlement captured / settled: = amount |
refilled | Given back by a completed reversal | mandate_effect is refill or refill_partial; refill_pending_settlement waits for the rail to confirm |
consumed | reserved + authorized + captured − refilled | Derived |
remaining | ceiling − consumed | Derived |
The budget in the policy follows the same events: a refilled amount is subtracted from what the window has spent, never below zero.
// the $24.98 purchase of 2026-09-10, then a $9.99 refund three days later, refill_policy on_settled_refund
issued ceiling 24.98 reserved 24.98 authorized 0.00 captured 0.00 refilled 0.00 remaining 0.00
authorized ceiling 24.98 reserved 0.00 authorized 24.98 captured 0.00 refilled 0.00 remaining 0.00
captured ceiling 24.98 reserved 0.00 authorized 0.00 captured 24.98 refilled 0.00 remaining 0.00
refund 9.99 ceiling 24.98 reserved 0.00 authorized 0.00 captured 24.98 refilled 9.99 remaining 9.99
// the single-use mandate cannot be spent again; the $9.99 returns to the policy's monthly budgetThe same words, “$1,000 a month”, mean four different things on four card issuers (some refill on refunds, some only on reversals, some cap clearing, some cap authorization). So the truth about a limit lives in the mandate account, and the rail’s limit is a fuse. When the two disagree, reconciliation raises RAIL_LIMIT_SEMANTICS_DIFF.
Human approval
When a policy requires a person, the mandate is not issued until that person signs. The signature is a passkey assertion or an authenticated link, and it binds the request hash. A chat reaction or an e-mail reply is not an approval; the record would say so. A refund above reversal.step_up_above is approved the same way, bound to the reversal’s subject hash.
An approval never leaves a payment half done. If, after the person signs, the mandate cannot be issued because of a failure inside Enruta (the database, the key store, a rail connection missing its stored credentials), the approval is rolled back: it is pending again, the request waits for approval with no mandate, nothing is sealed, no approval.completed is sent and approval.rolled_back is logged, so the person can approve again once it is fixed. A rail that fails while issuing hands the payment to a person instead, and the approval stands.
People stay in charge while an approval waits. When the person approves, the agent’s pause and the consent the decision relied on are checked again. If the agent was paused, or the user revoked that consent, no mandate is issued: the approval expires, the request closes expired with agent_paused or consent_revoked (not retryable), the budget reservation is released, the record is sealed, approval.completed carries the reason, and the person is told “This payment can no longer be approved”.
Compiled into the rail
| Rail | The mandate becomes | Status |
|---|---|---|
| Stripe Issuing (your account) | A single-use virtual card: amount = mandate amount, merchant lock, expiry = mandate expiry. The real-time authorization request is answered against the mandate. | Live |
| Sandbox | A token answered from Enruta’s own database; capture on fulfillment or two minutes after authorization. | Live |
| UCP / AP2 checkout | The ap2.checkout_mandate proof the merchant requires to complete the checkout. | Live |
| Mercury | A card or payment limited to the mandate, with the mandate id in the note. | Code complete; needs your Mercury account |
| Ramp | A virtual card whose total spend limit equals the mandate. | Code complete; needs your Ramp account |
| Bill.com | A read-only evidence source; an approval scoped to one bill when a trusted session is configured. | Code complete; needs your Bill.com account |
| Stripe Shared Payment Token | A token whose usage limits (currency, maximum amount, expiry) are the mandate’s, for the named seller. | Code complete; needs Stripe’s agent access on your account |
| Link | A Link spend request for the mandate. | Code complete; needs Stripe’s agent access on your account |
| x402 | An EIP-3009 authorization signed by your own signer, with the value, the recipient and the deadline held to the mandate. | Live |
| MPP | A payment session and receipt per the public MPP specification; the receipt is sealed. | Live |
Lifecycle
requested ─▶ decided ─▶ issued ─▶ consumed ─▶ (settlement: captured ─▶ settled)
│ ├─▶ expired
│ └─▶ void (handed off, or cancelled by a reversal before authorization)
├─▶ observe ─▶ issued … (shadow mode)
├─▶ step_up ─▶ approved ─▶ issued …
├─▶ not issued: the rail hands the payment to a person (handed_off)
└─▶ deniedHow a payment ends
Three ways. The agent presents the mandate and the credential to the merchant itself (Claude Code, Codex, any host that may carry the credential). Enruta presents them for it (complete_checkout: true, for hosts that block the credential). Or the merchant does not accept the credential from an agent at all: merchants that complete checkout only on their own page. Then the agent hands the person the merchant’s continue_url and tells Enruta (hand_off_checkout, or POST /v0/payment-requests/{id}/handoff): the mandate is voided, the request moves to handed_off, and once the person has paid the agent reports the order. The record seals with that order as a self-asserted receipt and outcome completed; without one it seals 15 minutes after the hand-off in state handed_off.
A payment can also end before a mandate exists. When the policy allows it (or a person approves it) but the rail cannot issue a credential, the request moves to handed_off with mandate: null and a handoff that names the rail, its reason, its message and the next_step for a person: Link above its 500 USD limit, a read-only Bill.com connection, or a rail that fails while it issues (rail_unavailable, rail_error). Nothing was paid, the budget reservation is released, and the record seals at once with that outcome (Rails).