IBExWalletAPI
AuthenticationSign in

Sign-in with passkey 2/2

Complete sign-in (v1.2) — 3 modes.

Request body:

  • credential (object, required for passkeys mode): WebAuthn credential from navigator.credentials.get()
  • wallet (string, optional): passkeys (default), kdf, email
  • externalUserId (string, required for wallet=kdf and wallet=email)
  • chainId (number, optional): Target blockchain ID
  • intent (string, optional): set to safe_provision to receive a short-lived safe_provision_token for POST /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 on chainId)
  • deploySaltNonce (string, optional): select Safe by salt slot on chainId ("" = primary / default salt)
  • includeBalance, includeTransactions, includeUserdata (boolean, optional): Include data in response
  • asyncData (boolean, optional): when true and include flags are set, returns quickly with dataRequestId; poll GET /v1.2/auth/sign-in/data/:dataRequestId for 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.
POST
/v1.2/auth/sign-in

Request Body

application/jsonOptional
bodyobject | object | object | object

Query Parameters

intentstring

Optional shortcut for passkeys mode. safe_provision enables issuance of safe_provision_token in response.

curl -X POST "https://passkeys-testnet.ibex.fi/v1.2/auth/sign-in?intent=%3Cstring%3E" \
  -H "Content-Type: application/json" \
  -d '{
    "credential": {
      "id": "base64url-credential-id",
      "rawId": "base64url-credential-raw-id",
      "type": "public-key",
      "authenticatorAttachment": "platform",
      "clientExtensionResults": {},
      "response": {
        "authenticatorData": "base64url-authenticator-data",
        "clientDataJSON": "base64url-client-data-json",
        "signature": "base64url-signature",
        "userHandle": "base64url-user-handle"
      }
    }
  }'

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 to safe_provision
  • safeAddress: attach the session to a specific Safe (owned by signer on selected chain)
  • deploySaltNonce: select Safe by slot on selected chain ("" = primary)

On this page