Wallets (end-user)
All routes mounted under /v1/wallets. Auth header convention:
X-Sigil-Publishable-Key: pk_live_xxxAuthorization: Bearer <end-user-jwt> # except where notedPOST /v1/wallets/auth/email-otp/start
Start the Sigil-hosted email-OTP login. Publishable key only — the user doesn’t have a JWT yet.
{ "email": "alice@example.com" }Returns 204 No Content regardless of whether the email is registered
(no enumeration leak). The email arrives at alice@example.com via
Resend.
POST /v1/wallets/auth/email-otp/verify
Exchange an OTP for a JWT. Publishable key only.
{ "email": "alice@example.com", "code": "449922" }Response:
{ "data": { "token": "eyJhbGciOi...", "user_identity_id": "00000000-...", "email": "alice@example.com" }, "error": null}Stash the token; every subsequent call uses it as Authorization: Bearer.
POST /v1/wallets/identify
Canary — confirms publishable key + JWT line up and a user_identity
exists.
{ "data": { "organization_id": "...", "user_identity_id": "...", "auth_provider": "sigil", "provider_user_id": "alice@example.com", "email": "alice@example.com" }, "error": null}GET /v1/wallets/me
Return the active wallet for the authenticated end user, or 404 if
they don’t have one yet (the iframe then prompts wallet creation).
POST /v1/wallets
Create a wallet. The iframe generates a secp256k1 (or ed25519) key pair, splits it via Shamir secret sharing, and sends the provider and recovery shares to the backend.
{ "curve": "secp256k1", "address": "0x...", "public_key": "04...", "chain_type": "evm", "provider_share": "base64", "provider_share_index": 2, "recovery_share": "base64", "recovery_share_index": 3}The curve field selects the cryptographic curve for the wallet:
secp256k1 for EVM chains, ed25519 for Solana. The address is the
wallet’s EOA address derived from the public key.
Sigil envelope-encrypts the provider share via Cloud KMS, dispatches the recovery share to your webhook (or stores it under a separate KMS purpose in Sigil-managed mode), and returns the wallet record.
Idempotent on (organization_id, user_identity_id, curve). A duplicate
returns 409 wallet_exists.
POST /v1/wallets/me/shares/provider
Return the cleartext provider share so the iframe can sign. The iframe combines it with its device share, reconstructs the key in memory, signs, and wipes.
{ "data": { "wallet_id": "...", "provider_share": "base64", "share_index": 2 }, "error": null }Raw EVM tx lane
Two endpoints power the SDK’s sendTransaction / useSendTransaction
primitive. They are only mounted when the backend has at least one EVM
RPC configured; they 404 otherwise.
POST /v1/wallets/evm/prepare-tx
Resolve nonce + gas + chainId for an EOA-signed transaction.
{ "chain": "arbitrum", "to": "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7", "data": "0xa9059cbb...", // optional 0x-prefixed call data "value": "0", // optional, decimal wei as string "gas_limit": "200000" // optional, override the estimate}Response (every field 0x-hex):
{ "data": { "chain": "arbitrum", "chain_id": "0xa4b1", "nonce": "0x3", "from": "0xeoaeoaeoa...", "to": "0x2df1c51e09aecf9cacb7bc98cb1742757f163df7", "data": "0xa9059cbb...", "value": "0x0", "gas_limit": "0x30d40", "max_fee_per_gas": "0x77359400", "max_priority_fee_per_gas": "0x3b9aca00" }, "error": null}from is the wallet’s EOA address.
Errors:
400 chain_unsupported— the chain handle isn’t wired.400 bad_to—toisn’t a 0x-prefixed 20-byte address.409 wallet_not_evm— the user’s wallet isn’t an EVM wallet.422 estimate_gas_reverted— the destination contract reverts during estimation. Passgas_limitexplicitly if you’ve verified the call is correct.502 rpc_failed— the RPC failed to answereth_getTransactionCountoreth_feeHistory. Retry.
POST /v1/wallets/evm/broadcast-tx
Forward a fully signed EIP-1559 envelope to the chain’s RPC.
{ "chain": "arbitrum", "raw_tx": "0x02f8..." // 0x02-prefixed RLP, signed}Response:
{ "data": { "chain": "arbitrum", "chain_id": "0xa4b1", "tx_hash": "0xabc..." }, "error": null}Errors:
400 chain_unsupported— chain handle not wired.400 unsupported_tx_type— first byte ofraw_txis not0x02. Only EIP-1559 envelopes are accepted from the iframe.400 bad_raw_tx— not 0x-prefixed hex.502 broadcast_failed— the RPC rejected the tx. The error message carries the underlying RPC reason (nonce too low,insufficient funds for gas * price + value, …).
Both endpoints audit-log per request (event types wallet.tx_prepared
and wallet.tx_broadcast).
Recovery sub-lane
Three endpoints — see Recovery.