Dual control
Every mandate binds one signer — a hosted payment agent’s, or any signer named directly at creation (Direct Control). Approving, and later cancelling, requires a recognized signer of the mandate’s customer other than the bound one:- The bound signer can never activate its own authority — a second identity must sign.
- The same rule applies to cancellation: the bound signer cannot mutate its own mandate, not even to reduce it.
- “Recognized” means the signer is in the signer group of a wallet the customer controls — the same recognition model as endorsed requests.
The canonical payload
The signature covers the RFC 8785 (JCS) canonical JSON of exactly these fields:GET /mandates/{mandate_id} — a client reproduces the payload from the API response alone.
Why byte-exactness matters: the platform verifies your signature against its own canonicalization of the same fields. JCS guarantees that both sides — regardless of language, struct field order, or whitespace habits — emit the identical byte sequence: keys sorted, no insignificant whitespace, standard number and string encoding. Use a JCS library (available for every mainstream language) rather than hand-rolling JSON.stringify ordering.
The action verb is inside the payload. "action": "approve" and "action": "cancel" produce different bytes, so an approval signature can never be replayed as a cancellation, or vice versa.
Approve
approver_public_key— the recognized signer’s public key (identifies who is signing; must differ from the bound signer).signature— base64, over the canonical payload with"action": "approve".
pending → active. Mandates and scheduled payments are decoupled — nothing is bound at approval; any payment the rule covers is matched at fire time. Approval is refused when:
- the mandate is not
pending(already active, rejected, or revoked), - its
valid_untilhas already passed — a signature over a dead grant authorizes nothing, so the platform refuses to spend one, - its originating instruction failed part-way — such an orphan must never become live authority.
Cancel
"action": "cancel". The resulting status records when it ended:
The audit trail keeps who did what:
approved_by_signer_id, rejected_by_signer_id, revoked_by_signer_id on the mandate.
Signing with a passkey (WebAuthn)
Browser passkeys are supported directly: pass the WebAuthn assertion as thesignature, using the canonical payload as the assertion challenge. The platform verifies that the assertion’s challenge matches the mandate payload and that the ES256 signature verifies against the enrolled passkey — standard authenticatorData ‖ SHA-256(clientDataJSON) WebAuthn semantics, no custom scheme. Server-held ES256 keys sign the payload’s SHA-256 digest directly.
Signing checklist
GET /mandates/{mandate_id}— readid,bound_signer_id,rule,valid_from,valid_until.- Build the payload object with the intended
action; canonicalize with a JCS (RFC 8785) library. - Sign with a recognized signer key that is not the bound signer (or run a WebAuthn assertion with the payload as challenge).
POST .../approve(or.../cancel) with the public key + base64 signature.- Confirm
statusin the response —activemeans the mandate can now cover payments at fire time.

