Docs
Get an API key

Use an open mandate

Delegate a reusable budget with allowed payees, category limits, charge counts, and race-safe settlement.

In short

4 calls, in the order this page makes themFor people and agents
  1. 1GET/v1/mandate/categoriesCharge categories
  2. 2POST/v1/mandateIssue a mandate
  3. 3POST/v1/mandate/{mandate_id}/confirmConfirm a mandate
  4. 4POST/v1/mandate/verifyVerify a token against a charge

The same list heads this page's Markdown:/mandate/open-mandates.md

A closed mandate approves one purchase. An open mandate delegates a budget that settlement can draw down across several purchases.

Consider a trip-planning agent. The user is comfortable delegating up to USD 3,000, but only for two known travel merchants, with no more than USD 1,800 spent on flights and no more than three charges overall. An open mandate expresses that authority without giving the agent an unlimited payment credential.

An open mandate records each successful charge, reports spent and remaining, and becomes exhausted when the total budget reaches zero.

When open mode fits

Choose mode: "open" when all of these are true:

  • the user intends to authorize more than one charge,
  • the charges belong to one bounded purpose or period,
  • a total budget can express the outer limit, and
  • payees or categories can be constrained tightly enough for the use case.

Use a closed mandate when the user is approving one known purchase. Do not use an open mandate as a long-lived substitute for ordinary account authorization.

1. Read the category taxonomy

Category-scoped constraints use the service taxonomy:

export OEX_API_KEY="<your API key>"
export OEX_BASE_URL="https://api.oex.sh"

curl "$OEX_BASE_URL/v1/mandate/categories" \
  -H "x-api-key: $OEX_API_KEY"
{
  "categories": ["flight", "hotel", "retail", "dining", "transport", "misc"]
}

Fetch this list rather than hard-coding it. Issue rejects an unknown category with HTTP 422.

2. Issue the budget

In open mode, payment_amount is the total budget across all successful charges. payee is optional; use payment.allowed_payees when the budget may be spent with more than one merchant.

Request
curl -X POST "$OEX_BASE_URL/v1/mandate" \
  -H "content-type: application/json" \
  -H "x-api-key: $OEX_API_KEY" \
  -d '{
    "user_id": "usr_demo_123",
    "mode": "open",
    "payment_amount": {
      "currency": "USD",
      "amount": 300000
    },
    "payment_instrument": {
      "id": "pi_demo_visa",
      "type": "card",
      "description": "Demo Visa ending 4242"
    },
    "agent_id": "agent_demo",
    "expires_in_seconds": 604800,
    "constraints": [
      {
        "type": "payment.allowed_payees",
        "allowed": [
          { "id": "airline_demo" },
          { "id": "hotel_demo" }
        ]
      },
      {
        "type": "payment.category_budget",
        "category": "flight",
        "currency": "USD",
        "max": 180000
      },
      {
        "type": "payment.max_charges",
        "max": 3
      }
    ],
    "evidence": {
      "source": "llm_agent",
      "presented": {
        "purpose": "Tokyo trip",
        "total_budget": "USD 3,000.00",
        "flight_budget": "USD 1,800.00",
        "allowed_merchants": ["airline_demo", "hotel_demo"],
        "maximum_charges": 3
      },
      "interaction": [
        "Book my flight and hotel within a total budget of USD 3,000."
      ],
      "context": "The user reviewed the total, merchant list and flight sub-budget.",
      "confidence_score": 0.99
    }
  }'

The response is pending, just like a closed mandate. Present the full budget and every constraint to the user, then confirm before confirm_expires_at.

export MANDATE_ID="mdt_0123456789abcdef0123456789abcdef"

curl -X POST "$OEX_BASE_URL/v1/mandate/$MANDATE_ID/confirm" \
  -H "content-type: application/json" \
  -H "x-api-key: $OEX_API_KEY" \
  -d '{
    "confirmation": {
      "channel": "app",
      "confirmed_by": "usr_demo_123"
    }
  }'

Require confirmed: true and retain the returned token unchanged.

3. Verify the first charge

The first intended charge is a USD 800 flight from the allowed airline:

Request
curl -X POST "$OEX_BASE_URL/v1/mandate/verify" \
  -H "content-type: application/json" \
  -H "x-api-key: $OEX_API_KEY" \
  -d '{
    "token": "<mandate-jwt>",
    "charge": {
      "amount": 80000,
      "currency": "USD",
      "payee_id": "airline_demo",
      "category": "flight",
      "at": "2026-08-23T10:00:00+00:00"
    }
  }'
Response
{
  "valid": true,
  "mandate_id": "mdt_0123456789abcdef0123456789abcdef",
  "checks": [
    { "check": "signature", "ok": true, "reason": "valid ES256 signature" },
    { "check": "expiry", "ok": true, "reason": "not expired" },
    { "check": "known", "ok": true, "reason": "issued by this service" },
    { "check": "confirmed", "ok": true, "reason": "user confirmed" },
    { "check": "revocation", "ok": true, "reason": "not revoked" },
    { "check": "agent_binding", "ok": true, "reason": "caller matches mandate agent" },
    { "check": "payment.allowed_payees", "ok": true, "reason": "payee allowed" },
    { "check": "payment.category_budget", "ok": true, "reason": "within category budget for flight" },
    { "check": "payment.max_charges", "ok": true, "reason": "charge 1 of 3" }
  ],
  "spent": 0,
  "remaining": 300000
}

Verify reports the ledger before settlement. The charge is valid, but it has not happened yet, so spent is still 0.

Send the exact token and charge to settlement. A successful exercise appends a charged event. The mandate remains active, with spent: 80000 and remaining: 220000.

4. Draw down the budget

Suppose settlement later exercises:

  1. USD 800 for airline_demo, category flight
  2. USD 1,200 for hotel_demo, category hotel
  3. USD 1,000 for hotel_demo, category hotel

After each successful exercise, the total changes:

ChargeAddedspentremainingStatus
FlightUSD 80080000220000active
Hotel depositUSD 1,200200000100000active
Hotel balanceUSD 1,0003000000exhausted

spent and remaining are returned in minor units, as shown.

The flight charge is also counted against the USD 1,800 category budget. Hotel charges do not consume that sub-budget, but all three charges consume the total USD 3,000 budget and the three-charge limit.

Once remaining reaches 0, computed status becomes exhausted. Further verification or exercise fails the budget or charge-count checks.

Why verify is advisory

Verify reads current spend without locking the mandate. Another charge may settle after verification and before this charge reaches settlement.

Exercise closes that race:

  1. lock the mandate row,
  2. recalculate total spend and stateful constraints,
  3. reject the charge if any limit would be exceeded,
  4. otherwise append the charged event.

Treat verify as an early answer for user experience and orchestration. Treat successful exercise as the final answer for settlement.

Category rules fail closed

If a mandate has a category-scoped constraint, every relevant charge needs a category.

  • Missing category: the scoped check fails.
  • Different category: that specific category constraint is not applicable; the total budget and other constraints still apply.
  • Unknown category on issue: HTTP 422.

The charge's category is compared exactly, so it should come from the payment path you trust, not from the agent.

This prevents an omitted label from silently bypassing a sub-budget.

Revoke or let time end the authority

An open mandate stays usable until one of four things happens:

  • the total budget is exhausted (status exhausted),
  • a stateful limit such as payment.max_charges refuses the next charge,
  • the signed token expires (status expired), or
  • the mandate is revoked (status revoked).

Only the budget changes the status. A mandate that has used all its payment.max_charges with budget left still reads active; the next verify or exercise fails that check. Revoke it if you want the status to say it is over.

Revocation is the right choice when the trip is cancelled or the delegated purpose ends before the budget is used.

Continue

On this page