# Get balances (/docs/api-reference/balances/get-balances) `GET /v1/balances` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `address` | query | object | no | | | `chain` | query | object | yes | | Responses: | Status | Description | | --- | --- | | 200 | Balances | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List wallet balances (/docs/api-reference/balances/list-wallet-balances) `GET /v1/wallets/balances` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `moduleId` | query | object | no | | | `customerId` | query | object | no | | Responses: | Status | Description | | --- | --- | | 200 | Wallets with balances | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get bridge (/docs/api-reference/bridges/get-bridge) `GET /v1/bridges/{id}` Requires scope transactions:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | object | yes | Bridge id, from prepare or submit | Responses: | Status | Description | | --- | --- | | 200 | Bridge | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 404 | No bridge with this id belongs to the API key's business | | 429 | The API key exceeded its request budget for the window | # Prepare bridge (/docs/api-reference/bridges/prepare-bridge) `POST /v1/bridges/prepare` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "arbitrum" | "polygon" | "optimism" | "base" | "plasma" | yes | The chain the USD₮ leaves from: ethereum, arbitrum, polygon, optimism, base or plasma. | | `destinationChain` | "ethereum" | "arbitrum" | "polygon" | "optimism" | "base" | "plasma" | "tron" | yes | Where the USD₮ lands: any of the six source chains other than chain, or tron. | | `recipient` | string | yes | The address that receives the USD₮ on destinationChain: a 0x address for an EVM destination, a base58 T... address for tron. | | `amount` | string | yes | Amount of USD₮ to send, in token units (for example "100"). | | `safeAddress` | string | yes | The Safe doing the bridge. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | Unsigned bridge userOperation | | 400 | As for the quote, or the Safe cannot cover the amount plus the gas | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 409 | The passkey does not own this Safe, the wallet is under recovery, the Safe is not deployed, or the same userOperation is already in flight | | 429 | The API key exceeded its request budget for the window | # Quote bridge (/docs/api-reference/bridges/quote-bridge) `POST /v1/bridges/quote` Requires scope quotes:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "arbitrum" | "polygon" | "optimism" | "base" | "plasma" | yes | The chain the USD₮ leaves from: ethereum, arbitrum, polygon, optimism, base or plasma. | | `destinationChain` | "ethereum" | "arbitrum" | "polygon" | "optimism" | "base" | "plasma" | "tron" | yes | Where the USD₮ lands: any of the six source chains other than chain, or tron. | | `recipient` | string | yes | The address that receives the USD₮ on destinationChain: a 0x address for an EVM destination, a base58 T... address for tron. | | `amount` | string | yes | Amount of USD₮ to send, in token units (for example "100"). | | `safeAddress` | string | yes | The Safe doing the bridge. | Responses: | Status | Description | | --- | --- | | 200 | Bridge quote | | 400 | Malformed body, a recipient that is not valid for the destination, a source chain bridging is not enabled from, a destination the router can only reach with a swap, an amount below the 10 USDT minimum, or a route whose fee breaches the ceiling | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | The API key lacks the quotes:read scope | | 409 | The wallet is under recovery, or the Safe is not deployed on the source chain | | 429 | The API key exceeded its request budget for the window | | 501 | Bridging is not enabled in this environment | | 502 | The bridge router answered with something other than a route to the pinned Across contract, or the chain's RPC node was unreachable | | 503 | The bridge router could not be reached | # Submit bridge (/docs/api-reference/bridges/submit-bridge) `POST /v1/bridges` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "arbitrum" | "polygon" | "optimism" | "base" | "plasma" | yes | The chain the USD₮ leaves from: ethereum, arbitrum, polygon, optimism, base or plasma. | | `destinationChain` | "ethereum" | "arbitrum" | "polygon" | "optimism" | "base" | "plasma" | "tron" | yes | Where the USD₮ lands: any of the six source chains other than chain, or tron. | | `recipient` | string | yes | The address that receives the USD₮ on destinationChain: a 0x address for an EVM destination, a base58 T... address for tron. | | `amount` | string | yes | Amount of USD₮ to send, in token units (for example "100"). | | `safeAddress` | string | yes | The Safe doing the bridge. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `passkeyData` | object | yes | The passkey assertion over the userOpHash returned by POST /v1/bridges/prepare. | | `userOp` | object | yes | The userOperation returned verbatim by POST /v1/bridges/prepare. | | `userOpHash` | string | yes | The userOpHash returned by POST /v1/bridges/prepare, which the passkey assertion signs. | Responses: | Status | Description | | --- | --- | | 200 | Bridge submitted | | 400 | The userOperation does not hash to userOpHash, the body disagrees with what was prepared under it, or the signing gateway refused the operation (its passkey_assertion_invalid refusal is relayed as validation_failed). | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 404 | Nothing was prepared under this userOpHash | | 409 | The bridge was already submitted, the quote expired before submit, the wallet entered recovery, or the signing gateway refused the operation | | 429 | The API key exceeded its request budget for the window | # Create customer (/docs/api-reference/customers/create-customer) `POST /v1/customers` Requires scope customers:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `reference` | string | no | | Responses: | Status | Description | | --- | --- | | 201 | Customer created | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List customers (/docs/api-reference/customers/list-customers) `GET /v1/customers` Requires scope customers:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `cursor` | query | object | no | | | `limit` | query | object | no | | Responses: | Status | Description | | --- | --- | | 200 | Customer page | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Enrol customer guardian (/docs/api-reference/guardians/enrol-customer-guardian) `POST /v1/guardians` Requires scope wallets:write and an x-customer-id header; the wallet is resolved from the customer, so there is no wallet id in the path. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 201 | | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Enrol guardian (/docs/api-reference/guardians/enrol-guardian) `POST /v1/wallets/{id}/guardians` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | object | yes | Wallet id | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | no | The one chain to act on. | | `chains` | array of "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | no | Several chains to prepare in one call. | | `passkeyPublicKey` | object | yes | The P-256 coordinates of the customer passkey that owns the Safe. | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `passkeyData` | object | no | A passkey assertion. | | `ref` | string | no | Your own identifier for this enrolment, which turns chains + passkeyData into a single-assertion call. | Responses: | Status | Description | | --- | --- | | 200 | Enrolment prepared, submitted, or already in place on the named chains | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get customer guardians (/docs/api-reference/guardians/get-customer-guardians) `GET /v1/guardians` Requires scope wallets:read and an x-customer-id header. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 200 | | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get guardians (/docs/api-reference/guardians/get-guardians) `GET /v1/wallets/{id}/guardians` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | object | yes | Wallet id | Responses: | Status | Description | | --- | --- | | 200 | Per-chain guardian enrolment state | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Revoke guardian (/docs/api-reference/guardians/revoke-guardian) # Rotate guardian (/docs/api-reference/guardians/rotate-guardian) # API Reference (/docs/api-reference) Requests are authenticated with an API key. Each endpoint lists the scope it requires. ## OpenAPI [#openapi] Download the [OpenAPI document](/openapi.json) to import into Postman or generate a client. # Add passkey (/docs/api-reference/passkeys/add-passkey) `POST /v1/passkeys` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `moduleId` | "evm_erc4337" | "bitcoin" | "spark" | "solana" | "tron" | "ton" | yes | | | `treasury` | boolean | no | | | `passkeyRegistration` | object | yes | | | `passkeyRegistration.credentialId` | string | yes | | | `passkeyRegistration.credentialPublicKey` | string | yes | The authenticator COSE public key (CBOR bytes) as base64url — the encoding browsers and @simplewebauthn emit. | | `passkeyRegistration.counter` | integer | no | Not persisted at creation — omit. | | `passkeyRegistration.transports` | array of string | no | Informational only — not persisted. | | `passkeyRegistration.rpId` | string | yes | | | `passkeyRegistration.origins` | array of string | yes | | | `passkeyData` | object | yes | | | `passkeyData.credential` | object | yes | | | `passkeyData.expectedChallenge` | string | no | | | `passkeyData.expectedOrigins` | array of string | no | Ignored on custodial transfers, possession proofs and add-device — the server uses the origins bound to the stored credential. | | `passkeyData.expectedRPID` | string | no | Ignored on custodial transfers, possession proofs and add-device — the server uses the RP id bound to the stored credential. | | `passkeyData.authenticator` | object | yes | | Responses: | Status | Description | | --- | --- | | 201 | Passkey enrolled | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List passkeys (/docs/api-reference/passkeys/list-passkeys) `GET /v1/passkeys` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `treasury` | query | object | no | | | `x-customer-id` | header | string | no | | Responses: | Status | Description | | --- | --- | | 200 | Enrolled passkeys | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Remove passkey (/docs/api-reference/passkeys/remove-passkey) # Retry passkey enrolment (/docs/api-reference/passkeys/retry-passkey-enrolment) # Cancel payment (/docs/api-reference/payment-links/cancel-payment) `POST /v1/payments/{id}/cancel` Requires scope payment_links:write and a business with a payment-links project in the key's environment; it acts on the record in whichever of those projects it belongs to. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 200 | Payment cancelled | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Create payment (/docs/api-reference/payment-links/create-payment) `POST /v1/payments` Requires scope payment_links:write and a business with a payment-links project in the key's environment. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string | yes | Stable per payment. | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `reference` | string | yes | Your own reference for this payment. | | `destinations` | object | yes | One entry per chain family you will accept. | Responses: | Status | Description | | --- | --- | | 201 | Payment created with its deposit addresses | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get payment (/docs/api-reference/payment-links/get-payment) `GET /v1/payments/{id}` Requires scope payment_links:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 200 | Payment | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List payments (/docs/api-reference/payment-links/list-payments) `GET /v1/payments` Requires scope payment_links:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `attemptStatus` | query | "DEPOSITED" | "PENDING_APPROVAL" | "SETTLING" | "SETTLED" | "FAILED" | "APPROVAL_EXPIRED" | no | Return only payments having an attempt in this state. | | `reference` | query | object | no | Your own reference. | | `status` | query | "AWAITING_PAYMENT" | "FULFILLED" | "EXPIRED" | "CANCELLED" | no | | | `cursor` | query | object | no | | | `limit` | query | object | no | 1-100, default 25 | | `q` | query | object | no | Search: matches part of your reference, case-insensitively, or a payment id exactly | Responses: | Status | Description | | --- | --- | | 200 | Payments | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get fee quote (/docs/api-reference/quotes/get-fee-quote) `POST /v1/quotes` Requires scope quotes:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | Identifies whose wallet to quote for. | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `moduleId` | "evm_erc4337" | "bitcoin" | "spark" | "solana" | "tron" | "ton" | yes | Selects the wallet to quote for. | | `treasury` | boolean | no | Set true to quote for the customer's treasury wallet rather than their ordinary one. | | `txParams` | object | yes | | | `txParams.chain` | string | yes | Chain to price on: an EVM chain name on evm_erc4337 (base, ethereum, …), otherwise the module's own chain (bitcoin, spark, solana, tron, ton). | | `txParams.amount` | string | yes | Amount in the display unit of the asset — BTC on bitcoin/spark, token units on the others — never base units (satoshis, lamports, wei). | | `txParams.recipientAddress` | string | yes | The destination, in the chain's own address format and on the network this deployment runs: bitcoin — base58check starting 1 or 3, or bech32 starting bc1 (bc1q and bc1p both accepted). | | `txParams.token` | string | no | Asset to price. evm_erc4337: "usdt" (default) or "xaut" — case-insensitive. solana: "USDT" or a base58 mint address. tron: "USDT" or the token's TRC-20 contract exactly as GasFree publishes it (that match is case-sensitive). ton: a jetton master address, defaulting to USD₮. bitcoin and spark ignore it — the fee is always BTC. | | `txParams.senderAddress` | string | no | Optional, quotes only. | Responses: | Status | Description | | --- | --- | | 200 | Quote | | 400 | Malformed body, an address that fails its chain's checksum, a chain or token the lane does not list, or a wallet that cannot cover the transfer plus its fee | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | The API key lacks the quotes:read scope | | 404 | No single wallet matched moduleId + x-customer-id + treasury on that chain. | | 409 | erc4337 only: the Safe is not deployed on this chain yet (retry once provisioning puts it on-chain), or the transfer reverted in simulation for a reason other than the balance — a blocked recipient or a paused token | | 429 | The API key exceeded its request budget for the window | | 500 | A fault on our side. | | 502 | Fee estimation failed upstream — the chain's RPC node, the bundler or the paymaster — or the wallet registry could not be reached. | | 503 | The owning chain service is shutting down, or your API key could not be resolved because the key service is unavailable. | | 504 | The owning chain service did not answer within the gateway's timeout. | # Quote Spark exit (/docs/api-reference/quotes/quote-spark-exit) `POST /v1/quotes/spark` Requires scope quotes:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | Identifies whose spark wallet to price against. | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `operation` | "quoteWithdraw" | "quoteLightningPayment" | yes | What to price. quoteWithdraw prices a cooperative exit from Spark to a Bitcoin L1 address and needs withdrawalAddress + amountSats. quoteLightningPayment prices paying a Lightning invoice and needs encodedInvoice. | | `treasury` | boolean | no | Set true to price against the customer's treasury spark wallet rather than their ordinary one. | | `withdrawalAddress` | string | no | quoteWithdraw only, and required for it: the Bitcoin L1 address the exit pays out to. | | `amountSats` | integer | no | quoteWithdraw only, and required for it: the amount to withdraw, as an integer count of satoshis. | | `encodedInvoice` | string | no | quoteLightningPayment only, and required for it: the BOLT-11 invoice to pay. | | `btcUsdRate` | number | no | Optional BTC/USD rate. | Responses: | Status | Description | | --- | --- | | 200 | Spark exit fee quote | | 400 | Malformed body — an operation outside the two priced ones, a field the chosen operation requires but did not get, a withdrawalAddress that is not a valid Bitcoin address on the configured network, or a non-positive amountSats. | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | The API key lacks the quotes:read scope | | 404 | No spark wallet exists for this owner. | | 429 | This business exceeded its per-minute enclave quote budget. | | 500 | A fault on our side. | | 502 | The enclave could not be reached, returned no fee, or the Spark service provider refused to price the exit. | | 504 | The quote did not finish within the gateway's quote budget. | # Cancel recovery (/docs/api-reference/recoveries/cancel-recovery) `POST /v1/recoveries/{id}/cancel` Requires scope recoveries:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | no | The cancellationToken returned once in the POST /v1/recoveries response — not the recoveryToken that started the recovery, and not a recovery-contact verifyToken. | | `passkey` | object | no | A passkey that currently owns the Safe. | | `passkey.passkeyPublicKey` | object | yes | | | `passkey.passkeyData` | object | no | | Responses: | Status | Description | | --- | --- | | 200 | Recovery cancellation accepted (status CANCELLING) | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get recovery (/docs/api-reference/recoveries/get-recovery) `GET /v1/recoveries/{id}` Requires scope recoveries:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 200 | Recovery status | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Send recovery code (/docs/api-reference/recoveries/send-recovery-code) `POST /v1/recoveries/challenge` Requires scope recoveries:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `customerId` | string | yes | | | `walletId` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | Code issued and delivered | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Start recovery (/docs/api-reference/recoveries/start-recovery) `POST /v1/recoveries` Requires scope recoveries:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string | no | Idempotency key for safe start retries (keyed with walletId) | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `walletId` | string | yes | | | `address` | string | yes | | | `chains` | array of "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `newPasskey` | object | yes | | | `newPasskey.x` | string | yes | | | `newPasskey.y` | string | yes | | | `newPasskey.credentialId` | string | yes | | | `newPasskey.passkeyBlob` | string | no | | | `newPasskey.credentialPublicKey` | string | yes | The new passkey's WebAuthn COSE public key, base64url or hex — the same value you send as passkeyRegistration.credentialPublicKey when provisioning. | | `newPasskey.rpId` | string | yes | The relying party the new passkey was created under. | | `newPasskey.origins` | array of string | yes | The origins the new passkey signs from. | | `newPasskey.counter` | integer | no | | | `newPasskey.transports` | array of string | no | | | `verification` | object | no | A business attestation over the recovery. | | `verification.payload` | object | yes | | | `verification.signatureB64` | string | yes | | | `recoveryToken` | string | no | A single-use token from POST /v1/recoveries/challenge/verify, proving the customer holds the recovery code. | | `recoveryContact` | string | no | The customer's recovery email address, recorded for future recoveries. | Responses: | Status | Description | | --- | --- | | 201 | Recovery started | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Verify recovery code (/docs/api-reference/recoveries/verify-recovery-code) # Check contact coverage (/docs/api-reference/recovery-contacts/check-contact-coverage) # Get recovery contact (/docs/api-reference/recovery-contacts/get-recovery-contact) `GET /v1/recovery-contacts` Requires scope recovery-contacts:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 200 | Recovery contact | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Request contact challenge (/docs/api-reference/recovery-contacts/request-contact-challenge) # Set recovery contact (/docs/api-reference/recovery-contacts/set-recovery-contact) `POST /v1/recovery-contacts` Requires scope recovery-contacts:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `contact` | string | yes | The address recovery alerts go to. | | `challenge` | string | yes | The challenge returned by POST /v1/recovery-contacts/challenge. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `passkeyData` | object | yes | An assertion over the challenge from a passkey that currently owns the customer's Safe. | Responses: | Status | Description | | --- | --- | | 200 | Recovery contact set | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Verify recovery contact (/docs/api-reference/recovery-contacts/verify-recovery-contact) `POST /v1/recovery-contacts/verify` Requires scope recovery-contacts:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `contactId` | string | yes | The contactId returned when the contact was set. | | `token` | string | yes | The verifyToken returned when the contact was set, after you have confirmed with the contact that it reached them. | Responses: | Status | Description | | --- | --- | | 200 | Recovery contact verified | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get max swap amount (/docs/api-reference/swaps/get-max-swap-amount) `POST /v1/swaps/max` Requires scope quotes:read and the passkey public key, because it reports the wallet's balance — it will not answer for a Safe the passkey does not own. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | Only ethereum is routable for USDT/XAUT today. | | `fromToken` | "usdt" | "xaut" | yes | Token being sold. | | `toToken` | "usdt" | "xaut" | yes | Token being bought. | | `safeAddress` | string | yes | The Safe doing the swap. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | Largest swappable amount | | 400 | Malformed body, or a chain the aggregator cannot route USDT/XAUT on | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | The API key lacks the quotes:read scope | | 409 | The passkey does not own this Safe, the wallet is under recovery, or the Safe is not deployed on this chain | | 429 | The API key exceeded its request budget for the window | | 502 | The swap aggregator or the chain's RPC node could not be reached | # Prepare swap (/docs/api-reference/swaps/prepare-swap) `POST /v1/swaps/prepare` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | Only ethereum is routable for USDT/XAUT today. | | `fromToken` | "usdt" | "xaut" | yes | Token being sold. | | `toToken` | "usdt" | "xaut" | yes | Token being bought. | | `amount` | string | yes | Amount of fromToken to sell, in token units (for example "100" USDT). | | `safeAddress` | string | yes | The Safe doing the swap. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | Unsigned swap userOperation | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Quote swap (/docs/api-reference/swaps/quote-swap) `POST /v1/swaps/quote` Requires scope quotes:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | Only ethereum is routable for USDT/XAUT today. | | `fromToken` | "usdt" | "xaut" | yes | Token being sold. | | `toToken` | "usdt" | "xaut" | yes | Token being bought. | | `amount` | string | yes | Amount of fromToken to sell, in token units (for example "100" USDT). | | `safeAddress` | string | yes | The Safe doing the swap. | Responses: | Status | Description | | --- | --- | | 200 | Swap quote | | 400 | Malformed body, a chain the aggregator cannot route USDT/XAUT on (everything but ethereum), or an amount below the per-token minimum. | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | The API key lacks the quotes:read scope | | 409 | The wallet is under recovery, so swaps are blocked | | 429 | The API key exceeded its request budget for the window | | 502 | The swap aggregator could not be reached or returned no route, or the chain's RPC node was unreachable | # Submit swap (/docs/api-reference/swaps/submit-swap) `POST /v1/swaps` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | Only ethereum is routable for USDT/XAUT today. | | `fromToken` | "usdt" | "xaut" | yes | Token being sold. | | `toToken` | "usdt" | "xaut" | yes | Token being bought. | | `amount` | string | yes | Amount of fromToken to sell, in token units (for example "100" USDT). | | `safeAddress` | string | yes | The Safe doing the swap. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `passkeyData` | object | yes | The passkey assertion over the userOpHash returned by POST /v1/swaps/prepare. | | `userOp` | object | yes | The userOperation returned verbatim by POST /v1/swaps/prepare. | | `userOpHash` | string | yes | The userOpHash returned by POST /v1/swaps/prepare, which the passkey assertion signs. | Responses: | Status | Description | | --- | --- | | 200 | Swap submitted | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get userOp status (/docs/api-reference/transactions/get-userop-status) `GET /v1/userops/{hash}` Requires scope transactions:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Responses: | Status | Description | | --- | --- | | 200 | Quorum status | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List transactions (/docs/api-reference/transactions/list-transactions) `GET /v1/transactions` Requires scope transactions:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `paymentId` | query | object | no | Only the transactions one payment link payment caused: its deposit, its sweep to the business and any recovery. | | `purpose` | query | "transfer" | "swap" | "bridge" | "paylink_deposit" | "paylink_sweep" | "paylink_recovery" | no | | | `chain` | query | object | no | | | `kind` | query | object | no | | | `status` | query | object | no | | | `walletId` | query | object | no | | | `cursor` | query | object | no | | | `limit` | query | object | no | | Responses: | Status | Description | | --- | --- | | 200 | Transaction page | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Prepare userOp (/docs/api-reference/transactions/prepare-userop) `POST /v1/userops/prepare` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `token` | "usdt" | "xaut" | no | | | `to` | string | yes | | | `amount` | string | yes | Amount in token units (for example "12.5" USDT) — digits with at most one decimal point and at most 6 decimal places. | | `safeAddress` | string | yes | The wallet's Safe address, as returned when the wallet was provisioned. | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `treasury` | boolean | no | | Responses: | Status | Description | | --- | --- | | 200 | Prepared userOp + hash to sign | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Submit batch signatures (/docs/api-reference/transactions/submit-batch-signatures) `POST /v1/userops/signatures` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `signatures` | array of object | yes | | Responses: | Status | Description | | --- | --- | | 200 | Per-userOp submission results | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Submit transaction (/docs/api-reference/transactions/submit-transaction) `POST /v1/transactions` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | Identifies whose wallet to send from. | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `moduleId` | "evm_erc4337" | "bitcoin" | "spark" | "solana" | "tron" | "ton" | yes | | | `treasury` | boolean | no | | | `txParams` | object | yes | | | `txParams.chain` | string | yes | | | `txParams.amount` | string | yes | Amount in the display unit of the asset — BTC on bitcoin/spark, token units on the others — never base units (satoshis, lamports, wei). | | `txParams.selectedPayment` | string | yes | | | `txParams.senderAddress` | string | no | Optional, quotes only. | | `txParams.recipientAddress` | string | no | The destination, in the chain's own address format and on the network this deployment runs: bitcoin — base58check starting 1 or 3, or bech32 starting bc1 (bc1q and bc1p both accepted). | | `txParams.transactionId` | integer | no | Your own integer sequence number for this transfer. | | `txParams.accountIndex` | integer | no | Do not send — the infrastructure owns the derivation index. | | `txParams.networkConfig` | object | yes | Only the asset selector is read (tokenMint / tokenRoot). | | `passkeyData` | object | no | | | `passkeyData.credential` | object | yes | | | `passkeyData.expectedChallenge` | string | yes | The challenge the assertion answers. | | `passkeyData.expectedOrigins` | array of string | no | Ignored on custodial transfers, possession proofs and add-device — the server uses the origins bound to the stored credential. | | `passkeyData.expectedRPID` | string | no | Ignored on custodial transfers, possession proofs and add-device — the server uses the RP id bound to the stored credential. | | `passkeyData.authenticator` | object | yes | | Responses: | Status | Description | | --- | --- | | 200 | Transaction submitted | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Submit userOp signature (/docs/api-reference/transactions/submit-userop-signature) `POST /v1/userops/{hash}/signatures` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `passkeyData` | object | yes | | | `passkeyData.credential` | object | yes | | | `passkeyData.expectedChallenge` | string | yes | | | `passkeyData.expectedOrigins` | array of string | yes | | | `passkeyData.expectedRPID` | string | yes | | | `passkeyData.authenticator` | object | yes | | Responses: | Status | Description | | --- | --- | | 200 | Updated quorum status | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Cancel pending approval (/docs/api-reference/treasury/cancel-pending-approval) > **Releasing soon:** This endpoint is being released and is not yet available. # Create delegate address (/docs/api-reference/treasury/create-delegate-address) `POST /v1/limits/delegates` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `signerId` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | delegateAddress and entityKey for this signer | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | forbidden: on-chain limits are a treasury feature — signerId is not a passkey enrolled on this treasury | | 429 | The API key exceeded its request budget for the window | # Create proposal (/docs/api-reference/treasury/create-proposal) > **Releasing soon:** This endpoint is being released and is not yet available. `POST /v1/treasury/proposals` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `kind` | "payment" | "owner_add" | "owner_remove" | "threshold_change" | "reject" | yes | What the Safe transaction does. params takes the matching shape: payment {token?, to, amount\|amountBaseUnits}. | | `params` | object | yes | | | `nonce` | integer | no | Pin the Safe nonce (for a replacement or a rejection). | | `ttlSeconds` | integer | no | How long this proposal collects signatures, in seconds: at least 3600 and at most the platform window (default 7 days). | Responses: | Status | Description | | --- | --- | | 200 | The proposal, with the safeTxHash and challenge to sign | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Discard pending threshold changes (/docs/api-reference/treasury/discard-pending-threshold-changes) > **Releasing soon:** This endpoint is being released and is not yet available. # Get pending approval (/docs/api-reference/treasury/get-pending-approval) > **Releasing soon:** This endpoint is being released and is not yet available. # Get proposal (/docs/api-reference/treasury/get-proposal) > **Releasing soon:** This endpoint is being released and is not yet available. # Get Safe threshold (/docs/api-reference/treasury/get-safe-threshold) `GET /v1/wallets/{id}/threshold` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `chain` | query | object | yes | | Responses: | Status | Description | | --- | --- | | 200 | Current threshold | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get signing threshold (/docs/api-reference/treasury/get-signing-threshold) `GET /v1/signing-threshold` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `treasury` | query | boolean | no | | Responses: | Status | Description | | --- | --- | | 200 | Threshold in force, and the signers enrolled | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List allowances (/docs/api-reference/treasury/list-allowances) > **Releasing soon:** This endpoint is being released and is not yet available. `GET /v1/limits/allowances` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `safeAddress` | query | object | yes | The Safe whose allowances to read | | `chain` | query | object | yes | An EVM chain | Responses: | Status | Description | | --- | --- | | 200 | The Safe's active allowances on that chain | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List pending approvals (/docs/api-reference/treasury/list-pending-approvals) > **Releasing soon:** This endpoint is being released and is not yet available. `GET /v1/transactions/approvals` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `treasury` | query | boolean | no | | | `x-customer-id` | header | string | no | | Responses: | Status | Description | | --- | --- | | 200 | Pending approval collections | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List proposals (/docs/api-reference/treasury/list-proposals) > **Releasing soon:** This endpoint is being released and is not yet available. `GET /v1/treasury/proposals` Requires scope transactions:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | object | no | | | `chain` | query | object | no | | | `x-customer-id` | header | string | no | | Responses: | Status | Description | | --- | --- | | 200 | Proposals | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List spend caps (/docs/api-reference/treasury/list-spend-caps) `GET /v1/limits` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `treasury` | query | object | no | Defaults true. false reads a customer wallet. | Responses: | Status | Description | | --- | --- | | 200 | The caps currently in force | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Prepare allowance setup (/docs/api-reference/treasury/prepare-allowance-setup) `POST /v1/limits/setup` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `safeAddress` | string | no | | | `passkeyPublicKey` | object | yes | | | `passkeyPublicKey.x` | string | yes | | | `passkeyPublicKey.y` | string | yes | | | `includeModuleEnable` | boolean | no | | | `feeToken` | "usdt" | "xaut" | no | | | `allowances` | array of object | yes | | Responses: | Status | Description | | --- | --- | | 200 | A prepared userOp and its hash | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 403 | forbidden: on-chain limits are a treasury feature — safeAddress is not this tenant's treasury Safe, or the tenant has no treasury | | 429 | The API key exceeded its request budget for the window | # Prepare delegate spend (/docs/api-reference/treasury/prepare-delegate-spend) `POST /v1/limits/spend/prepare` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `safeAddress` | string | yes | | | `delegate` | string | yes | | | `token` | string | yes | | | `to` | string | yes | | | `amountBaseUnits` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | The prepared spend and the hash to sign | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 409 | spend_limit_exceeded: the amount exceeds the delegate's remaining allowance. | | 429 | The API key exceeded its request budget for the window | # Reject proposal (/docs/api-reference/treasury/reject-proposal) > **Releasing soon:** This endpoint is being released and is not yet available. # Remove spend cap (/docs/api-reference/treasury/remove-spend-cap) `DELETE /v1/limits` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `asset` | query | object | yes | | | `signerId` | query | object | yes | | | `treasury` | query | object | no | Defaults true. false targets a customer wallet. | Responses: | Status | Description | | --- | --- | | 200 | Cap removed | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Set Safe threshold (/docs/api-reference/treasury/set-safe-threshold) `POST /v1/wallets/{id}/threshold` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `threshold` | integer | yes | | | `existingOwner` | object | yes | | | `existingOwner.x` | string | yes | | | `existingOwner.y` | string | yes | | | `passkeyData` | object | yes | | | `passkeyData.credential` | object | yes | | | `passkeyData.expectedChallenge` | string | yes | | | `passkeyData.expectedOrigins` | array of string | yes | | | `passkeyData.expectedRPID` | string | yes | | | `passkeyData.authenticator` | object | yes | | Responses: | Status | Description | | --- | --- | | 200 | Threshold change submitted | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Set signing threshold (/docs/api-reference/treasury/set-signing-threshold) `PUT /v1/signing-threshold` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `treasury` | boolean | no | Which owner the threshold belongs to. | | `threshold` | integer | yes | How many distinct passkey signers must approve a transfer. | | `lane` | "userop" | "proposal" | no | How the on-chain change is carried. userop (default): one userOp per deployed chain, approved within the quorum window (minutes) and re-prepared when it lapses. proposal: one threshold_change treasury proposal per deployed chain not yet at the target, which approvers sign over days through /v1/treasury/proposals. | | `ttlSeconds` | integer | no | lane proposal only: how long each proposal collects signatures, from 3600 up to the platform's proposal lifetime (7 days by default). | Responses: | Status | Description | | --- | --- | | 200 | Threshold now in force | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Set spend cap (/docs/api-reference/treasury/set-spend-cap) `PUT /v1/limits` Requires scope wallets:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `treasury` | boolean | no | Which wallet the cap applies to. | | `signerId` | string | yes | | | `asset` | string | yes | | | `limitBaseUnits` | string | yes | | | `windowSeconds` | integer | no | | | `onChain` | object | no | Links this cap to its on-chain allowance. | | `onChain.chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `onChain.safeAddress` | string | yes | | | `onChain.delegate` | string | yes | | | `onChain.token` | string | yes | | Responses: | Status | Description | | --- | --- | | 200 | Cap stored. onChain describes the on-chain half when one was requested. | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Sign proposal (/docs/api-reference/treasury/sign-proposal) > **Releasing soon:** This endpoint is being released and is not yet available. # Sign proposals in batch (/docs/api-reference/treasury/sign-proposals-in-batch) > **Releasing soon:** This endpoint is being released and is not yet available. # Submit delegate spend (/docs/api-reference/treasury/submit-delegate-spend) `POST /v1/limits/spend/submit` Requires scope transactions:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `chain` | "ethereum" | "base" | "arbitrum" | "polygon" | "celo" | "optimism" | "plasma" | yes | | | `safeAddress` | string | yes | | | `delegate` | string | yes | | | `token` | string | yes | | | `to` | string | yes | | | `amountBaseUnits` | string | yes | | | `delegateEntityKey` | string | yes | | | `passkeyData` | object | yes | | Responses: | Status | Description | | --- | --- | | 200 | The relay operation the bundler accepted | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 409 | spend_limit_exceeded: the amount exceeds the delegate's remaining allowance. | | 429 | The API key exceeded its request budget for the window | # Create wallet (/docs/api-reference/wallets/create-wallet) `POST /v1/wallets` Requires scope wallets:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | Scope the wallet to a business-supplied customer id | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `moduleId` | "evm_erc4337" | "bitcoin" | "spark" | "solana" | "tron" | "ton" | yes | | | `isTreasury` | boolean | no | | | `passkeyRegistration` | object | yes | | | `passkeyRegistration.credentialId` | string | yes | | | `passkeyRegistration.credentialPublicKey` | string | yes | The authenticator COSE public key (CBOR bytes) as base64url — the encoding browsers and @simplewebauthn emit. | | `passkeyRegistration.counter` | integer | no | Not persisted at creation — omit. | | `passkeyRegistration.transports` | array of string | no | Informational only — not persisted. | | `passkeyRegistration.rpId` | string | yes | | | `passkeyRegistration.origins` | array of string | yes | | Responses: | Status | Description | | --- | --- | | 201 | Wallet created | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Get wallet control (/docs/api-reference/wallets/get-wallet-control) `GET /v1/wallets/{id}/sovereignty` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | object | yes | Wallet id | Responses: | Status | Description | | --- | --- | | 200 | Per-chain owner counts and the sole-key verdict | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List wallets (/docs/api-reference/wallets/list-wallets) `GET /v1/wallets` Requires scope wallets:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `customerId` | query | object | no | | | `treasury` | query | object | no | | | `moduleId` | query | object | no | | | `cursor` | query | object | no | | | `limit` | query | object | no | | Responses: | Status | Description | | --- | --- | | 200 | Wallet page | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Provision wallets (/docs/api-reference/wallets/provision-wallets) `POST /v1/wallets/provision` Requires scope wallets:create. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-customer-id` | header | string | no | Scope the wallets to a business-supplied customer id | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `isTreasury` | boolean | no | | | `passkeyRegistration` | object | yes | | | `passkeyRegistration.credentialId` | string | yes | | | `passkeyRegistration.credentialPublicKey` | string | yes | The authenticator COSE public key (CBOR bytes) as base64url — the encoding browsers and @simplewebauthn emit. | | `passkeyRegistration.counter` | integer | no | Not persisted at creation — omit. | | `passkeyRegistration.transports` | array of string | no | Informational only — not persisted. | | `passkeyRegistration.rpId` | string | yes | | | `passkeyRegistration.origins` | array of string | yes | | | `modules` | array of "evm_erc4337" | "bitcoin" | "spark" | "solana" | "tron" | "ton" | no | | Responses: | Status | Description | | --- | --- | | 201 | Wallets provisioned | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Create webhook endpoint (/docs/api-reference/webhooks/create-webhook-endpoint) `POST /v1/webhook-endpoints` Requires scope webhooks:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `projectId` | query | object | no | The project whose endpoints to manage. | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | Public https endpoint that receives the signed POST. | | `events` | array of "wallet.created" | "wallet.passkey.enrolled" | "wallet.passkey.revoked" | "wallet.passkey.removal_blocked" | "transaction.approval_required" | "transaction.submitted" | "transaction.pending" | "transaction.reconciled" | "transaction.success" | "transaction.failed" | "transaction.reconciliation_expired" | "transaction.summary_corrected" | "deposit.detected" | "deposit.confirmed" | "bridge.filled" | "bridge.refunded" | "bridge.failed" | "bridge.stalled" | "payment.created" | "payment.detected" | "payment.approval_required" | "payment.approval_expired" | "payment.settled" | "payment.unclaimed" | "payment.recovered" | "payment.cancelled" | "payment.expired" | "payment.settlement_stalled" | "recovery.code" | "recovery.started" | "recovery.reminder" | "recovery.completed" | "recovery.cancelled" | "recovery.failed" | yes | Event types to subscribe to. | Responses: | Status | Description | | --- | --- | | 201 | The registered endpoint and its one-time signing secret | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Delete webhook endpoint (/docs/api-reference/webhooks/delete-webhook-endpoint) `DELETE /v1/webhook-endpoints/{id}` Requires scope webhooks:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `projectId` | query | object | no | The project whose endpoints to manage. | Responses: | Status | Description | | --- | --- | | 204 | The webhook endpoint was deleted | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # List webhook endpoints (/docs/api-reference/webhooks/list-webhook-endpoints) `GET /v1/webhook-endpoints` Requires scope webhooks:read. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `projectId` | query | object | no | The project whose endpoints to manage. | Responses: | Status | Description | | --- | --- | | 200 | The project's webhook endpoints | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Update webhook endpoint (/docs/api-reference/webhooks/update-webhook-endpoint) `PATCH /v1/webhook-endpoints/{id}` Requires scope webhooks:write. Authorization: `Bearer sk_test_...` or `Bearer sk_live_...`. Parameters: | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `projectId` | query | object | no | The project whose endpoints to manage. | Request body (`application/json`): | Field | Type | Required | Description | | --- | --- | --- | --- | | `events` | array of "wallet.created" | "wallet.passkey.enrolled" | "wallet.passkey.revoked" | "wallet.passkey.removal_blocked" | "transaction.approval_required" | "transaction.submitted" | "transaction.pending" | "transaction.reconciled" | "transaction.success" | "transaction.failed" | "transaction.reconciliation_expired" | "transaction.summary_corrected" | "deposit.detected" | "deposit.confirmed" | "bridge.filled" | "bridge.refunded" | "bridge.failed" | "bridge.stalled" | "payment.created" | "payment.detected" | "payment.approval_required" | "payment.approval_expired" | "payment.settled" | "payment.unclaimed" | "payment.recovered" | "payment.cancelled" | "payment.expired" | "payment.settlement_stalled" | "recovery.code" | "recovery.started" | "recovery.reminder" | "recovery.completed" | "recovery.cancelled" | "recovery.failed" | no | | | `status` | "active" | "disabled" | no | Disable to stop delivery without losing the signing secret. | Responses: | Status | Description | | --- | --- | | 200 | The updated webhook endpoint | | 401 | The Authorization header is missing, malformed, or the API key is unknown or revoked | | 429 | The API key exceeded its request budget for the window | # Password-free dashboard sign-in (/docs/changelog/2026-07-16-password-free-dashboard-sign-in) all businesses # Passkey wallets and sending (/docs/changelog/2026-07-20-passkey-wallets-and-sending) non-treasury, treasury # Real-time notifications (/docs/changelog/2026-07-21-real-time-notifications) all businesses # Seven Ethereum-family networks and fee sponsorship (/docs/changelog/2026-07-30-seven-ethereum-family-networks-and-fee-sponsorship) non-treasury, treasury # Send on Solana without holding SOL (/docs/changelog/2026-08-03-send-on-solana-without-holding-sol) non-treasury, treasury # Balances and quotes endpoints (/docs/changelog/2026-08-04-api-balances-and-quotes-endpoints) # Quotes before sending (/docs/changelog/2026-08-04-quotes-before-sending) non-treasury, treasury # One send endpoint for every wallet type (/docs/changelog/2026-08-05-api-one-send-endpoint-for-every-wallet-type) # One send request for every network (/docs/changelog/2026-08-05-one-send-request-for-every-network) non-treasury, treasury # Business dashboard (/docs/changelog/2026-08-06-business-dashboard) all businesses # Business verification (KYB) (/docs/changelog/2026-08-07-business-verification-kyb) all businesses # Recovery endpoints (/docs/changelog/2026-08-08-api-recovery-endpoints) # Backup passkeys (/docs/changelog/2026-08-08-backup-passkeys) non-treasury, treasury # Recovery contacts and owner alerts (/docs/changelog/2026-08-11-recovery-contacts-and-owner-alerts) non-treasury, treasury # Automatic deposit detection (/docs/changelog/2026-08-13-automatic-deposit-detection) non-treasury, treasury # All networks in one wallet request (/docs/changelog/2026-08-18-all-networks-in-one-wallet-request) non-treasury, treasury # Provision wallets on every chain (/docs/changelog/2026-08-18-api-provision-wallets-on-every-chain) # Treasury wallets and approval thresholds (/docs/changelog/2026-08-19-api-treasury-wallets-and-approval-thresholds) # Treasury wallets with multiple approvals (/docs/changelog/2026-08-19-treasury-wallets-with-multiple-approvals) treasury # Signer spending limit endpoints (/docs/changelog/2026-08-20-api-signer-spending-limit-endpoints) # Spending limits per signer (/docs/changelog/2026-08-20-spending-limits-per-signer) treasury # A treasury wallet for each customer (/docs/changelog/2026-08-22-a-treasury-wallet-for-each-customer) treasury # Webhook endpoint management (/docs/changelog/2026-08-24-api-webhook-endpoint-management) # Notification endpoint management (/docs/changelog/2026-08-24-notification-endpoint-management) all businesses # Send USD₮ on Tron without holding TRX (/docs/changelog/2026-08-25-send-usdt-on-tron-without-holding-trx) non-treasury, treasury # Swap endpoints (/docs/changelog/2026-08-26-api-swap-endpoints) # USD₮ and gold swaps (/docs/changelog/2026-08-26-usdt-and-gold-swaps) non-treasury, treasury # Tron sweep (/docs/changelog/2026-08-29-tron-sweep) non-treasury, treasury # Multiple passkeys per wallet (/docs/changelog/2026-08-30-multiple-passkeys-per-wallet) non-treasury, treasury # Payments and signing threshold endpoints (/docs/changelog/2026-08-31-api-payments-and-signing-threshold-endpoints) # Payment links (/docs/changelog/2026-08-31-payment-links) payment-link projects # Faster Bitcoin and Spark deposits (/docs/changelog/2026-09-03-faster-bitcoin-and-spark-deposits) non-treasury, treasury # Bitcoin and Spark reliability (/docs/changelog/2026-09-06-bitcoin-and-spark-reliability) non-treasury, treasury # Four transaction statuses (/docs/changelog/2026-09-11-api-four-transaction-statuses) # Four payment statuses (/docs/changelog/2026-09-11-four-payment-statuses) non-treasury, treasury # Recovery guardian on every wallet (/docs/changelog/2026-09-16-recovery-guardian-on-every-wallet) non-treasury, treasury # Immediate failure reporting (/docs/changelog/2026-09-17-immediate-failure-reporting) non-treasury, treasury # EVM deposits confirm after one block (/docs/changelog/2026-09-19-api-evm-deposits-confirm-after-one-block) # One-confirmation deposits (/docs/changelog/2026-09-19-one-confirmation-deposits) non-treasury, treasury # Network fees charged to the business (/docs/changelog/2026-09-21-network-fees-charged-to-the-business) all businesses # Recover access to a new passkey (/docs/changelog/2026-09-22-recover-access-to-a-new-passkey) non-treasury, treasury # Recovery when a business closes (/docs/changelog/2026-09-23-recovery-when-a-business-closes) non-treasury, treasury # Guardian rotation and removal (/docs/changelog/2026-09-24-guardian-rotation-and-removal) non-treasury, treasury # Dashboard overview (/docs/changelog/2026-10-03-dashboard-overview) all businesses # Payment-link approvals (/docs/changelog/2026-10-04-payment-link-approvals) payment-link projects # Approvers from your team (/docs/changelog/2026-10-06-approvers-from-your-team) payment-link projects # Approver limits (/docs/changelog/2026-10-07-approver-limits) payment-link projects # Company logo and date filters (/docs/changelog/2026-10-12-company-logo-and-date-filters) all businesses # Bridge endpoints and events (/docs/changelog/2026-10-20-api-bridge-endpoints-and-events) # One USD₮ balance and cross-network transfers (/docs/changelog/2026-10-20-one-usdt-balance-and-cross-network-transfers) non-treasury, treasury # Recipients receive a single USD₮ token (/docs/changelog/2026-10-21-recipients-receive-a-single-usdt-token) non-treasury, treasury # Optimism USD₮ sends deliver USDT only (/docs/changelog/2026-10-22-api-optimism-usdt-sends-deliver-usdt-only) # USDT on Optimism (/docs/changelog/2026-10-22-usdt-on-optimism) non-treasury, treasury # No deposit events for swap proceeds (/docs/changelog/2026-10-23-api-no-deposit-events-for-swap-proceeds) # Swap proceeds no longer counted as deposits (/docs/changelog/2026-10-23-swap-proceeds-no-longer-counted-as-deposits) non-treasury, treasury # Changelog (/docs/changelog) # Introduction (/docs/guides) ## Start here [#start-here] - [Quick Start](/docs/guides/platform/quick-start): Create your first customer and wallet. - [Wallets](/docs/guides/wallets): Passkey-secured wallets on every supported network. - [API Reference](/docs/api-reference): Every endpoint, with request and response examples. - [Changelog](/docs/changelog): What changed in each release. ## Products [#products] - [Platform & Onboarding](/docs/guides/platform): Set up your business on Wallet Infrastructure: authentication, projects, verification and team access. - [Wallets](/docs/guides/wallets): Create passkey-secured wallets for your customers and your business, send and receive payments on every supported network. - [Treasury](/docs/guides/treasury): Treasury wallets with multiple signers, multiple approvals per payment and spending limits for each signer. - [Swaps & Transfers](/docs/guides/swaps-and-transfers): Swap USD₮ and gold, hold USD₮ as one balance, and move USD₮ between networks. - [Payment Links](/docs/guides/payment-links): Collect payments from anyone on the networks you choose, with team approvers for larger amounts. - [Account Recovery](/docs/guides/recovery): Let customers regain access to their wallet after losing a device, with the real owner protected throughout. - [Notifications](/docs/guides/notifications): Receive signed, real-time notifications for every wallet, payment, deposit, recovery and transfer event. ## Wallet types [#wallet-types] | Wallet type | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Non-treasury** | Belongs to one of your customers and is secured by that customer's passkey. | | **Treasury** | Belongs to your business or one of your customers. Supports multiple signers, multiple approvals per payment and spending limits per signer. | # Event Catalogue (/docs/guides/notifications/events) ## Envelope [#envelope] Every event has the same envelope: | Field | Value | | ------------- | -------------------------------------------- | | `event_id` | Unique id. Also sent as `x-idempotency-key`. | | `event_type` | The event name. | | `business_id` | Your business. | | `project_id` | The project the event belongs to. | | `customer_id` | The customer, when there is one. | | `occurred_at` | When it happened. | | `data` | The event's details. | ## Events [#events] | Area | Events | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wallets | `wallet.created`, `wallet.passkey.enrolled`, `wallet.passkey.revoked`, `wallet.passkey.removal_blocked` | | Transactions | `transaction.approval_required`, `transaction.submitted`, `transaction.pending`, `transaction.success`, `transaction.failed`, `transaction.reconciled`, `transaction.reconciliation_expired`, `transaction.summary_corrected` | | Deposits | `deposit.detected`, `deposit.confirmed` | | Bridges | `bridge.filled`, `bridge.refunded`, `bridge.failed`, `bridge.stalled` | | Payments | `payment.created`, `payment.detected`, `payment.approval_required`, `payment.approval_expired`, `payment.settled`, `payment.unclaimed`, `payment.recovered`, `payment.cancelled`, `payment.expired`, `payment.settlement_stalled` | | Recovery | `recovery.code`, `recovery.started`, `recovery.reminder`, `recovery.completed`, `recovery.cancelled`, `recovery.failed` | `recovery.cancelled` carries a `reason` of `token`, `passkey` or `business`; accept all three. # Overview (/docs/guides/notifications) ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | ------------------- | ------------------------------------------------------------------------- | -------------------- | | Real-time events | Wallet, transaction, deposit, bridge, payment and recovery events. | all businesses | | Signed deliveries | Every notification is signed with your endpoint's secret. | all businesses | | Automatic retries | Failed deliveries are retried. | all businesses | | Exact subscriptions | Subscribe to named events; unknown names are refused when you register. | all businesses | | Endpoint management | Manage endpoints through the API or the dashboard, with delivery history. | all businesses | ## Guides [#guides] - [Setup and Verification](/docs/guides/notifications/setup): Register notification endpoints and verify signed notifications. - [Event Catalogue](/docs/guides/notifications/events): Every event we send, and the payload envelope. # Setup and Verification (/docs/guides/notifications/setup) - [Sample: webhook consumer](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart/tree/main/03-webhook-consumer): Verify signatures, reject stale events, deduplicate and process durably. Delivery flow: Event → Your endpoint, then Delivered or Retried later. ## Register an endpoint [#register-an-endpoint] [Create webhook endpoint](/docs/api-reference/webhooks/create-webhook-endpoint) with: | Field | Value | | -------- | -------------------------------------------------------------------------------- | | `url` | A public `https` address. Private and loopback addresses are refused. | | `events` | The events to receive, by exact name. Wildcards such as `deposit.*` are refused. | The response includes the signing secret, shown only once. ## Choosing the project [#choosing-the-project] Endpoints receive the events of one project. By default that is the project of the API key you call with. To manage another project's endpoints, add `?projectId=` to any webhook endpoint request. A key can name its own project, or any of your business's payment-links projects in the same environment. Other projects are managed in the dashboard. Payment events go to the project the payment belongs to, so register endpoints on your payment-links project to receive them. To add events to an endpoint, [update it](/docs/api-reference/webhooks/update-webhook-endpoint) rather than registering the same URL again. `recovery.code` and `recovery.started` are managed only in the dashboard, by an owner or admin. ## Verify a notification [#verify-a-notification] Each notification is a `POST` with these headers: | Header | Value | | ------------------- | ------------------------------------------ | | `x-signature` | `t=,v1=` | | `x-timestamp` | When it was sent, in Unix seconds. | | `x-event-type` | The event name. | | `x-idempotency-key` | The event's id. Use it to skip duplicates. | The signature is the hex HMAC-SHA256 of `.` with your signing secret. ```js import { createHmac, timingSafeEqual } from 'node:crypto'; function verify(secret, rawBody, header) { const parts = Object.fromEntries(header.split(',').map(p => p.split('='))); const expected = createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex'); const a = Buffer.from(parts.v1, 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && timingSafeEqual(a, b); } ``` ## Responding and retries [#responding-and-retries] | Your response | We | | ---------------------------------- | ------------------ | | `2xx` | Mark it delivered. | | `5xx`, `408`, `429` or no response | Retry it later. | | Any other `3xx` or `4xx` | Stop. No retry. | Respond quickly with `2xx`, then process the event. ## Delivery guarantees [#delivery-guarantees] | Guarantee | Detail | | ------------- | ------------------------------------------------------------------------------------------------------------------ | | At least once | An event can be delivered more than once. Deduplicate on `x-idempotency-key`. | | Order | Not guaranteed. Act on the status in each event, and read the API when unsure. | | Retries | Exponential backoff with random variation, starting at about 5 seconds and capped at 1 hour between attempts. | | Retry limit | Up to 15 attempts, then the delivery stops. The total retry window depends on scheduling and the random variation. | | History | Delivery history is kept for 30 days. | | Timestamp | `x-timestamp` is when it was sent. We recommend rejecting events more than 5 minutes old. | ## Sample consumer [#sample-consumer] ```ts import express from 'express'; const app = express(); app.post( '/webhooks/wallet', express.raw({ type: 'application/json' }), async (req, res) => { const rawBody = req.body.toString('utf8'); const signature = req.header('x-signature') ?? ''; const sentAt = Number(req.header('x-timestamp')); const eventId = req.header('x-idempotency-key') ?? ''; if (!verify(process.env.WEBHOOK_SECRET!, rawBody, signature)) return res.sendStatus(401); if (Math.abs(Date.now() / 1000 - sentAt) > 300) return res.sendStatus(400); if (await alreadyProcessed(eventId)) return res.sendStatus(200); await saveForProcessing(eventId, JSON.parse(rawBody)); res.sendStatus(200); }, ); ``` `verify` is the function above. `alreadyProcessed` and `saveForProcessing` are your own storage: record the event first, respond, then process it from your store. ## Errors [#errors] | Code | Status | Cause | | ------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `validation_failed` | 400 | An event name that does not exist, a wildcard such as `deposit.*`, a URL that is not a public `https` address, or `projectId` given more than once. | | `validation_failed` | 409 | Another endpoint in the project already has the same URL and the same event. | | `forbidden` | 403 | `recovery.code` or `recovery.started` was requested with an API key, or `projectId` names a project this key cannot manage. | # Approvals and Recovered Funds (/docs/guides/payment-links/approvals-and-recovered-funds) > **In the dashboard:** Approvals, approver limits and recovering held funds are done by your team in the dashboard. They are not part of the API. ## Approvers [#approvers] Approvers are team members who approve payments with their own passkey. Add them, and register their passkeys, in the dashboard. ## Approvals [#approvals] Payments above your project's approval ceiling wait for an approver before the funds are sent to your address. Payments below it are sent automatically. You receive `payment.approval_required` when a payment is waiting, and `payment.approval_expired` if no one approves it in time. ## Approver limits [#approver-limits] An owner can cap how much each approver may approve: | Limit | Applies to | | ----------- | ----------------------------------- | | Per payment | Each payment the approver approves. | | Daily | The approver's total for the day. | Limits are set in US dollars across every asset, or per asset (USD₮, XAUT, BTC). An approver with no limits can approve any amount. ## Held funds [#held-funds] Funds that arrive but do not complete a payment are held for you: - the excess of an overpayment; - payments on other networks after the payment completes; - payments that arrive after the payment expires or is cancelled. You receive `payment.unclaimed` when funds are held. Recover them in the dashboard to an address you name; an approver approves each recovery. You receive `payment.recovered` when they are sent. # Creating a Payment (/docs/guides/payment-links/creating-a-payment) - [Sample: payment link](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart/tree/main/04-payment-link): Create receiving addresses with an Idempotency-Key and track payment status. Money flow: Payer wallet → Deposit address → Payment, then Your address or Your server. ## Before you start [#before-you-start] You need a payment-links project. Payments are created in your business's default payment-links project, set in the dashboard; without one, requests are refused with `409`. ## Create a payment [#create-a-payment] Send [Create payment](/docs/api-reference/payment-links/create-payment) with an `Idempotency-Key` header. The key is required: a retry without it would create a second set of addresses the payer never sees. | Field | Value | | -------------- | -------------------------------------------------------------------------------------------- | | `reference` | Your own reference, for example an invoice number. | | `destinations` | One entry per network family you accept: `evm`, `bitcoin`, `spark`, `solana`, `tron`, `ton`. | Each destination takes: | Field | Value | | -------------------- | ---------------------------------------------------------------- | | `asset` | What the payer sends, for example `USDT` or `BTC`. | | `amount` | Exactly what must arrive, for example `"10.5"`. | | `destinationAddress` | Where the funds go once they arrive. It cannot be changed later. | One `evm` entry covers every EVM network. ## Share the addresses [#share-the-addresses] The response has one deposit address per family. Show them to the payer. - The first exact payment on any network completes the payment. - A short payment does not complete it. - An overpayment completes it, and the excess is held for you. - Payments that arrive on other networks after it completes are held for you. See [Approvals and Recovered Funds](/docs/guides/payment-links/approvals-and-recovered-funds) for held funds. ## Expiry and cancelling [#expiry-and-cancelling] A payment expires after 30 minutes. [Cancel payment](/docs/api-reference/payment-links/cancel-payment) stops one sooner, while it is still awaiting payment. After a payment expires or is cancelled, its addresses stay watched for 24 hours, so funds sent late are held, not lost. ## Statuses [#statuses] | Status | Meaning | | ------------------ | ------------------------------------------------------------------------ | | `AWAITING_PAYMENT` | Waiting for the payer. | | `FULFILLED` | Paid. Funds are sent to your address, after approval when one is needed. | | `EXPIRED` | Not paid within 30 minutes. | | `CANCELLED` | Cancelled before it was paid. | ## Tracking [#tracking] - [List payments](/docs/api-reference/payment-links/list-payments) and [Get payment](/docs/api-reference/payment-links/get-payment) show each payment with its addresses, attempts and held funds. - `payment.*` events arrive at the webhook endpoints of the project the payment belongs to. See [Notifications](/docs/guides/notifications). ## Errors [#errors] | Code | Status | Cause | | ------------------- | ------ | --------------------------------------------------------------------------- | | `validation_failed` | 400 | The `Idempotency-Key` header is missing. | | `validation_failed` | 409 | No default payment-links project is set and the key does not belong to one. | | `validation_failed` | 409 | The payment cannot be cancelled because it is no longer awaiting payment. | | `not_found` | 404 | No payment has this id. | # Overview (/docs/guides/payment-links) ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | --------------------- | ------------------------------------------------------------------------------------- | ----------------------------- | | Create a payment | Receiving addresses on the networks you choose; the first exact payment completes it. | payment-link projects | | Every network family | Accept EVM, Bitcoin, Spark, Solana, Tron and TON in one payment. | payment-link projects | | Automatic settlement | Funds move to your address on completion. | payment-link projects | | Expiry and cancelling | Payments expire after 30 minutes, or cancel one sooner. | payment-link projects | | Payment events | Follow each payment, approval and held deposit with notifications. | payment-link projects | | Team approvers | In the dashboard: larger payments approved by a named team member. | payment-link projects | | Approver limits | In the dashboard: per-payment and daily limits for each approver. | payment-link projects | | Held funds | In the dashboard: recover overpayments, late payments and payments on other networks. | payment-link projects | ## Guides [#guides] - [Creating a Payment](/docs/guides/payment-links/creating-a-payment): Create a payment and share its receiving addresses. - [Approvals and Recovered Funds](/docs/guides/payment-links/approvals-and-recovered-funds): In the dashboard: team approvers and limits, and recovering held funds. # Authentication and API Keys (/docs/guides/platform/authentication) ## API keys [#api-keys] Every request carries an API key as a Bearer token. ```http Authorization: Bearer sk_live_... ``` | Key | Project | Use | | ---------- | ------- | ----------------------------------------------- | | `sk_test_` | Test | Build and test. Available before verification. | | `sk_live_` | Live | Real funds. Requires a verified business (KYB). | A key is shown once, when it is issued. We store only a hash of it, so a lost key cannot be recovered; issue a new one and revoke the old. ## Test and live [#test-and-live] Each project is either test or live, and its keys match. Creating a live project, or calling with a live key, before your business is verified returns `403 kyb_required`. See [Verification and Team](/docs/guides/platform/verification-and-team). ## Scopes [#scopes] Each key carries the scopes you grant it. Every endpoint in the [API Reference](/docs/api-reference) states the scope it requires. | Resource | Scopes | | ----------------- | --------------------------------------------------- | | Customers | `customers:read`, `customers:write` | | Wallets | `wallets:read`, `wallets:create`, `wallets:write` | | Transactions | `transactions:read`, `transactions:create` | | Quotes | `quotes:read` | | Payment Links | `payment_links:read`, `payment_links:write` | | Webhooks | `webhooks:read`, `webhooks:write` | | Recoveries | `recoveries:read`, `recoveries:create` | | Recovery contacts | `recovery-contacts:read`, `recovery-contacts:write` | ### Scope by area [#scope-by-area] | Area | Read | Change | | ---------------------------- | ---------------------- | ----------------------------------------------- | | Wallets, passkeys, guardians | `wallets:read` | `wallets:create` (new wallets), `wallets:write` | | Treasury and spend caps | `wallets:read` | `wallets:write` | | Payments and approvals | `transactions:read` | `transactions:create` | | Swaps and transfers | `quotes:read` (quotes) | `transactions:create` | ## Acting for a customer [#acting-for-a-customer] Requests about one customer's wallets carry the `x-customer-id` header, set to the `id` returned when you created the customer. ```http x-customer-id: ``` ## Errors [#errors] | Code | Status | Cause | | --------------------- | ------ | ------------------------------------------------------------------------------------- | | `unauthorized` | 401 | The `Authorization` header is missing or malformed, or the key is unknown or revoked. | | `forbidden` | 403 | The key lacks the scope the endpoint requires. | | `kyb_required` | 403 | A live project or live key was used before your business is verified. | | `rate_limit_exceeded` | 429 | The key exceeded its request budget for the window. | # Build with AI Agents (/docs/guides/platform/build-with-ai-agents) The integration guidance gives your coding agent the integration order, the two signing paths, webhook verification and the common mistakes. It also points the agent at these docs as Markdown, so it reads the exact endpoints instead of guessing. - [Sample code on GitHub](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart): Runnable TypeScript for each step of the integration. The guidance points your agent at these samples. ## Install [#install] Run the command for your agent from the root of your project, and commit the file so your whole team gets it. | Agent | File it reads | | --------------------------------------- | -------------------------------------------------- | | Claude Code | `.claude/skills/shiga-wallet-integration/SKILL.md` | | Codex, Cursor, GitHub Copilot, Windsurf | `AGENTS.md` | | Gemini CLI | `GEMINI.md`, importing `AGENTS.md` | ### Claude Code [#claude-code] ```bash mkdir -p .claude/skills/shiga-wallet-integration curl -fsSL https://docs.wallet.shiga.io/skills/shiga-wallet-integration/SKILL.md \ -o .claude/skills/shiga-wallet-integration/SKILL.md ``` Claude Code loads the skill when the task matches, or run `/shiga-wallet-integration`. To use it in every project, install it to `~/.claude/skills/shiga-wallet-integration/` instead. ### Codex, Cursor, GitHub Copilot and Windsurf [#codex-cursor-github-copilot-and-windsurf] ```bash grep -qs '^# Shiga Wallet Infrastructure integration' AGENTS.md || \ { echo; curl -fsSL https://docs.wallet.shiga.io/skills/shiga-wallet-integration/AGENTS.md; } >> AGENTS.md ``` The guidance is appended to an existing `AGENTS.md`, never replacing it. Running the command again adds nothing. ### Gemini CLI [#gemini-cli] Add `AGENTS.md` as above, then import it from `GEMINI.md`: ```bash grep -qsx '@./AGENTS.md' GEMINI.md || printf '\n@./AGENTS.md\n' >> GEMINI.md ``` An existing `GEMINI.md` keeps its instructions; the import is added once. ## Use [#use] Describe what you want, for example: ```text Integrate Shiga Wallet Infrastructure: create a customer at sign-up, register their passkey, provision wallets, and credit deposits from webhooks. ``` ## What the guidance covers [#what-the-guidance-covers] | Area | Guidance | | ------------- | --------------------------------------------------------------------------- | | Setup | API key on your server only, `x-customer-id` on every per-customer request. | | Wallets | Passkey registration with the COSE public key, then Provision wallets. | | Payments | EVM `userOpHash` signing, and the transfer challenge on the other networks. | | Notifications | Signature check, stale-event rejection, deduplication. | | Reliability | Credit only on `confirmed`, read state before retrying, back off on `429`. | ## Any other assistant [#any-other-assistant] Any AI assistant can read these docs directly: | Source | URL | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Page index | [`/llms.txt`](/llms.txt) | | All docs and the API | [`/llms-full.txt`](/llms-full.txt) | | OpenAPI spec | [`/openapi.json`](/openapi.json) | | One page | `/llms.mdx/docs//content.md`, for example [`/llms.mdx/docs/guides/wallets/sending/content.md`](/llms.mdx/docs/guides/wallets/sending/content.md) | Review generated code before you ship it, and work through the [Go-Live Checklist](/docs/guides/platform/go-live-checklist). # Go-Live Checklist (/docs/guides/platform/go-live-checklist) ## Account [#account] - [ ] Your business is verified, and you have a live project and live API key. See [Verification and Team](/docs/guides/platform/verification-and-team). - [ ] Each API key has only the scopes it needs, and live keys are stored only on your server. See [Authentication and API Keys](/docs/guides/platform/authentication). - [ ] Your team members have the right roles. ## Wallets and payments [#wallets-and-payments] - [ ] Passkeys register on your production domain, and `rpId` and `origins` match it. - [ ] Customers are credited only on `deposit.confirmed`. - [ ] Payments are marked paid only at `confirmed` (`transaction.success`). - [ ] Retries follow [Reliability](/docs/guides/platform/reliability): no resubmitting after a timeout without reading the state first. - [ ] You handle every error on the pages you use, including `insufficient_funds`, `wallet_not_found` and `idempotency_conflict`. ## Notifications [#notifications] - [ ] A live webhook endpoint is registered on each project, including your payment-links project. - [ ] Signatures are verified, old events are rejected and duplicates are skipped. See [Setup and Verification](/docs/guides/notifications/setup). - [ ] Your endpoint responds `2xx` quickly and processes events afterwards. ## Recovery [#recovery] - [ ] Wallets are enrolled for recovery, and customers have a verified recovery contact. See [How Recovery Works](/docs/guides/recovery/how-recovery-works). ## Operations [#operations] - [ ] You reconcile your records against [List transactions](/docs/api-reference/transactions/list-transactions) and balances regularly. - [ ] Your client backs off on `429`. See [Errors and Limits](/docs/guides/support/errors-and-limits). - [ ] You log `request_id` from errors, to quote when you contact us. # Overview (/docs/guides/platform) ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | --------------------------- | ---------------------------------------------------------------------------------- | -------------------- | | Password-free sign-in | Sign in to the dashboard with a passkey or magic link. | all businesses | | Test and live projects | Build in test before verification; go live once your business is verified. | all businesses | | Scoped API keys | Grant each key only the scopes it needs. Keys are shown once and stored as a hash. | all businesses | | Customer references | Create customers with your own reference and act for them with `x-customer-id`. | all businesses | | Business verification (KYB) | Verify your business to unlock live projects and live keys. | all businesses | | Team members and roles | Invite your team as owner, admin, developer or viewer. | all businesses | ## Guides [#guides] - [Quick Start](/docs/guides/platform/quick-start): Create your first customer and wallet. - [Integration Walkthrough](/docs/guides/platform/integration-walkthrough): One customer, from wallet to confirmed payment. - [Reliability](/docs/guides/platform/reliability): Retries, duplicates, timeouts and reconciliation. - [Go-Live Checklist](/docs/guides/platform/go-live-checklist): What to confirm before moving to live. - [Authentication and API Keys](/docs/guides/platform/authentication): Authenticate requests, issue scoped API keys, and separate test from live projects. - [Verification and Team](/docs/guides/platform/verification-and-team): Verify your business to unlock live mode, and manage team members and roles. # Integration Walkthrough (/docs/guides/platform/integration-walkthrough) - [Sample code on GitHub](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart): Runnable TypeScript for every step of this walkthrough, plus the complete quickstart app. Integration: Create customer → Register passkey → Provision wallets → Receive and send → Reconcile. ## Architecture [#architecture] Keep your API key on your server. Your app handles the customer and their passkey; your server calls Wallet Infrastructure and processes notifications. | Part | Does | | --------------------- | ------------------------------------------------------------------- | | Your app | Registers the customer's passkey and asks them to approve payments. | | Your server | Holds the API key, calls the API and keeps your own records. | | Your webhook endpoint | Verifies, deduplicates and records notifications. | ## 1. Create the customer [#1-create-the-customer] ```bash curl -X POST https://api.pilot.shiga.io/v1/customers \ -H 'Authorization: Bearer sk_test_...' \ -H 'Content-Type: application/json' \ -d '{ "reference": "cust_8812" }' ``` Store the `id` from the response. Every later request about this customer sends it as `x-customer-id`. ## 2. Register a passkey on the customer's device [#2-register-a-passkey-on-the-customers-device] Use WebAuthn with your own domain as the relying party and an ES256 (P-256) key. This example uses `@simplewebauthn`. ```ts import { startRegistration } from '@simplewebauthn/browser'; const options = await fetch('/passkey/options').then(r => r.json()); const registration = await startRegistration({ optionsJSON: options }); await fetch('/passkey/register', { method: 'POST', body: JSON.stringify(registration), }); ``` On your server, verify the registration and send its credential to Wallet Infrastructure: ```ts import { verifyRegistrationResponse } from '@simplewebauthn/server'; const { registrationInfo } = await verifyRegistrationResponse({ response: registration, expectedChallenge, expectedOrigin: 'https://your-domain.com', expectedRPID: 'your-domain.com', }); const passkeyRegistration = { credentialId: registrationInfo.credential.id, credentialPublicKey: Buffer.from( registrationInfo.credential.publicKey, ).toString('base64url'), rpId: 'your-domain.com', origins: ['https://your-domain.com'], }; ``` > **Send the COSE public key:** `credentialPublicKey` is the COSE key from the verified registration, as base64url. The browser's `publicKey` field is a different format and is refused. - [Sample: passkey and wallets](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart/tree/main/01-passkey-and-wallets): Register an ES256 passkey, extract the COSE key and provision wallets. ## 3. Provision the wallets [#3-provision-the-wallets] ```bash curl -X POST https://api.pilot.shiga.io/v1/wallets/provision \ -H 'Authorization: Bearer sk_test_...' \ -H 'x-customer-id: ' \ -H 'Content-Type: application/json' \ -d '{ "passkeyRegistration": { "credentialId": "...", "credentialPublicKey": "...", "rpId": "your-domain.com", "origins": ["https://your-domain.com"] } }' ``` The response lists each wallet with its `moduleId`, `chain`, `address` and `status`: `created`, `already_exists` or `failed`. Retrying is safe; wallets that exist come back as `already_exists`. ## 4. Receive funds [#4-receive-funds] Show the customer their address. Credit them on `deposit.confirmed`; `deposit.detected` means the deposit is seen, not final. See [Receiving Deposits](/docs/guides/wallets/receiving-deposits). ## 5. Send funds [#5-send-funds] Quote, have the customer approve with their passkey, and submit. See [Sending Payments](/docs/guides/wallets/sending). ## 6. Confirm and reconcile [#6-confirm-and-reconcile] A payment is complete only when its status is `confirmed`. `submitted` and `pending` are not final. See [Reliability](/docs/guides/platform/reliability). # Quick Start (/docs/guides/platform/quick-start) - [Run the sample](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart/tree/main/quickstart): The complete journey in one TypeScript app: customer, passkey, wallets, payment and webhooks. ## 1. Get a test API key [#1-get-a-test-api-key] Create a test project in the dashboard and issue an API key for it. Test keys start with `sk_test_` and work before your business is verified. The key is shown once, so store it securely. ## 2. Authenticate [#2-authenticate] Send the key as a Bearer token on every request. ```bash curl https://api.pilot.shiga.io/v1/customers \ -H 'Authorization: Bearer sk_test_...' ``` ## 3. Create a customer [#3-create-a-customer] ```bash curl -X POST https://api.pilot.shiga.io/v1/customers \ -H 'Authorization: Bearer sk_test_...' \ -H 'Content-Type: application/json' \ -d '{ "reference": "your-customer-id" }' ``` `reference` is your own identifier for the customer. Keep the `id` in the response; it identifies the customer in the requests that follow. ## 4. Create the customer's wallets [#4-create-the-customers-wallets] Register a passkey on the customer's device, then send its registration to [Provision wallets](/docs/api-reference/wallets/provision-wallets). One call creates a wallet on every supported network, all secured by that passkey. ```bash curl -X POST https://api.pilot.shiga.io/v1/wallets/provision \ -H 'Authorization: Bearer sk_test_...' \ -H 'x-customer-id: ' \ -H 'Content-Type: application/json' \ -d '{ "passkeyRegistration": { "credentialId": "...", "credentialPublicKey": "...", "rpId": "your-domain.com", "origins": ["https://your-domain.com"] } }' ``` The response lists each wallet with its network and address. ## 5. Read balances [#5-read-balances] ```bash curl 'https://api.pilot.shiga.io/v1/wallets/balances?customerId=' \ -H 'Authorization: Bearer sk_test_...' ``` ## Next steps [#next-steps] - [Sending](/docs/guides/wallets/sending): Quote and send a payment, approved with the customer's passkey. - [Notifications](/docs/guides/notifications): Receive events as wallets, deposits and transactions change. - [API Reference](/docs/api-reference): Every endpoint, with request and response examples. # Reliability (/docs/guides/platform/reliability) ## Only `confirmed` means paid [#only-confirmed-means-paid] | Status | Event | Credit or settle? | | ----------- | ----------------------- | ----------------- | | `submitted` | `transaction.submitted` | No | | `pending` | `transaction.pending` | No | | `confirmed` | `transaction.success` | Yes | | `failed` | `transaction.failed` | No | `transaction.reconciled` is the signing side's report of the same confirmation as `transaction.success`; act on `transaction.success`. `transaction.summary_corrected` corrects a past transaction's details and does not change its status. ## Avoiding duplicate operations [#avoiding-duplicate-operations] | Operation | Protection | | ----------------------------------------- | ----------------------------------------------------------------------------------------------- | | Create payment | The `Idempotency-Key` header is required. Reuse the same key when retrying. Unique per project. | | Start recovery | Send an `Idempotency-Key` header; retries with the same key are safe. | | Send on Bitcoin, Spark, Solana, Tron, TON | Your `transactionId` identifies the transfer. | | Approvals | A signer's repeated approval is counted once. | | Provision wallets | Wallets that already exist come back as `already_exists`. | | Swaps and transfers | Never retried automatically; a second submit is refused with `idempotency_conflict`. | `idempotency_conflict` means the key or request was already used for something different. ### When a retry is refused [#when-a-retry-is-refused] | Response | Meaning | Do | | -------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------- | | `409 idempotency_conflict`, "retry shortly" | The first request is still running. | Wait, then retry the same request. | | `409 idempotency_conflict`, "needs reconciliation" | An earlier attempt may have reached the network without finishing. | Do not resend. Read List transactions first. | ### Retrying by operation [#retrying-by-operation] | Operation | Safe to retry? | | --------------------- | ---------------------------------------------------------------------------------------------------------- | | Provision wallets | Yes. Existing wallets come back as `already_exists`. | | Send (EVM) | Resubmitting the same signature is counted once. Do not prepare a new payment until the first is `failed`. | | Send (other networks) | Yes, with the same `transactionId` and details, within 24 hours. | | Create payment | Yes, with the same `Idempotency-Key`. | | Submit bridge or swap | No. Read Get bridge or List transactions instead. | ## Timeouts [#timeouts] A timeout is not a failure; the operation may still complete. Before retrying, read its state: | After a timeout on | Read | | ------------------ | ----------------------------------------------------------------------- | | A payment | [List transactions](/docs/api-reference/transactions/list-transactions) | | A transfer | [Get bridge](/docs/api-reference/bridges/get-bridge) | | A swap | [List transactions](/docs/api-reference/transactions/list-transactions) | | Adding a passkey | [List passkeys](/docs/api-reference/passkeys/list-passkeys) | ## Processing notifications [#processing-notifications] 1. Verify `x-signature`. See [Setup and Verification](/docs/guides/notifications/setup). 2. Reject a notification whose `x-timestamp` is far from now; we recommend 5 minutes. 3. Skip any `x-idempotency-key` you have already processed. 4. Respond `2xx` quickly, then process. Notifications can arrive out of order and more than once. Act on the status in the event, and if unsure, read the current state from the API. Delivery guarantees are on [Setup and Verification](/docs/guides/notifications/setup#delivery-guarantees). ## Reconciling [#reconciling] Regularly compare your records with [List transactions](/docs/api-reference/transactions/list-transactions) and [List wallet balances](/docs/api-reference/balances/list-wallet-balances), and treat the API as the source of truth. # Verification and Team (/docs/guides/platform/verification-and-team) ## Business verification (KYB) [#business-verification-kyb] Test projects work straight away. Live projects and live API keys need a verified business; until then they return `403 kyb_required`. Complete verification from **Verification** in the dashboard. ## Signing in [#signing-in] Team members sign in to the dashboard with a passkey or a magic link. ## Roles [#roles] | Role | What they can do | | ----------- | -------------------------------------------------------------------------------------------------------------------------- | | `owner` | Everything, including adding or changing other owners and admins. | | `admin` | Manage setup, verification, team, transactions, projects, webhooks and settings. Can add or change developers and viewers. | | `developer` | Manage projects and webhooks. View everything else. | | `viewer` | View everything. Change nothing. | A business always keeps at least one owner. # How Recovery Works (/docs/guides/recovery/how-recovery-works) Recovery replaces a lost passkey with a new one on the wallet's Safe, on every EVM network the wallet uses. Recovery flow: New passkey → Your server → Recovery started, then New owner or Owner alerted. ## Which wallets can be recovered [#which-wallets-can-be-recovered] | Wallet | Recovery | | ------------------------------------------- | ----------------------------------- | | EVM, non-treasury | Yes, once enrolled with a guardian. | | EVM treasury needing one approval | Yes, once enrolled. | | EVM treasury needing more than one approval | No. Add or remove signers instead. | Your responsibilities: enrol each wallet, set and verify a recovery contact, and confirm the customer's identity before you attest to a recovery. ## 1. Enrol the wallet [#1-enrol-the-wallet] A wallet can be recovered only once it has a guardian. Enrol it with the customer's passkey: - [Enrol customer guardian](/docs/api-reference/guardians/enrol-customer-guardian): one approval covers every network. - [Enrol guardian](/docs/api-reference/guardians/enrol-guardian): per wallet, one approval per network. [Get guardians](/docs/api-reference/guardians/get-guardians) shows which networks are enrolled. Treasury wallets that need more than one approval cannot be enrolled. ## 2. Set a recovery contact [#2-set-a-recovery-contact] A recovery contact is an address the customer confirms. It is how the customer hears about a recovery, even one your business did not start, and how they can still recover if your business stops operating. 1. [Request contact challenge](/docs/api-reference/recovery-contacts/request-contact-challenge), for the customer to sign with their passkey. 2. [Set recovery contact](/docs/api-reference/recovery-contacts/set-recovery-contact). You receive a verification token. 3. Deliver the token to the customer through your own channel. 4. [Verify recovery contact](/docs/api-reference/recovery-contacts/verify-recovery-contact) once they confirm it arrived. [Check contact coverage](/docs/api-reference/recovery-contacts/check-contact-coverage) lists which of your wallets have a verified contact. ## 3. Recover [#3-recover] Register a new passkey on the customer's new device, then authorise the recovery in one of two ways: | Way | How | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Business attests | Your business signs a verification attestation for the customer. | | One-time code | [Send recovery code](/docs/api-reference/recoveries/send-recovery-code) delivers a six-digit code to you to pass on; [Verify recovery code](/docs/api-reference/recoveries/verify-recovery-code) exchanges it for a single-use token. Needs a verified recovery contact. | Then [Start recovery](/docs/api-reference/recoveries/start-recovery). ### Business attestation [#business-attestation] Send `verification` as `{ payload, signatureB64 }`: | Field | Value | | ----------------------- | --------------------------------------------- | | `businessId` | Your business. | | `customerId` | The customer being recovered. | | `walletId` | The wallet, as in the request. | | `newSignerAddress` | The address of the customer's new passkey. | | `chains` | The networks to recover, as in the request. | | `intent` | `"recovery"`. | | `nonce` | A unique string. | | `issuedAt`, `expiresAt` | Epoch milliseconds; at most 15 minutes apart. | Sign the canonical JSON of `payload` (keys sorted at every level, no spaces) with your business's Ed25519 key, and send the signature base64-encoded as `signatureB64`. Register the matching public key in the dashboard first. The payload must match the request exactly, or the recovery is refused with `401`. ## 4. The 72-hour protection window [#4-the-72-hour-protection-window] A recovery waits 72 hours before it completes. During that time the real owner can cancel it with the cancellation token or their passkey: [Cancel recovery](/docs/api-reference/recoveries/cancel-recovery). [Get recovery](/docs/api-reference/recoveries/get-recovery) shows its state on each network. Events: `recovery.started`, `recovery.reminder`, `recovery.completed`, `recovery.cancelled` and `recovery.failed`. ## Rotating or turning off recovery [#rotating-or-turning-off-recovery] - [Rotate guardian](/docs/api-reference/guardians/rotate-guardian) moves the wallet to a new guardian. - [Revoke guardian](/docs/api-reference/guardians/revoke-guardian) turns recovery off. Tell the customer first: once it is off, a lost passkey means a lost wallet. Both need the customer's passkey. ## Errors [#errors] | Code | Status | Cause | | ------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unauthorized` | 401 | The business attestation is invalid, expired, or does not match the request. | | `forbidden` | 403 | Revoking with a passkey that does not own the wallet. | | `recovery_contact_required` | 422 | Recovery by one-time code was started for a customer without a verified recovery contact. | | `recovery_contact_unavailable` | 422 | Recovery contacts are switched off on the platform. | | `validation_failed` | 422 | A recovery code was requested but no endpoint subscribes to `recovery.code`. | | `chain_unavailable` | 502 | Enrolling, rotating or revoking failed on a network: for example the wallet was never enrolled, the new guardian is already registered, or the treasury needs more than one approval. `detail` says which. | # Overview (/docs/guides/recovery) ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | ----------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | | Recovery guardian | Enrol a wallet for recovery with one passkey approval. | non-treasury, treasury | | Recover to a new passkey | Regain access after a lost device, with network fees covered. | non-treasury, treasury | | Two ways to authorise | Your business attests to the customer, or the customer proves it with a one-time code. | non-treasury, treasury | | 72-hour protection window | The real owner can cancel; cancellations are recorded. | non-treasury, treasury | | Recovery contacts | An owner alert the business cannot suppress. | non-treasury, treasury | | Contact coverage | See which wallets have a verified recovery contact. | non-treasury, treasury | | Guardian rotation and opt-out | Rotate the guardian, or turn recovery off with the customer's passkey. | non-treasury, treasury | ## Guides [#guides] - [How Recovery Works](/docs/guides/recovery/how-recovery-works): Guardians, the 72-hour protection window and recovery contacts. # Errors and Limits (/docs/guides/support/errors-and-limits) ## Error format [#error-format] Errors share one shape: | Field | Value | | ------------ | --------------------------------------------------------------------------------- | | `code` | Machine-readable cause, for example `validation_failed`. | | `status` | The HTTP status. | | `title` | A short summary. | | `detail` | What to change, naming the field at fault when there is one. | | `category` | `client` (your request), `upstream` (a network or provider) or `internal` (ours). | | `request_id` | Quote it when you contact us. | ## Common codes [#common-codes] | Code | Status | Meaning | | ---------------------------- | ------------- | ------------------------------------------------------------ | | `unauthorized` | 401 | Missing, malformed, unknown or revoked API key. | | `forbidden` | 403 | The key lacks the scope, or the action is not allowed. | | `kyb_required` | 403 | Live mode needs a verified business. | | `not_found` | 404 | The resource does not exist for this project. | | `wallet_not_found` | 404 | No wallet matches; send `x-customer-id`. | | `validation_failed` | 400, 409 | The request is invalid; `detail` says what to change. | | `insufficient_funds` | 400 | The balance does not cover the amount and fees. | | `idempotency_conflict` | 409 | The idempotency key was used for a different request. | | `passkey_credential_invalid` | 409 | The passkey assertion did not verify. | | `transfer_reverted` | 409 | The network rejected the transfer. | | `chain_unavailable` | 400, 409, 502 | The network or route is not available for this request. | | `rate_limit_exceeded` | 429 | Too many requests; wait and retry. | | `upstream_unavailable` | 502, 503, 504 | A network or provider is unavailable; retry later. | | `unknown` | 500 | Something failed on our side; retry, and quote `request_id`. | ## Rate limits [#rate-limits] | Limit | Requests per 60 seconds | | ------------ | ----------------------- | | Per API key | 300 | | Per business | 3,000 | Over the limit, requests return `429 rate_limit_exceeded`. Responses carry no `Retry-After` or rate-limit headers, so back off and retry with increasing delays. # FAQ (/docs/guides/support/faq) ## Is there a seed phrase? [#is-there-a-seed-phrase] No. Wallets are secured by the customer's passkey. ## Can an API key move funds on its own? [#can-an-api-key-move-funds-on-its-own] No. Every payment is approved with a passkey. An API key alone can prepare a payment but cannot sign it. ## What happens if a customer loses their passkey? [#what-happens-if-a-customer-loses-their-passkey] If the wallet is enrolled for recovery, they can recover to a new passkey. See [How Recovery Works](/docs/guides/recovery/how-recovery-works). ## How do I test? [#how-do-i-test] Use a test project and a `sk_test_` key. Live mode needs a verified business. See [Authentication and API Keys](/docs/guides/platform/authentication). ## Why am I not receiving notifications? [#why-am-i-not-receiving-notifications] Check that the endpoint is a public `https` URL, that it subscribes to the exact event names, and that it belongs to the right project. See [Setup and Verification](/docs/guides/notifications/setup). ## Why was a payment to the wallet itself refused? [#why-was-a-payment-to-the-wallet-itself-refused] A payment to the wallet's own address is refused with `400 validation_failed`; it would move nothing and still cost a network fee. # Glossary (/docs/guides/support/glossary) | Term | Meaning | | ------------- | ----------------------------------------------------------------------------------------------------------- | | Base units | An asset's smallest unit, for example satoshis for BTC. Spend caps use base units. | | COSE key | The passkey's public key in the format the API expects, from the verified registration. | | Display units | The amount as people write it, for example `"12.5"` USD₮. Quotes, payments and transfers use display units. | | Entity key | The identifier of a wallet's owner, returned as `entityKey` by List wallets. | | Passkey | A device credential (Face ID, fingerprint or device PIN) that approves payments. There is no seed phrase. | | Paymaster | The service that pays a network fee on the wallet's behalf when fees are sponsored. | | Safe | The smart account that holds an EVM wallet. Its owners are the customer's passkeys. | | Spark | A Bitcoin layer for fast, fee-free transfers between Spark wallets. | | userOp | A user operation: an EVM payment from a Safe, prepared for passkey signing. | | USD₮0 | A second USD₮ token on Optimism. It counts as one balance with USDT. | | XAUT | Tether Gold, a token backed by gold. | # Networks and Assets (/docs/guides/support/networks-and-assets) ## EVM networks (`evm_erc4337`) [#evm-networks-evm_erc4337] | Network | `chain` | Chain ID | USD₮ contract | XAUT contract | | -------- | ---------- | -------- | -------------------------------------------- | -------------------------------------------- | | Ethereum | `ethereum` | 1 | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | `0x68749665ff8d2d112fa859aa293f07a622782f38` | | Base | `base` | 8453 | `0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2` | — | | Arbitrum | `arbitrum` | 42161 | `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9` | `0x40461291347e1ecbb09499f3371d3f17f10d7159` | | Polygon | `polygon` | 137 | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` | `0xf1815bd50389c46847f0bda824ec8da914045d14` | | Celo | `celo` | 42220 | `0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e` | — | | Optimism | `optimism` | 10 | `0x94b008aA00579c1307B0EF2c499aD98a8ce58e58` | — | | Plasma | `plasma` | 9745 | `0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb` | `0x1B64B9025EEbb9A6239575dF9Ea4b9Ac46D4d193` | USD₮ and XAUT have 6 decimals. ### USD₮ and USD₮0 on Optimism [#usd-and-usd0-on-optimism] Optimism has two USD₮ tokens: USDT (above) and USD₮0 (`0x01bFF41798a0BcF287b996046Ca68b395DbC1071`). They count as one balance, and every USD₮ payment on Optimism arrives as standard USDT. ## Other networks [#other-networks] | Network | `moduleId` | Asset | Identifier | Decimals | | ------- | ---------- | ----- | --------------------------------------------------------- | -------- | | Bitcoin | `bitcoin` | BTC | — | 8 | | Spark | `spark` | BTC | — | 8 | | Solana | `solana` | USD₮ | Mint `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 | | Tron | `tron` | USD₮ | Contract `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | | TON | `ton` | USD₮ | Jetton `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` | 6 | ## Amounts [#amounts] Send amounts in the asset's display unit, for example `"12.5"` USD₮ or `"0.001"` BTC. Spend caps use base units (`limitBaseUnits`). # Troubleshooting (/docs/guides/support/troubleshooting) | Situation | Code or sign | What to do | | ---------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Wallet not yet deployed on a network | `chain_unavailable` (409) | Wait for the wallet to be deployed, then retry. | | Wrong wallet, or none found | `wallet_not_found` (404) | Send `x-customer-id`. | | Passkey approval rejected | `passkey_credential_invalid` | Sign the exact hash or challenge returned, with a registered passkey. | | Not enough for amount and fees | `insufficient_funds` | Quote first, and send at most the quoted maximum. | | Transfer quote expired | `validation_failed` (409) | Quote and prepare again; submit before `quoteExpiresAt`. | | Transfer stuck | `bridge.stalled` | Read [Get bridge](/docs/api-reference/bridges/get-bridge) and contact us with the transfer id. | | Network fee not sponsored | `paymaster_rejected` | Retry. | | Timeouts | `upstream_unavailable`, 504 | Read the operation's state before retrying. See [Reliability](/docs/guides/platform/reliability). | | No notifications arriving | — | Check exact event names, a public `https` URL and the right project. | | Live key refused | `kyb_required` | Complete business verification. | | Retry refused: "retry shortly" | `idempotency_conflict` (409) | The first request is still running. Wait, then retry the same request. | | Retry refused: "needs reconciliation" | `idempotency_conflict` (409) | An earlier attempt may have reached the network. Do not resend; read [List transactions](/docs/api-reference/transactions/list-transactions). | | Passkey does not own the wallet | `passkey_credential_invalid` (409) | Use the passkey registered for this wallet; read [List passkeys](/docs/api-reference/passkeys/list-passkeys). | | Wallet under recovery | `forbidden` (409) | Swaps and transfers are blocked until the recovery completes or is cancelled. | | EVM signature refused over the challenge | `expectedChallenge` in the error | Send the base64url encoding of the `userOpHash` bytes, not the hash as text. | | Deposit seen but not credited | `deposit.detected` only | Wait for `deposit.confirmed` before crediting the customer. | | Too many requests | `rate_limit_exceeded` (429) | Back off and retry with increasing delays. | # Cross-Network Transfers (/docs/guides/swaps-and-transfers/cross-network-transfers) Transfer flow: Wallet on source network → Bridge, then Destination network or Back to the wallet. ## Routes [#routes] Move USD₮ from an EVM wallet on Ethereum, Arbitrum, Polygon, Optimism, Base or Plasma to a recipient on another of those networks, or on Tron. ## Flow [#flow] 1. [Quote bridge](/docs/api-reference/bridges/quote-bridge). `amountOut` is what arrives after the bridge fee. 2. [Prepare bridge](/docs/api-reference/bridges/prepare-bridge). It checks the balance, refusing with `insufficient_funds` when it cannot cover the amount and gas, and returns a `userOpHash`. 3. Have the customer sign the `userOpHash` with their passkey. 4. [Submit bridge](/docs/api-reference/bridges/submit-bridge) before `quoteExpiresAt`. ## Fees [#fees] - The bridge fee comes out of the USD₮ sent. - The customer pays source-network gas in USD₮, on top of the amount. - Transfers to Tron carry a flat destination cost of about 19 TRX, so small amounts lose a large share. ## Tracking [#tracking] Follow a transfer with [Get bridge](/docs/api-reference/bridges/get-bridge) or the `bridge.filled`, `bridge.refunded`, `bridge.failed` and `bridge.stalled` events. | Status | Meaning | | ----------- | -------------------------------------------------------- | | `prepared` | Built, not yet signed. | | `submitted` | Signed and sent. | | `deposited` | Deposited with the bridge, awaiting delivery. | | `filled` | Arrived at the recipient. | | `refunded` | Could not be delivered; the USD₮ returned to the wallet. | | `failed` | Did not leave the wallet. | | `stalled` | Tracking stopped before an outcome was seen. | The sending side also appears in [List transactions](/docs/api-reference/transactions/list-transactions) with `purpose: bridge`. ## Errors [#errors] | Code | Status | Cause | | ---------------------------- | ------------- | --------------------------------------------------------------------------------------------------------- | | `validation_failed` | 400 | Malformed body, a recipient not valid for the destination, or a body that differs from what was prepared. | | `validation_failed` | 409 | The quote expired before submit. | | `chain_unavailable` | 400, 409, 501 | The source network is not enabled for transfers (400, 501), or the wallet is not deployed on it (409). | | `insufficient_funds` | 400 | The wallet cannot cover the amount plus gas. | | `forbidden` | 409 | The wallet is under recovery. | | `passkey_credential_invalid` | 409 | The passkey does not own this wallet. | | `idempotency_conflict` | 409 | The transfer was already submitted. | | `not_found` | 404 | Nothing was prepared under this `userOpHash`, or no transfer has this id. | # Overview (/docs/guides/swaps-and-transfers) ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | ----------------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | | USD₮ and XAUT swaps | Swap either way on Ethereum, with a quote first. | non-treasury, treasury | | Maximum swap amount | The most a wallet can swap once gas is held back. | non-treasury, treasury | | One USD₮ balance | USDT and USD₮0 count as one balance. | non-treasury, treasury | | Cross-network transfers | Move USD₮ between Ethereum, Arbitrum, Polygon, Optimism, Base and Plasma, and to Tron. | non-treasury, treasury | | Fees up front | Quotes show the bridge fee and the amount that arrives. | non-treasury, treasury | | Arrival tracking | Follow each transfer to arrival, refund or failure. | non-treasury, treasury | | USDT on Optimism | Every USD₮ payment on Optimism arrives as standard USDT. | non-treasury, treasury | ## Guides [#guides] - [Swaps](/docs/guides/swaps-and-transfers/swaps): Swap between USD₮ and gold (XAUT). - [Cross-Network Transfers](/docs/guides/swaps-and-transfers/cross-network-transfers): Move USD₮ between networks, including to Tron. # Swaps (/docs/guides/swaps-and-transfers/swaps) ## What you can swap [#what-you-can-swap] USD₮ and XAUT, in either direction, within the same EVM wallet. Swaps run on Ethereum; other networks are refused with `chain_unavailable`. ## Flow [#flow] 1. [Quote swap](/docs/api-reference/swaps/quote-swap) for the price and gas. 2. [Get max swap amount](/docs/api-reference/swaps/get-max-swap-amount) for the most the wallet can swap after gas. `belowMinimum: true` means the balance is too low to swap. 3. [Prepare swap](/docs/api-reference/swaps/prepare-swap). It returns a `userOpHash`. 4. Have the customer sign the `userOpHash` with their passkey. 5. [Submit swap](/docs/api-reference/swaps/submit-swap) with the signature. ## Fees [#fees] The customer pays gas, in the token being sold. ## Retries [#retries] A swap is never retried automatically. If the response has no `transactionHash`, `pendingUserOp: true` means the outcome is still being resolved; read it from [List transactions](/docs/api-reference/transactions/list-transactions). ## Errors [#errors] | Code | Status | Cause | | ---------------------------- | -------- | ----------------------------------------------------------------------------------- | | `chain_unavailable` | 400, 409 | A network other than Ethereum (400), or the wallet is not yet deployed on it (409). | | `validation_failed` | 400 | Malformed body, or an amount below the minimum. | | `forbidden` | 409 | The wallet is under recovery, so swaps are blocked. | | `passkey_credential_invalid` | 409 | The passkey does not own this wallet. | | `upstream_unavailable` | 502 | No swap route could be found or reached; retry. | # Overview (/docs/guides/treasury) > **Releasing soon:** Proposals, pending approvals, allowance usage and the network-by-network threshold change are being released. The rest of Treasury is available now. ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | ---------------------------------- | ---------------------------------------------------------------------------- | ------------------------- | | Multiple signers | Add, list and remove signers, each with their own passkey. | treasury | | Multiple approvals | Require, for example, 2 of 3 signers to approve a payment. | treasury | | Approval threshold through the API | Set how many approvals a payment needs, up to the number of signers. | treasury | | Threshold on every network | One threshold, applied on every network once the current signers approve it. | treasury | | Proposals | Signers approve payments, signer changes and threshold changes over days. | treasury | | Pending approvals | List, read and cancel payments still collecting approvals. | treasury | | Business and customer treasuries | A treasury for your business, and one for each customer that needs it. | treasury | | Spending limits per signer | An amount per asset per rolling time window, across every network. | treasury | | Also enforced on chain | On treasuries that hold a Safe, the same cap is also enforced by the Safe. | treasury | | Allowance usage | See what each signer has spent and has left in the current window. | treasury | ## Guides [#guides] - [Signers and Approvals](/docs/guides/treasury/signers-and-approvals): Manage signers, require multiple approvals, and change the approval threshold. - [Spending Limits](/docs/guides/treasury/spending-limits): Set an amount per time window for each signer. # Proposals (releasing soon) (/docs/guides/treasury/proposals) > **Releasing soon:** Proposals are being released and are not yet available. ## What a proposal is [#what-a-proposal-is] A proposal is one transaction on a treasury's Safe that signers approve over time. Approvals do not expire with network fees, so signers can approve hours or days apart. | `kind` | Does | | ------------------ | -------------------------------------------------------- | | `payment` | Sends a token or the network's native coin. | | `owner_add` | Adds a signer. | | `owner_remove` | Removes a signer. | | `threshold_change` | Changes the signing threshold. | | `reject` | Cancels another proposal at the same place in the queue. | Proposal flow: Your server → Proposal → Sign over days → Sent, fee paid. ## Flow [#flow] 1. [Create proposal](/docs/api-reference/treasury/create-proposal) with the network, `kind` and `params`. It returns a `safeTxHash`, which is the proposal's id and what each signer signs. 2. Each signer signs the `safeTxHash` with their passkey: [Sign proposal](/docs/api-reference/treasury/sign-proposal), or [Sign proposals in batch](/docs/api-reference/treasury/sign-proposals-in-batch) for up to 16 at once. 3. Once enough signers have signed, the platform sends it and pays the network fee. A proposal needs the larger of the treasury's signing threshold and the Safe's own threshold, checked again when it is sent. > **Verify before signing:** A signer sees only the `safeTxHash` on their device. Your app must recompute it from the returned transaction fields and refuse to sign if it does not match. ## Timing and order [#timing-and-order] - A proposal collects signatures for 7 days by default. Set `ttlSeconds` (at least 3,600) for a shorter window. - Proposals on the same network execute in order. `queuePosition` shows how many must execute first, and `statusReason` says what a proposal is waiting for. ## Rejecting a proposal [#rejecting-a-proposal] [Reject proposal](/docs/api-reference/treasury/reject-proposal) creates a `reject` proposal at the same place in the queue. Once signers approve it, the original can never execute. A rejection needs as many signatures as any other proposal. ## Tracking [#tracking] [List proposals](/docs/api-reference/treasury/list-proposals) and [Get proposal](/docs/api-reference/treasury/get-proposal) show who has signed, the queue position and the transaction hash once sent. Finished proposals are kept for 30 days. ## Errors [#errors] | Code | Status | Cause | | ------------------- | ------ | --------------------------------------------------------------------------------- | | `not_found` | 404 | The proposal belongs to another business, project or customer treasury. | | `validation_failed` | 409 | The signer is not a current owner of the Safe on that network, or already signed. | | `validation_failed` | 400 | `ttlSeconds` is below 3,600 or above the platform window. | # Signers and Approvals (/docs/guides/treasury/signers-and-approvals) > **Releasing soon:** Proposals, the `lane` option, discarding pending threshold changes, and listing or cancelling pending approvals are being released. ## Treasury wallets [#treasury-wallets] Create a treasury wallet with `isTreasury: true`. Requests for your business's own treasury leave out the `x-customer-id` header; requests for a customer's treasury include it. ## Signers [#signers] Each signer is a passkey. | Action | Request | | ------------ | ----------------------------------------------------------------------------- | | Add a signer | [Add passkey](/docs/api-reference/passkeys/add-passkey) with `treasury: true` | | List signers | [List passkeys](/docs/api-reference/passkeys/list-passkeys) | | Remove one | [Remove passkey](/docs/api-reference/passkeys/remove-passkey) | Adding or removing a signer can also be approved over days as a [proposal](/docs/guides/treasury/proposals). ## Signing threshold [#signing-threshold] The signing threshold is how many signers must approve a payment. It applies on every network the treasury holds. - [Set signing threshold](/docs/api-reference/treasury/set-signing-threshold) changes it. - [Get signing threshold](/docs/api-reference/treasury/get-signing-threshold) returns it, the number of signers, and its state on each network. **Changes need the current signers.** A new threshold takes effect only once the current signers have approved it on every network. Until then payments use the current threshold, and the response shows the new value as `pendingThreshold`. | Choose `lane` | When | | --------------------------- | ----------------------------------------------------------------------------------- | | `userop` (default) | One person approves at once. Approvals must arrive within minutes. | | `proposal` (releasing soon) | Several people approve over days. See [Proposals](/docs/guides/treasury/proposals). | **Limits.** - The threshold cannot exceed the number of signers (`409`). - Customer wallets that are not treasuries can only use a threshold of 1. - A treasury payment on a network still behind the threshold is refused with `409` until that network's change is approved. [Discard pending threshold changes](/docs/api-reference/treasury/discard-pending-threshold-changes) drops changes that were never sent (releasing soon). Approval flow: Your server → Each signer approves → Threshold reached, then Recipient or Your server. ## Approving a payment [#approving-a-payment] 1. **Start the payment.** It waits for approvals. 2. **Each signer approves it with their passkey.** 3. **It is sent once the signing threshold is reached.** | Step | Request | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Start | [Prepare userOp](/docs/api-reference/transactions/prepare-userop) (EVM) · [Submit transaction](/docs/api-reference/transactions/submit-transaction) (other networks) | | Approve | [Submit userOp signature](/docs/api-reference/transactions/submit-userop-signature) · [Submit transaction](/docs/api-reference/transactions/submit-transaction), once per signer | | Track | [Get userOp status](/docs/api-reference/transactions/get-userop-status) · [List pending approvals](/docs/api-reference/treasury/list-pending-approvals) (releasing soon) | | Cancel | [Cancel pending approval](/docs/api-reference/treasury/cancel-pending-approval) (releasing soon) | For payments that several signers approve over days, use a [proposal](/docs/guides/treasury/proposals). ## Errors [#errors] | Code | Status | Cause | | ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | | `validation_failed` | 409 | The threshold is higher than the number of signers, or a treasury payment was made on a network still behind the threshold. | | `forbidden` | 403 | A threshold above 1 was set on a customer wallet that is not a treasury. | | `not_found` | 404 | No pending approval matches, or it has expired. | | `idempotency_conflict` | 409 | The pending approval already reached its threshold, so it cannot be cancelled. | # Spending Limits (/docs/guides/treasury/spending-limits) > **Releasing soon:** Allowance usage and delegate spends sent by the platform, with the `spend_limit_exceeded` error, are being released. ## Spend caps [#spend-caps] A spend cap limits how much one signer can send of one asset within a rolling time window. We record it and check it before every payment, on every network the wallet holds. | Field | Value | | ---------------- | ---------------------------------------------------------------------- | | `signerId` | The signer the cap applies to. | | `asset` | The asset, for example USD₮. | | `limitBaseUnits` | The cap, in the asset's base units. | | `windowSeconds` | The rolling window, in seconds. | | `treasury` | `true` for a treasury wallet (default), `false` for a customer wallet. | | Action | Request | | ------------ | ----------------------------------------------------------------- | | Set a cap | [Set spend cap](/docs/api-reference/treasury/set-spend-cap) | | List caps | [List spend caps](/docs/api-reference/treasury/list-spend-caps) | | Remove a cap | [Remove spend cap](/docs/api-reference/treasury/remove-spend-cap) | ## Also enforced on chain [#also-enforced-on-chain] On treasuries that hold a Safe, the same cap can also be written to the Safe, which then enforces it itself. Add `onChain` to [Set spend cap](/docs/api-reference/treasury/set-spend-cap): 1. The cap is recorded and enforced by us straight away. 2. The response returns a `userOpHash` for the on-chain allowance. 3. Signers approve it like any payment; see [Signers and Approvals](/docs/guides/treasury/signers-and-approvals). If the on-chain step does not start, the recorded cap stays in force and the response says why. To write allowances yourself instead, [Create delegate address](/docs/api-reference/treasury/create-delegate-address) returns the signer's `delegateAddress` and the `entityKey` that authorises spending against it, for use in [Prepare allowance setup](/docs/api-reference/treasury/prepare-allowance-setup). The signer must be a passkey registered on the treasury; any other signer is refused with `403`. ## Allowance usage (releasing soon) [#allowance-usage-releasing-soon] [List allowances](/docs/api-reference/treasury/list-allowances) returns each on-chain allowance with its cap, what was spent and what remains in the current window, and when the window resets. ## Spending within the allowance (releasing soon) [#spending-within-the-allowance-releasing-soon] A signer spends within their allowance with [Prepare delegate spend](/docs/api-reference/treasury/prepare-delegate-spend) and [Submit delegate spend](/docs/api-reference/treasury/submit-delegate-spend). The platform sends the payment and pays the network fee. A spend above what remains is refused with `409 spend_limit_exceeded`, which states the remaining amount and when the window resets. Nothing is sent. ## Removing a cap [#removing-a-cap] A cap linked to an on-chain allowance is removed only after the allowance is revoked; until then the request is refused with `409`. Revoke it with [Prepare allowance setup](/docs/api-reference/treasury/prepare-allowance-setup) and a cap of zero. ## Errors [#errors] | Code | Status | Cause | | ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- | | `spend_limit_exceeded` | 409 | The amount is above what remains of the allowance. `detail` states what remains and when the window resets. | | `forbidden` | 403 | The signer is not a passkey registered on this treasury, or the Safe is not this treasury's. | | `validation_failed` | 409 | Removing a cap whose on-chain allowance is still in force, or that may have one we cannot check. Revoke the allowance first. | # Creating Wallets (/docs/guides/wallets/creating-wallets) ## Networks and assets [#networks-and-assets] Each wallet type is a `moduleId`. | `moduleId` | Networks | Assets | | ------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `evm_erc4337` | Ethereum, Base, Arbitrum, Polygon, Celo, Optimism and Plasma | USD₮ on every network (also USD₮0 on Optimism); XAUT on Ethereum, Arbitrum, Polygon and Plasma | | `bitcoin` | Bitcoin | BTC | | `spark` | Spark | BTC | | `solana` | Solana | USD₮ | | `tron` | Tron | USD₮ | | `ton` | TON | USD₮ | On the EVM networks the wallet is a Safe smart account, with the same address on every chain. ## How wallets are secured [#how-wallets-are-secured] | Network | Who holds the key | What the customer's passkey does | | --------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------- | | EVM | The passkey is an owner of the wallet's Safe | Signs each payment itself; the Safe on chain enforces it. | | Bitcoin, Spark, Solana, Tron, TON | The wallet key is held in our signing enclave | Approves the exact transfer; the enclave signs only after verifying it. | On every network, an API key alone cannot move funds. [Get wallet control](/docs/api-reference/wallets/get-wallet-control) shows, for each EVM network, the Safe's owners, how many are passkeys, its signing threshold, and whether the customer's passkey is the only key. ## Passkeys [#passkeys] A wallet is secured by your customer's passkey. There is no seed phrase. Register a passkey on the customer's device and send its registration: | Field | Value | | --------------------- | ---------------------------------------------- | | `credentialId` | The passkey's credential ID. | | `credentialPublicKey` | The passkey's public key. | | `rpId` | Your domain, the relying party of the passkey. | | `origins` | The origins the passkey is used from. | ## One network or all [#one-network-or-all] | Request | Creates | | ------------------------------------------------------------------ | ----------------------------------------------------------- | | [Provision wallets](/docs/api-reference/wallets/provision-wallets) | A wallet on every network, all secured by the same passkey. | | [Create wallet](/docs/api-reference/wallets/create-wallet) | A wallet for one `moduleId`. | Both take the customer in the `x-customer-id` header. ## Non-treasury and treasury [#non-treasury-and-treasury] Wallets are non-treasury by default. Set `isTreasury: true` to create a treasury wallet, which supports multiple signers, approvals and spending limits. See [Treasury](/docs/guides/treasury). ## Adding another passkey [#adding-another-passkey] [Add passkey](/docs/api-reference/passkeys/add-passkey) registers a second passkey, for example a new device, approved with a passkey the customer already has. On EVM the new passkey also becomes an owner of the Safe. If that on-chain step does not start, the response says `onChain.status: "not_started"` with `retryable: true`. Finish it with [Retry passkey enrolment](/docs/api-reference/passkeys/retry-passkey-enrolment), naming the current owner (`existingOwner`) by the `passkeyPublicKey` from [List passkeys](/docs/api-reference/passkeys/list-passkeys). # Overview (/docs/guides/wallets) ## Key capabilities [#key-capabilities] | Capability | What it does | Applies to | | ---------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------- | | Passkey-secured wallets | No seed phrase; authorised with Face ID, fingerprint or device PIN. | non-treasury, treasury | | Six wallet types | EVM on seven networks, plus Bitcoin, Spark, Solana, Tron and TON. | non-treasury, treasury | | Every network in one request | Create a customer's wallets on all supported networks at once. | non-treasury, treasury | | Supported assets | USD₮ on every network, XAUT on four EVM networks, BTC on Bitcoin and Spark. | non-treasury, treasury | | Balances in one request | Every wallet of a customer, with its balances. | non-treasury, treasury | | Quotes before sending | See the network fee and the maximum sendable amount. | non-treasury, treasury | | Sending | Every payment approved with the customer's passkey. | non-treasury, treasury | | Self-send protection | Payments to the wallet's own address are refused. | non-treasury, treasury | | Deposits | Incoming funds detected and confirmed automatically, with an event at each step. | non-treasury, treasury | | Fee sponsorship | On EVM networks and Solana, Shiga can turn on fee sponsorship so your business pays the fees. | non-treasury, treasury | | Payment statuses | Submitted, Pending, Confirmed or Failed. | non-treasury, treasury | ## Guides [#guides] - [Creating Wallets](/docs/guides/wallets/creating-wallets): Non-treasury and treasury wallets, passkeys, and the networks and assets each supports. - [Sending Payments](/docs/guides/wallets/sending): Balances, quotes, network fees and the four payment statuses. - [Receiving Deposits](/docs/guides/wallets/receiving-deposits): How incoming funds are detected and credited. - [Network Fees](/docs/guides/wallets/network-fees): What each network charges, who pays, and fee sponsorship. # Network Fees (/docs/guides/wallets/network-fees) ## Fees by network [#fees-by-network] | Network | Fee charged in | Paid by | Can be sponsored | | ------- | ------------------------------- | ---------------------------- | ---------------- | | EVM | As quoted (`gasToken`) | The wallet, unless sponsored | Yes, except XAUT | | Bitcoin | BTC, on top of the amount | The wallet | No | | Spark | No fee between Spark wallets | — | — | | Solana | USD₮; the customer needs no SOL | The wallet, unless sponsored | Yes | | Tron | USD₮ | The wallet | No | | TON | USD₮ | The wallet | No | [Get fee quote](/docs/api-reference/quotes/get-fee-quote) returns the fee (`gas`, `gasToken`), what the customer pays (`userCost`), whether it is `sponsored`, and the most the wallet can send. The Bitcoin fee follows current network fee estimates. ## Fee sponsorship [#fee-sponsorship] With sponsorship on, your business pays the network fee and the customer pays nothing: the quote shows `sponsored: true` and `userCost` `"0"`. Sponsorship covers the EVM networks (except XAUT) and Solana. Shiga turns it on for your business, per network; contact us to enable it. ## Leaving Spark [#leaving-spark] Transfers between Spark wallets are free. Leaving Spark is not: a withdrawal to a Bitcoin address and a Lightning payment each carry a fee. Price them with [Quote Spark exit](/docs/api-reference/quotes/quote-spark-exit) before sending. A quote moves no funds and needs no passkey; the matching spend does. # Receiving Deposits (/docs/guides/wallets/receiving-deposits) Deposit flow: Any wallet → Wallet address → Deposit detected, then Wallet balance or Your server. ## Wallet address [#wallet-address] Share the wallet's address with the sender. It is returned when the wallet is created, and by [List wallets](/docs/api-reference/wallets/list-wallets). ## Detection [#detection] Incoming funds are detected automatically on every supported network. | Event | Sent when | | ------------------- | ----------------------------------- | | `deposit.detected` | The deposit is seen on the network. | | `deposit.confirmed` | The deposit is final. | See [Notifications](/docs/guides/notifications) to receive these events. ## Records [#records] Deposits appear in [List transactions](/docs/api-reference/transactions/list-transactions) alongside the wallet's payments, and in its balance from [List wallet balances](/docs/api-reference/balances/list-wallet-balances). # Sending Payments (/docs/guides/wallets/sending) - [Sample: send a payment](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart/tree/main/02-send-payment): Quote, prepare, passkey approval, submit and wait for confirmation, on EVM and other networks. Payment flow: Your server → Prepare payment → Passkey approves → Submit, then Recipient or Your server. ## 1. Check the balance [#1-check-the-balance] [List wallet balances](/docs/api-reference/balances/list-wallet-balances) returns every wallet of a customer with its balances. ## 2. Get a quote [#2-get-a-quote] [Get fee quote](/docs/api-reference/quotes/get-fee-quote) returns the network fee and the maximum amount the wallet can send. Do not send the sender's address: we find the wallet from `moduleId` and the `x-customer-id` header, and return the one we priced. Amounts are in the asset's display unit, for example `"12.5"` USD₮ or `"0.001"` BTC, never in base units. ## 3. Send [#3-send] Every payment is approved with the customer's passkey. The flow depends on the network. ### EVM (`evm_erc4337`) [#evm-evm_erc4337] 1. [Prepare userOp](/docs/api-reference/transactions/prepare-userop) with the chain, token, recipient and amount. It returns a `userOpHash`. 2. Have the customer sign the `userOpHash` with their passkey. 3. [Submit userOp signature](/docs/api-reference/transactions/submit-userop-signature). The payment is broadcast once the wallet's signing threshold is met; for most wallets that is one signature. #### Example: approving an EVM payment [#example-approving-an-evm-payment] In the browser, turn the `userOpHash` into the passkey challenge and ask the customer to approve: ```ts import { startAuthentication } from '@simplewebauthn/browser'; function hashToChallenge(userOpHash: string) { const bytes = userOpHash .slice(2) .match(/../g)! .map(h => parseInt(h, 16)); return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } const challenge = hashToChallenge(userOpHash); const assertion = await startAuthentication({ optionsJSON: { challenge, rpId: 'your-domain.com', allowCredentials: [{ id: credentialId, type: 'public-key' }], userVerification: 'required', }, }); await fetch('/payments/approve', { method: 'POST', body: JSON.stringify({ userOpHash, challenge, assertion }), }); ``` On your server, submit the signature: ```ts await fetch(`https://api.pilot.shiga.io/v1/userops/${userOpHash}/signatures`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.SHIGA_API_KEY}`, 'x-customer-id': customerId, 'Content-Type': 'application/json', }, body: JSON.stringify({ passkeyPublicKey: { x: passkey.x, y: passkey.y }, passkeyData: { credential: assertion, expectedChallenge: challenge, expectedOrigins: ['https://your-domain.com'], expectedRPID: 'your-domain.com', authenticator: { credentialId: passkey.credentialId, credentialPublicKey: passkey.credentialPublicKey, counter: 0, }, }, }), }); ``` `passkeyPublicKey` is the `{ x, y }` returned when the wallets were provisioned. `expectedChallenge` must be the base64url encoding of the `userOpHash` bytes, not the hash as text. ### Bitcoin, Spark, Solana, Tron and TON [#bitcoin-spark-solana-tron-and-ton] On these networks the wallet key is held in our signing enclave. The enclave signs only after it verifies the customer's passkey approval of the exact transfer. See [How wallets are secured](/docs/guides/wallets/creating-wallets#how-wallets-are-secured). 1. **Choose a `transactionId`.** Your own integer sequence number for this transfer. Keep it the same if you retry. 2. **Build the challenge** from the wallet's `entityKey`, its `moduleId` and the transfer details (below). 3. **Have the customer approve** with their passkey, using the challenge. 4. **Submit** with [Submit transaction](/docs/api-reference/transactions/submit-transaction). We sign and broadcast in one step. #### Transfer details (`txParams`) [#transfer-details-txparams] | Field | Value | | ------------------ | ---------------------------------------------------------------------------------------------------- | | `chain` | The network, for example `tron`. | | `amount` | In the asset's display unit, for example `"12.5"` USD₮ or `"0.0001"` BTC. | | `recipientAddress` | The destination, in the network's own address format. | | `transactionId` | Your integer sequence number from step 1. | | `networkConfig` | `{}`, or `{ "tokenMint": "..." }` on Solana and `{ "tokenRoot": "..." }` on TON to choose the token. | | `selectedPayment` | `"native"`. On Solana the platform sets it. It is not part of the challenge. | #### Building the challenge [#building-the-challenge] The challenge is the base64url SHA-256 of a canonical JSON object: keys sorted at every level, no spaces. | Key | Value | | ----------- | ------------------------------------------------------------------------------------------------------ | | `entityKey` | The wallet's `entityKey` from [List wallets](/docs/api-reference/wallets/list-wallets). | | `moduleId` | For example `tron`. | | `txParams` | Your `txParams` without `selectedPayment`, and with `networkConfig` cut to `tokenMint` or `tokenRoot`. | ```ts import { createHash } from 'node:crypto'; function canonical(value: unknown): unknown { if (Array.isArray(value)) return value.map(canonical); if (value && typeof value === 'object') { return Object.fromEntries( Object.keys(value) .sort() .map(key => [key, canonical((value as Record)[key])]), ); } return value; } function transferChallenge( entityKey: string, moduleId: string, txParams: Record, ) { const { selectedPayment, networkConfig, ...rest } = txParams; const asset = Object.fromEntries( Object.entries((networkConfig ?? {}) as Record).filter( ([key]) => ['tokenMint', 'tokenRoot'].includes(key), ), ); const intent = networkConfig === undefined ? rest : { ...rest, networkConfig: asset }; return createHash('sha256') .update( JSON.stringify(canonical({ entityKey, moduleId, txParams: intent })), ) .digest('base64url'); } ``` #### Submitting [#submitting] Send `moduleId`, `txParams` and `passkeyData`: the customer's assertion (`credential`), the challenge as `expectedChallenge`, and the passkey's `authenticator` details. The response returns the transaction with status `submitted`. On a treasury wallet that needs more than one approval, each signer submits the same transfer with their own passkey. Until enough have approved, the response carries `awaitingApprovals`; approvals lapse after 15 minutes. A payment to the wallet's own address is refused with `400 validation_failed`. ## Payment statuses and events [#payment-statuses-and-events] | Status | Event | Credit or settle? | | ----------- | ----------------------- | ----------------- | | `submitted` | `transaction.submitted` | No | | `pending` | `transaction.pending` | No | | `confirmed` | `transaction.success` | Yes | | `failed` | `transaction.failed` | No | `transaction.reconciled` is the signing side's report of the same confirmation as `transaction.success`; act on `transaction.success`. `transaction.summary_corrected` corrects a past transaction's details and does not change its status. Only `confirmed` means paid; see [Reliability](/docs/guides/platform/reliability). Read payments with [List transactions](/docs/api-reference/transactions/list-transactions), or receive each change as a notification. See [Notifications](/docs/guides/notifications). ## Errors [#errors] | Code | Status | Cause | | ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `validation_failed` | 400 | Malformed body, an address that fails its network's checksum, an asset the network does not list, or a payment to the wallet's own address. | | `insufficient_funds` | 400 | The wallet cannot cover the amount and fees. | | `wallet_not_found` | 404 | No single wallet matched `moduleId` and `x-customer-id`. Send `x-customer-id`. | | `chain_unavailable` | 409 | The wallet is not yet deployed on this network; retry once it is. | | `transfer_reverted` | 409 | The network would reject the transfer. | | `passkey_credential_invalid` | 409 | The passkey assertion did not verify. | | `paymaster_rejected` | 502 | The network fee could not be sponsored; retry. | | `upstream_unavailable` | 502, 503, 504 | A network service did not answer in time; retry. |