Issue your first closed mandate
Issue a closed mandate, record consent, verify the intended charge, hand it to settlement, and inspect the evidence.
In short
7 calls, in the order this page makes themFor people and agents- 1POST
/v1/mandateIssue a mandate - 2POST
/v1/mandate/{mandate_id}/confirmConfirm a mandate - 3POST
/v1/mandate/verifyVerify a token against a charge - 4GET
/v1/mandate/{mandate_id}Retrieve a mandate - 5GET
/v1/mandate/{mandate_id}/evidenceEvidence pack - 6GET
/v1/mandateList a user's mandates - 7POST
/v1/mandate/{mandate_id}/revokeRevoke a mandate
The same list heads this page's Markdown:/mandate/getting-started-mandates.md
In this tutorial, a user authorizes an agent to book one itinerary from Demo Travel for at most USD 1,800. You will create the proposal, record consent, verify the intended USD 1,700 charge, and inspect the audit trail.
The partner side stops before exercise. Settlement receives the signed token and exact charge, then performs the state-changing exercise when it commits the payment.
Before you start
Every request in this tutorial goes through the OEX API gateway:
export OEX_API_KEY="<your API key>"
export OEX_BASE_URL="https://api.oex.sh"Amounts are integer minor units: 180000 USD is USD 1,800.00. The route prefix is /v1/mandate, singular.
1. Propose the authority
Issue creates a draft. It does not create a signed payment authority yet.
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": "closed",
"payee": {
"id": "merchant_demo",
"name": "Demo Travel"
},
"payment_amount": {
"currency": "USD",
"amount": 180000
},
"payment_instrument": {
"id": "pi_demo_visa",
"type": "card",
"description": "Demo Visa ending 4242"
},
"agent_id": "agent_demo",
"expires_in_seconds": 3600,
"constraints": [
{
"type": "payment.amount_range",
"currency": "USD",
"min": 100000,
"max": 180000
},
{
"type": "payment.allowed_payees",
"allowed": [{ "id": "merchant_demo" }]
}
],
"evidence": {
"source": "llm_agent",
"presented": {
"route": "SIN-NRT",
"departure_date": "2026-09-10",
"merchant": "Demo Travel",
"maximum": "USD 1,800.00"
},
"interaction": [
"Book this itinerary with Demo Travel if it stays under USD 1,800."
],
"context": "The user reviewed the merchant, itinerary and maximum total.",
"confidence_score": 0.98
}
}'The response is a pending proposal:
{
"id": "mdt_0123456789abcdef0123456789abcdef",
"status": "pending",
"claims_preview": {
"iss": "https://mandates.oex.dev",
"sub": "usr_demo_123",
"jti": "mdt_0123456789abcdef0123456789abcdef",
"vct": "mandate.payment.1",
"mode": "closed",
"payee": { "id": "merchant_demo", "name": "Demo Travel" },
"payment_amount": { "currency": "USD", "amount": 180000 },
"payment_instrument": { "id": "pi_demo_visa", "type": "card", "description": "Demo Visa ending 4242" },
"constraints": [
{ "type": "payment.amount_range", "currency": "USD", "min": 100000, "max": 180000 },
{ "type": "payment.allowed_payees", "allowed": [{ "id": "merchant_demo" }] }
],
"agent_id": "agent_demo"
},
"confirm_expires_at": "2026-08-23T09:15:00+00:00"
}Save id and confirm_expires_at. The preview has no iat, exp, consent_method, or token because the user has not confirmed it.
evidence is the consent trail and is stored for audit only; none of it is evaluated. presented is what the user saw, interaction the messages that led to consent, and confidence_score (0 to 1) how sure the gathering agent is that the user meant it.
A 422 means the proposal was invalid and nothing was created. Fix the field named in detail rather than retrying the same body.
2. Present the terms and confirm
Show the user the merchant, maximum amount, instrument, expiry, and constraints. Confirm only after the user accepts those exact terms.
The optional confirmation object is stored in the event history. It does not enter the JWT.
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": "sms",
"message_id": "msg_demo_42",
"confirmed_by": "usr_demo_123"
}
}'{
"confirmed": true,
"id": "mdt_0123456789abcdef0123456789abcdef",
"token": "<mandate-jwt>",
"claims": {
"iss": "https://mandates.oex.dev",
"sub": "usr_demo_123",
"jti": "mdt_0123456789abcdef0123456789abcdef",
"vct": "mandate.payment.1",
"mode": "closed",
"payee": { "id": "merchant_demo", "name": "Demo Travel" },
"payment_amount": { "currency": "USD", "amount": 180000 },
"payment_instrument": { "id": "pi_demo_visa", "type": "card", "description": "Demo Visa ending 4242" },
"constraints": [
{ "type": "payment.amount_range", "currency": "USD", "min": 100000, "max": 180000 },
{ "type": "payment.allowed_payees", "allowed": [{ "id": "merchant_demo" }] }
],
"agent_id": "agent_demo",
"consent_method": "partner_attested",
"iat": 1787475600,
"exp": 1787479200
},
"status": "active"
}Require confirmed: true and retain token exactly as returned.
If the proposal was already confirmed, revoked, or allowed to pass its confirmation window, the endpoint still returns HTTP 200 but sets confirmed: false, includes a reason, and returns no token. Branch on confirmed, never on the HTTP status.
3. Verify the exact intended charge
The itinerary now costs USD 1,700. Verify that exact amount and merchant before handing the payment to settlement.
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": 170000,
"currency": "USD",
"payee_id": "merchant_demo",
"at": "2026-08-23T09:30:00+00:00"
}
}'{
"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": "single_use", "ok": true, "reason": "not yet exercised" },
{ "check": "agent_binding", "ok": true, "reason": "caller matches mandate agent" },
{ "check": "payment.amount_range", "ok": true, "reason": "within amount range" },
{ "check": "payment.allowed_payees", "ok": true, "reason": "payee allowed" }
]
}valid is true only when every check has ok: true. Verification is advisory and repeatable; it records a verified event but does not reserve budget, charge the instrument, or exercise the mandate.
Do not omit charge for a payment decision. Without it, the service can validate the token and lifecycle but cannot evaluate constraints, currency, or amount.
4. Hand off to settlement
Send settlement:
- the mandate ID,
- the unchanged JWT,
- the same charge you verified, and
- your reconciliation reference.
The partner API does not need to perform exercise. Keeping the state-changing operation with settlement prevents the agent that requested authority from declaring its own payment final.
Settlement exercises the mandate as part of committing the charge:
curl -X POST "$OEX_BASE_URL/v1/mandate/$MANDATE_ID/exercise" \
-H "content-type: application/json" \
-H "x-api-key: $OEX_API_KEY" \
-d '{
"token": "<mandate-jwt>",
"charge": {
"amount": 170000,
"currency": "USD",
"payee_id": "merchant_demo"
},
"reference_id": "pay_demo_001"
}'{
"exercised": true,
"mandate_id": "mdt_0123456789abcdef0123456789abcdef",
"status": "exercised",
"checks": [
{ "check": "signature", "ok": true, "reason": "valid ES256 signature" },
{ "check": "single_use", "ok": true, "reason": "not yet exercised" },
{ "check": "jti_match", "ok": true, "reason": "token jti matches path" }
]
}checks is abridged here; exercise runs the same checks as verify, then jti_match. Require exercised: true. A second exercise of a closed mandate fails single_use. reference_id is your reconciliation reference; it is stored, not used for deduplication.
5. Read the result and evidence
Retrieve the mandate after settlement:
curl "$OEX_BASE_URL/v1/mandate/$MANDATE_ID" \
-H "x-api-key: $OEX_API_KEY"Its computed status is now exercised after a successful settlement. To inspect the consent trail and event history:
curl "$OEX_BASE_URL/v1/mandate/$MANDATE_ID/evidence" \
-H "x-api-key: $OEX_API_KEY"The evidence pack ties together:
- the terms presented at issue,
- confirmation metadata,
- the signed claims and token,
- each verification attempt, and
- the final exercise event.
Use GET /v1/mandate?user_id=usr_demo_123 to list a user's mandates, newest first. Pending entries have draft claims and token: null.
Revoke when the authority should end early
Revoke works before or after confirmation and is idempotent:
curl -X POST "$OEX_BASE_URL/v1/mandate/$MANDATE_ID/revoke" \
-H "x-api-key: $OEX_API_KEY"{
"id": "mdt_0123456789abcdef0123456789abcdef",
"status": "revoked",
"already_revoked": false
}Later calls return already_revoked: true. If exercise happened first, subsequent reads remain exercised because exercise has higher status precedence.
Where to go next
- Use an open mandate for a reusable delegated budget.
- Choose mandate constraints for every supported limit.
- Handle checks, refusals and errors for production control flow.
- Design choices and AP2 for the reasoning behind the lifecycle.