Give each key only the scopes it needs
A key carries a fixed set of scopes, such aswallets:read, wallets:create, transactions:create, transactions:sign, and policies:manage. A call outside them fails with 403 insufficient_permissions.
- Issue a separate key per service. A reporting job needs
wallets:readandtransactions:read; only the service that sends funds needstransactions:create. - Check what a key can do with Get API key info, which lists its
scopes. - Keep keys server-side only. Never ship one in a mobile app, a browser bundle, or a public repository.
- Where the key supports an IP allowlist, use it. A request from outside the list fails with
403ip_not_allowed.
Let policies set the limits
Policies are checked before every signature, independently of the key’s scopes, so they’re your control if a key leaks. Use them for spending limits, destination allowlists, time locks, and approvals on large transfers. See Policies. Build your flow around the two outcomes policies can produce. A transaction that needs approval comes back202 with status: "pending_authorization", and one held by a time lock comes back 202 with status: "delayed" and an execute_at timestamp. Its funds are locked in both cases, so show it as pending rather than failed.
Make every write idempotent
Send anIdempotency-Key on every POST, PATCH, and DELETE. Some endpoints require it, including the ones that create wallets, addresses, transactions, policies, and sponsored transfers. Idempotency has the full list.
- Generate the key and store it with your own record before you send the request.
- After a timeout or a network error, retry with the same key. A completed request replays its original response, and a failed one runs again.
- Use a new key for each distinct operation. Reusing one with a different body fails with
422idempotency_key_conflict. - Keys expire after 24 hours, so don’t rely on them for retries older than that.
Treat transactions as asynchronous
A201 means the transaction was signed and submitted, not that it’s final. Track it to a terminal state with Get transaction or a webhook.
An empty
tx_hash means the transaction hasn’t been broadcast yet. Once it has, confirmations rises until the transaction is completed. Only credit or release funds on completed.
Handle amounts as strings
Amounts are JSON strings in two forms:amount in the asset’s display unit, and amount_raw in its smallest unit. Parse them with a decimal type, never a float. Read fee_asset and fee_decimals from each response, because the fee is often paid in the chain’s native coin rather than the asset you sent.
Verify webhooks and plan for missed ones
Verify the signature on every delivery and reject old timestamps; see Webhooks. Don’t let a webhook be the only way you learn a transaction finished. Reconcile anything you still hold as in progress against Get transaction on a schedule.Stay inside rate limits
Limits apply per key and per organization. ReadX-RateLimit-Remaining and the X-Org-RateLimit-* headers and slow down before you reach zero. On a 429, wait for the Retry-After delay before retrying.
Pin the version and watch for deprecations
SendAPI-Version on every request so a new default can’t change behavior under you. Alert on Deprecation and Sunset response headers, and migrate before the sunset date.
Log the request ID
Store theX-Request-Id from every response with your own record of the call. It identifies the exact request when you need to investigate an issue with support.