If you haven’t set up a wallet yet, start with Non-Custodial Wallets first, then return here for the complete signing reference.
The Endorsed Request Model
An endorsed request wraps two things:- An intent — a JSON object describing exactly what you want to do
- One or more signatures — ECDSA P-256 signatures (or WebAuthn assertions) proving that authorized signers approved the intent
- You never send private keys — only signatures
- The server cannot fabricate intents — every action has cryptographic proof of authorization
- Multiple signers can co-approve — the
signaturesarray accepts as many entries as your threshold requires
How Signing Works
Every signature follows the same three-step process, regardless of intent type.Step 1: Build the Intent JSON
Create the intent object with the exact fields required for the operation. Field names aresnake_case. Amounts are strings (e.g., "10.5", not 10.5). Omit any optional fields that don’t apply.
Step 2: Canonicalize with RFC 8785 (JCS)
RFC 8785 JSON Canonicalization Scheme produces a deterministic byte sequence from any JSON object by sorting keys alphabetically and removing insignificant whitespace. This ensures that your client and Dakota’s policy engine hash identical bytes, regardless of how your language orders JSON keys.Step 3: Hash with SHA-256, Sign with ECDSA P-256
Hash the canonical bytes with SHA-256, then sign the hash using your ECDSA P-256 private key. The signature must be in ASN.1 DER encoding (not rawr || s), then base64-encoded.
This is the flow for
ES256 signers. If your signer is a WebAuthn credential (key_type: WEBAUTHN) — a passkey or hardware authenticator — the signature is a WebAuthn assertion bundle rather than a DER signature over the hash, and the intent is carried as the assertion’s challenge. See WebAuthn & Passkey Signing.Code Examples
Intent Types Reference
Every endorsed request carries one of nine intent types. The table below lists all of them with their discriminator value, endpoint, and required fields.Send Transaction
Send crypto from a wallet. This is the only intent type without atype discriminator field.
operation.kind field determines which sub-fields are required:
Amounts are strings (
"10.5", not 10.5). The caip2 field uses CAIP-2 chain identifiers (e.g., eip155:1 for Ethereum mainnet, eip155:11155111 for Sepolia).Attach Signer Group to Wallet
Detach Signer Group from Wallet
Attach Policy to Wallet
Detach Policy from Wallet
Add Policy Rule
Remove Policy Rule
Update Policy Rule
updated_definition is a JSON string, not a nested object. Serialize the definition object to a string before including it in the intent.The update path is stricter than create — threshold must be > 0, and amount_threshold updates require the full proto Asset shape with contract_address (not just {id, name}). The accepted shapes:approval_threshold:{"threshold": int32 (> 0), "description"?: string}amount_threshold:{"min_amount": int64 (>= 0), "threshold": int32 (> 0), "asset": {"id", "name", "network_id", "contract_address", "token_standard", "decimals"}}address_list:{"addresses": [string] (non-empty)}
Delete Policy
Signer Group Lifecycle
A signer group controls who can authorize wallet actions. Here is the complete lifecycle from creation through teardown.Step-by-step
Adding and removing signers (steps 3 and 6) do not require endorsed requests — they are standard API calls authenticated with your API key. Only wallet-level operations (attach, detach, transact) require cryptographic signatures.

