Docs Concepts

Mandate

LiveUpdated

View as Markdown

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 consumed state moves from issued to consumed, expired or void exactly 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.

ClaimExampleMeaning
isshttps://enruta.aiIssuer
suborg_halvernThe principal
mandate_idmnd_01M2512M8FQ7ZK4T1V9W3N6BXC
agentagt_procurement_02
bindssha256:9f1c…2e4aThe request hash; same value as approval.binds and request.hash
amount, currency"24.98", "USD"The exact amount
payeereamandrule.comThe payee’s domain
checkout{ protocol: "ucp", reference: "chk_01M2511XQ4N7B8T2R6H9K3W5MD" }
checkout_hashsha256:…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_classbounded_stepup, never, reversibleNew in this release; the cap itself is not in the JWS
principal_login_idhalvern/dana.reyes@halvernrobotics.comNew 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_digestsha256:5b0e…41a7Present 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, exp1789048800, 1789049700Default 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

FieldValuesSet from
modespecified · bounded · bounded_stepupThe policy’s mode; bounded_stepup when absent
refill_policynever · on_void_only · on_settled_refund · on_any_reversalThe policy; decides the mandate_effect of a reversal
finality_classreversible · conditionally_reversible · irreversibleThe 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 nullThe policy’s retry_policy, as signed into the mandate
visibility_scope{ merchant[], issuer_only[] } or nullThe policy’s visibility_scope; null when it declares none and the mandate is issued as before
hidden_constraints_digestsha256: digest or nullComputed 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:

FieldMeaningMoves when
ceilingThe amount at issuanceNever
reservedCredential minted, rail not yet authorizedIssue: = amount. Rail authorization, void, expiry or hand-off: = 0
authorizedRail authorized, not yet capturedAuthorization approved: = amount. Capture: = 0
capturedCaptured or settled by the railSettlement captured / settled: = amount
refilledGiven back by a completed reversalmandate_effect is refill or refill_partial; refill_pending_settlement waits for the rail to confirm
consumedreserved + authorized + captured − refilledDerived
remainingceiling − consumedDerived

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 budget

The 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

RailThe mandate becomesStatus
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
SandboxA token answered from Enruta’s own database; capture on fulfillment or two minutes after authorization.Live
UCP / AP2 checkoutThe ap2.checkout_mandate proof the merchant requires to complete the checkout.Live
MercuryA card or payment limited to the mandate, with the mandate id in the note.Code complete; needs your Mercury account
RampA virtual card whose total spend limit equals the mandate.Code complete; needs your Ramp account
Bill.comA 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 TokenA 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
LinkA Link spend request for the mandate.Code complete; needs Stripe’s agent access on your account
x402An EIP-3009 authorization signed by your own signer, with the value, the recipient and the deadline held to the mandate.Live
MPPA 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)
                 └─▶ denied

How 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).