Skip to content

Wallets (end-user)

All routes mounted under /v1/wallets. Auth header convention:

X-Sigil-Publishable-Key: pk_live_xxx
Authorization: Bearer <end-user-jwt> # except where noted

POST /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_toto isn’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. Pass gas_limit explicitly if you’ve verified the call is correct.
  • 502 rpc_failed — the RPC failed to answer eth_getTransactionCount or eth_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 of raw_tx is not 0x02. 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.