Docs
Get an API key

Handle checks, refusals and errors

Separate transport success from authorization, recover from failed checks, and use the evidence trail safely.

In short

1 callFor people and agents
  1. 1GET/v1/mandate/{mandate_id}/evidenceEvidence pack

The same list heads this page's Markdown:/mandate/checks-results-and-errors.md

Mandate responses answer two separate questions:

  1. Could the API understand and process the request? The HTTP status answers this.
  2. Does the authority permit the requested action? A result boolean and checks answer this.

A robust integration handles both layers. HTTP 200 alone is never proof that the user authorized a charge.

The control flow

For issue:

  1. require HTTP 201,
  2. save id and confirm_expires_at.

For confirm:

  1. require HTTP 200,
  2. require confirmed: true,
  3. save token unchanged.

For verify:

  1. require HTTP 200,
  2. require valid: true,
  3. require every returned check to have ok: true.

Settlement applies the same pattern to exercise with exercised: true.

Do not collapse transport success and authorization into one “success” branch. Log the result boolean, failed checks, mandate ID, and reconciliation reference separately.

HTTP status reference

StatusMeaningAction
200The request was processed. The body may still be a refusal.Read the operation boolean, reason, and checks.
201A mandate was issued.Save id and confirm_expires_at; the mandate is still pending.
401The API key or bearer token is missing or invalid.Fix the credential; the request never reached the service.
404The path mandate ID does not exist.Stop retrying that ID.
422Body or query validation failed.Fix the field identified by detail; nothing was created.

Successful responses do not use a shared envelope. The list endpoint returns { "user_id", "mandates" }; other operations return their resource or result directly.

Confirmation refusals

Confirm returns HTTP 200 with confirmed: false and no token when the draft cannot become authority:

reasonReturned statusRecovery
already confirmedUsually activeRetrieve the mandate and use its existing token.
revokedrevokedIssue a new mandate only after obtaining new user intent.
confirmation window expiredexpiredIssue a new proposal and present it again.

Concurrent confirmation requests are serialized by a database uniqueness rule. One wins; the other receives already confirmed.

Verification order

Token, lifecycle, binding, constraint, and amount checks run in a fixed order. Exercise adds jti_match after the shared pipeline.

For a token that decodes successfully, checks follow this order:

OrderCheckFailure means
1signatureThe token is not a valid ES256 token for the configured issuer and key.
2expiryThe JWT exp is in the past.
3knownThe token's jti is not a mandate in this service.
4confirmedNo confirmation event exists.
5revocationA revocation event exists.
6single_useClosed only: an exercise event already exists. Open mandates skip this check.
7agent_bindingThe mandate names an agent_id and the authenticated caller does not match it.
8one per constraintThe supplied charge violates a rule. Omitted when no charge is supplied.
9currency, amount, or budgetThe charge conflicts with top-level payment_amount. Added only on failure.
10jti_matchExercise only: the token ID differs from the mandate ID in the path.

Open responses also carry spent and remaining.

Invalid or expired tokens stop early:

{
  "valid": false,
  "checks": [
    {
      "check": "signature",
      "ok": false,
      "reason": "<JWT validation error>"
    }
  ]
}

An expired token returns an expiry failure instead. Neither case includes mandate_id, and neither records a verified event.

Read the whole check set

A refusal can contain successful checks alongside the one that matters:

{
  "valid": false,
  "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": false, "reason": "revoked" },
    { "check": "single_use", "ok": true, "reason": "not yet exercised" }
  ]
}

Do not stop at the first ok: true, and do not infer validity from the presence of mandate_id. Require the top-level boolean and every check.

Recovery by failed check

Failed checkWhat to do
signatureReject the token. Do not edit or re-encode it. Confirm you received it from the expected environment.
expiryObtain new user authority; do not extend the old token locally.
knownReject it in this environment. Check that issuer, database and deployment match.
confirmedComplete the user-confirmation flow before payment.
revocationStop. New user intent requires a new mandate.
single_useTreat the closed mandate as already settled; reconcile rather than retrying exercise.
agent_bindingUse the authenticated agent that owns the mandate or obtain a new mandate with the intended binding.
constraint nameCorrect the charge or obtain authority with different user-approved terms.
currency / amount / budgetCorrect the charge or obtain a new/expanded mandate.
jti_matchPair the token with its own mandate ID.

Never “fix” a refusal by deleting a constraint, changing claims, or substituting a different charge after confirmation.

Retry and idempotency boundaries

  • Issue is not idempotent. Repeating it creates another mandate ID.
  • Confirm can succeed once. A repeat returns confirmed: false with already confirmed.
  • Verify is repeatable, but not invisible. Every decodable token records a verified event, even if a later check fails.
  • Revoke is idempotent. Repeats return already_revoked: true.
  • Closed exercise succeeds once. If the client loses the response, retrieve status and reconcile before retrying.
  • Open exercise may succeed many times. reference_id is stored for reconciliation but is not enforced as an idempotency key.

That last point matters: settlement must provide its own duplicate-protection strategy around network retries.

Audit trail

GET /v1/mandate/{id}/evidence returns:

  • the draft or confirmed claims,
  • the confirmed token when one exists,
  • evidence supplied at issue, and
  • ordered lifecycle events.

issued and revoked events may have detail: null. Non-null event details are returned as JSON-encoded strings, so consumers that need their structured contents must parse detail a second time.

A successful token decode records verified even when a later lifecycle or constraint check fails. Closed settlement records exercised; open settlement records charged. Together, these events show what the user approved, what callers attempted, and what settlement committed.

Continue

On this page

Endpoints on this page

GETgetMandateEvidence