Skip to main content
A mandate becomes a live spending authorization only when a customer-controlled signer signs it. This page defines the exact bytes that get signed, so any client — browser passkey, server-held key, hardware signer — can produce a verifiable signature.

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:
Every field comes from the mandate as returned by 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".
On success the mandate flips 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_until has 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

Same payload with "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 the signature, 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

  1. GET /mandates/{mandate_id} — read id, bound_signer_id, rule, valid_from, valid_until.
  2. Build the payload object with the intended action; canonicalize with a JCS (RFC 8785) library.
  3. Sign with a recognized signer key that is not the bound signer (or run a WebAuthn assertion with the payload as challenge).
  4. POST .../approve (or .../cancel) with the public key + base64 signature.
  5. Confirm status in the response — active means the mandate can now cover payments at fire time.