> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Mandate Signing

> The canonical payload a customer signs to approve or cancel a mandate, and the dual-control rules around it

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](/documentation/agentic-payments/direct-control#mandates)). 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](/documentation/signing-guide).

## The canonical payload

The signature covers the [RFC 8785 (JCS)](https://www.rfc-editor.org/rfc/rfc8785) canonical JSON of exactly these fields:

```json theme={null}
{
  "action": "approve",
  "id": "2vWxMandate00000000000000000",
  "bound_signer": "2vWxSigner000000000000000000",
  "rule": {
    "target_type": "recipient",
    "targets": ["Alice"],
    "network_id": "base-sepolia",
    "asset": "USDC",
    "max_per_tx": "10",
    "window": "MONTHLY",
    "max_count_per_target_in_window": 1
  },
  "valid_from": 0,
  "valid_until": 1798675200
}
```

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

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/mandates/{mandate_id}/approve \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "approver_public_key": "BHkApproverKeyQm",
    "signature": "MEUCIQExampleSignature"
  }'
```

* `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

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/mandates/{mandate_id}/cancel \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "signer_public_key": "BHkSignerKeyQm",
    "signature": "MEUCIQExampleSignature"
  }'
```

Same payload with `"action": "cancel"`. The resulting status records *when* it ended:

| Cancelled while | Final status | Meaning                                                                             |
| --------------- | ------------ | ----------------------------------------------------------------------------------- |
| `pending`       | `rejected`   | declined at review — it never held authority                                        |
| `active`        | `revoked`    | authority withdrawn — future fires under it fail with `no_mandate`/`mandate_denied` |

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.
