# Mandate

One single-use authorization per payment, its signed claims, and the ledger that tracks what it consumed.

Status: Live · Updated: 2026-09-25
Source: https://enruta.ai/docs/mandate

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.

| 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](https://enruta.ai/docs/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:

```json
{
  "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:

```json
"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](https://enruta.ai/docs/accept-middleware) accept it. The sealed record’s `mandate` carries `retry_policy`, `visibility_scope` and `hidden_constraints_digest`. [enruta-verify](https://enruta.ai/docs/verification) 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](https://enruta.ai/docs/decision#retrying).

## 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.

```text
// 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

| 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

```text
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](https://enruta.ai/docs/rails)).
