Sign-in with passkey 2/2
Complete sign-in (v1.2) — 3 modes.
Request body:
credential(object, required for passkeys mode): WebAuthn credential fromnavigator.credentials.get()wallet(string, optional):passkeys(default),kdf,emailexternalUserId(string, required forwallet=kdfandwallet=email)chainId(number, optional): Target blockchain IDintent(string, optional): set tosafe_provisionto receive a short-livedsafe_provision_tokenforPOST /v1.2/auth/sign-in/safe-provision(PASSKEY + SAFE_4337 only)safeAddress(string, optional): pin the session Login to this Safe (must belong to signer onchainId)deploySaltNonce(string, optional): select Safe by salt slot onchainId(""= primary / default salt)includeBalance,includeTransactions,includeUserdata(boolean, optional): Include data in responseasyncData(boolean, optional): when true and include flags are set, returns quickly withdataRequestId; pollGET /v1.2/auth/sign-in/data/:dataRequestIdfor heavy data
Exemples (4 modes)
1. Mode passkeys (défaut)
POST body:
{
"credential": { "id": "base64url...", "rawId": "base64url...", "type": "public-key", "response": { "authenticatorData": "base64url...", "clientDataJSON": "base64url...", "signature": "base64url...", "userHandle": "base64url..." } },
"chainId": 421614,
"includeBalance": true,
"includeUserdata": true
}Response 200:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"issuer": "foo.domain",
"audience": "foo.domain",
"subject": "externalUserId-uuid",
"roles": ["USER"],
"authMethod": "PASSKEY",
"hasPasskey": true,
"chainId": 421614,
"safeAddress": {
"421614": "0xd4c819A7f5A1dC2E55D89513E9a0B38fadd622E7",
"8453": "0x7C4755101468f4fD8b8E739165926D81851Ccc0F",
"100": "0x1923FD4e893fd7bb5a77cd4a9804D1bB5f20e33a"
},
"eoaAddress": "0xa6D9e8Ed50F21391047da545C4Eb3Fd0d258560b",
"eoaAddresses": [
{ "type": "EVM", "address": "0xa6D9e8Ed50F21391047da545C4Eb3Fd0d258560b" },
{ "type": "SOLANA", "address": "5tUYndEfq8hFwsQTtcwg9Zugu4hPf6sw6sggrXkSpWn3" },
{ "type": "BITCOIN_P2WPKH", "address": "bc1q88hhscjmxt8df5aaye8svjxut7lzlmrjh5preg" },
{ "type": "BITCOIN_P2TR", "address": "bc1pc3dwu5j7q6vv7v5f4majx262pe9hu0cgvsgjfl0khpu2hu5v8erqxeyvl3" },
{ "type": "BITCOIN_P2WPKH_TESTNET", "address": "tb1q88hhscjmxt8df5aaye8svjxut7lzlmrjaj6szm" },
{ "type": "BITCOIN_P2TR_TESTNET", "address": "tb1pc3dwu5j7q6vv7v5f4majx262pe9hu0cgvsgjfl0khpu2hu5v8erq33jr97" }
],
"prfCapable": false,
"keyName": "foo.domain abc123",
"keyDisplayName": "foo.domain abc123",
"walletMode": "SAFE_4337",
"services": [
{ "isPrivateLending": true },
{ "isTransfer": true },
{ "isSwap": true },
{ "isLending": true }
]
}> Note: \eoaAddresses\ lists all derived addresses from the passkey. Available types depend on the key derivation at sign-up. The list can be filtered server-side via the \WALLET_TYPE_SIGNIN\ env (CSV of allowed types).
2. Mode wallet=kdf
POST body:
{
"wallet": "kdf",
"externalUserId": "externalUserId-uuid",
"publicKey": "0xYourDerivedAddress",
"signature": "0xSignedCanonicalMessage",
"challenge": "base64-from-get",
"challengeExpiresAt": "2025-12-12T12:59:00.000Z",
"nonce": "base64-random",
"timestamp": 1765544000,
"serverSignature": "base64-from-get",
"serverKeyId": "pin-kdf-hmac-v1",
"saltVersion": 1,
"kdfParamsVersion": 1
}Response 200:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"issuer": "foo.domain",
"audience": "foo.domain",
"subject": "externalUserId-uuid",
"roles": ["USER"],
"authMethod": "PIN_KDF",
"hasPasskey": false,
"wallet": "kdf",
"flow": "pin-kdf",
"publicKey": "0xyourderivedaddress",
"signerVersion": 1,
"chainId": 421614,
"safeAddress": { "421614": "0xd4c819A7f5A1dC2E55D89513E9a0B38fadd622E7" },
"eoaAddress": "0xYourDerivedAddress",
"eoaAddresses": [{ "type": "EVM", "address": "0xYourDerivedAddress" }],
"walletMode": "SAFE_4337",
"services": [{ "isPrivateLending": true }, { "isTransfer": true }, { "isSwap": true }, { "isLending": true }]
}---
3. Mode wallet=email
POST body:
{
"wallet": "email",
"externalUserId": "externalUserId-uuid",
"publicKey": "0xYourDerivedAddress",
"signature": "0xSignedCanonicalMessage",
"challenge": "base64-from-get",
"challengeExpiresAt": "2025-12-12T12:59:00.000Z",
"nonce": "base64-random",
"timestamp": 1765544000,
"serverSignature": "base64-from-get",
"serverKeyId": "email-token-hmac-v1"
}Response 200:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"issuer": "foo.domain",
"audience": "foo.domain",
"subject": "externalUserId-uuid",
"roles": ["USER"],
"authMethod": "EMAIL_TOKEN",
"hasPasskey": false,
"wallet": "email",
"publicKey": "0xyourderivedaddress",
"signerVersion": 1,
"chainId": 421614,
"safeAddress": { "421614": "0xd4c819A7f5A1dC2E55D89513E9a0B38fadd622E7" },
"eoaAddress": "0xYourDerivedAddress",
"eoaAddresses": [{ "type": "EVM", "address": "0xYourDerivedAddress" }],
"walletMode": "SAFE_4337",
"services": [{ "isPrivateLending": true }, { "isTransfer": true }, { "isSwap": true }, { "isLending": true }]
}---
4. Mode wallet=7702 (EOA + EIP-7702)
POST body:
{
"wallet": "7702",
"address": "0xCa3384350beC79971C13a1710FC4868eD84f497F",
"signature": "0xSignedSIWEChallenge...",
"nonce": "abc123def456789"
}Response 200:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"issuer": "foo.domain",
"audience": "foo.domain",
"subject": "externalUserId-uuid",
"roles": ["USER"],
"authMethod": "EOA_7702",
"hasPasskey": false,
"wallet": "7702",
"walletMode": "EOA_7702",
"eoaAddress": "0xCa3384350beC79971C13a1710FC4868eD84f497F",
"safeAddress": { "11155111": "0xCa3384350beC79971C13a1710FC4868eD84f497F" },
"delegations": [{ "status": "ACTIVE", "delegateAddress": "0xSafeEIP7702Proxy", "chainId": 11155111, "txHash": "0x...", "eoaAddress": "0xCa33..." }]
}---
### Base response fields reference
All sign-in modes return these base JWT fields:
| Field | Type | Description |
|-------|------|-------------|
| \access_token\ | string | JWT access token |
| \refresh_token\ | string | JWT refresh token |
| \token_type\ | string | Always "Bearer" |
| \expires_in\ | number | Token TTL in seconds |
| \issuer\ | string | rpId (domain) |
| \audience\ | string | rpId (domain) |
| \subject\ | string | externalUserId |
| \roles\ | string[] | User roles (["USER"]) |
| \authMethod\ | string | PASSKEY, PIN_KDF, EMAIL_TOKEN, or EOA_7702 |
| \hasPasskey\ | boolean | Whether user has a passkey signer |
### Enrichment fields (added automatically)
| Field | Type | Description |
|-------|------|-------------|
| \chainId\ | number | Active blockchain ID for this session |
| \safeAddress\ | object | Map of chainId to Safe wallet address (all chains) |
| \eoaAddress\ | string | Primary EVM EOA address (derived from passkey/key) |
| \eoaAddresses\ | array | All derived addresses: [{ type, address }]. Types: EVM, SOLANA, BITCOIN_P2WPKH, BITCOIN_P2TR, BITCOIN_P2WPKH_TESTNET, BITCOIN_P2TR_TESTNET, COSMOS, POLKADOT, TEZOS_TZ1, TEZOS_TZ2, TEZOS_TZ3, NEAR, STELLAR, CARDANO |
| \prfCapable\ | boolean | Whether the passkey supports PRF extension |
| \keyName\ | string | Passkey human-friendly name |
| \keyDisplayName\ | string | Passkey display name |
| \walletMode\ | string | SAFE_4337 or EOA_7702 |
| \services\ | array | Domain feature flags: isPrivateLending, isTransfer, isSwap, isLending |
---
### Optional enrichment flags (POST body)
Enriched response examples (what each flag adds):
| Flag | Type | What it adds |
|------|------|-------------|
| \includeBalance\ | boolean | \balance\ — all monitored token balances for the wallet on the requested chain, including tokens with zero balance |
| \includeTransactions\ | boolean | \transactions\ — paginated transaction history for the wallet |
| \includeUserdata\ | boolean | \userdata\ — same aggregated payload format as \GET /v1.2/users/me\ |
Use \chainId\ in the POST body to select which chain to query (defaults to the platform's default chain).
1) When \includeBalance=true\ → adds \balance\
Returns all monitored token balances for the user's Safe wallet on the requested chain, including tokens with zero balance.
{
"...base + enrichment fields...": "...",
"balance": {
"0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430": { "address": "0x420c...", "balance": "123.45" },
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd": { "address": "0xabcd...", "balance": "0" }
}
}2) When \includeTransactions=true\ → adds \transactions\
{
"...base + enrichment fields...": "...",
"transactions": {
"total": 2, "page": 1, "limit": 2, "totalPages": 1,
"data": [{ "hash": "0x...", "timestamp": 1700000000 }, { "hash": "0x...", "timestamp": 1700000100 }]
}
}3) When \includeUserdata=true\ → adds \userdata\
Returns the same aggregator payload as \GET /v1.2/users/me\ (sections \{ status, data }\ per sub-endpoint).
{
"...base + enrichment fields...": "...",
"userdata": {
"addresses": { "status": 200, "data": { "count": 1, "wallets": [] } },
"balances": { "status": 200, "data": { "type": "crypto", "identifier": "0x..." } },
"ibans": { "status": 200, "data": { "count": 0, "ibans": [] } },
"lending": { "status": 200, "data": [] },
"pools": { "status": 200, "data": { "type": "crypto", "data": [] } },
"signers": { "status": 200, "data": { "count": 1, "signers": [] } },
"transactions": { "status": 200, "data": { "type": "crypto", "total": 0, "data": [] } },
"kycStatus": { "status": 200, "data": { "kycLevel": "0", "verified": false } },
"addressbook": { "status": 200, "data": { "success": true, "data": [] } }
}
}---
Automatic multi-chain Safe expansion:
After a successful sign-in (passkey or KDF mode, \walletMode=SAFE_4337\), the API automatically expands the user's Safe wallet to all active chains. This is non-blocking — the JWT response is returned immediately while expansion runs in the background.
- Call \
GET /v1.2/users/me/address\after sign-in to see all wallets across all chains. - If the expansion is still in progress, retry after 2–3 seconds.
- EOA/EIP-7702 wallets are excluded from this mechanism.
Request Body
application/jsonOptionalbodyobject | object | object | objectQuery Parameters
intentstringOptional shortcut for passkeys mode. safe_provision enables issuance of safe_provision_token in response.
Default Response
Additional Safe flow (intent=safe_provision)
Use POST /v1.2/auth/sign-in with intent=safe_provision to receive a short-lived safe_provision_token, then call:
POST /v1.2/auth/sign-in/safe-provision
Optional fields on POST /v1.2/auth/sign-in for passkeys mode:
intent: set tosafe_provisionsafeAddress: attach the session to a specific Safe (owned by signer on selected chain)deploySaltNonce: select Safe by slot on selected chain (""= primary)