IBExWalletAPI
AuthenticationSign in

Sign-in with passkey 1/2

Sign-in endpoint (v1.2) supporting 4 authentication methods.

rpId (tenant identification):

The rpId (Relying Party Identifier) is a domain string that scopes users, wallets, and credentials to your application. Each rpId is an isolated namespace — users created under one rpId are invisible to another. It also serves as the WebAuthn rpId, so the browser enforces that it matches your page origin.

Your rpId must be registered with IBEx. Pass it via:

  • Query parameter: ?rpId=yourdomain.com
  • Header: X-RpId: yourdomain.com
  • Automatically via Origin header (browser requests)

If missing or unregistered, returns 400 Unknown domain/rpId.

For local development, localhost is automatically recognized.

See the full [rpId guide](/docs/guides/integration/rpid) for production setup, WebAuthn constraints, and common errors.

---

Query parameters reference:

| Parameter | Type | Required for |

|-----------|------|-------------|

| wallet | string | All modes (passkeys default, kdf, email, 7702) |

| externalUserId | string | wallet=kdf and wallet=email |

| address | string | wallet=7702 only (0x-prefixed, 42 chars) |

| chainId | number | wallet=7702 (optional) |

| flow | string | ⚠️ Deprecated — use wallet=kdf instead |

---

### 1. Passkeys Safe wallet (wallet=passkeys, default)

What it means?

Authenticates a returning user via their registered WebAuthn passkey (biometric / security key). The server generates a challenge that the browser signs with the stored private key. This proves possession without ever transmitting the key.

Next step: POST the credential returned by navigator.credentials.get() to POST /sign-in.

Example: GET /v1.2/auth/sign-in

Important — PRF extension (passkeys mode):

The extensions.prf.eval.first value in the response is a base64url-encoded string (JSON cannot represent binary).

Before passing to navigator.credentials.get(), you must decode it to ArrayBuffer:

import { base64URLStringToBuffer } from '@simplewebauthn/browser';
const opts = response.credentialRequestOptions;
if (opts.extensions?.prf?.eval?.first) {
    opts.extensions.prf.eval.first = base64URLStringToBuffer(opts.extensions.prf.eval.first);
    if (opts.extensions.prf.eval.second)
        opts.extensions.prf.eval.second = base64URLStringToBuffer(opts.extensions.prf.eval.second);
}

Without this conversion, the browser will throw: Failed to read the 'prf' property: The provided value is not of type '(ArrayBuffer or ArrayBufferView)'.

---

### 2. KDF Safe wallet (wallet=kdf)

What it means?

Authenticates a user whose signer was derived from a PIN/password. The server returns the same salt and KDF parameters used at sign-up so the client can re-derive the exact same key and sign a fresh challenge.

Next step: Re-derive the key from PIN + salt, sign the canonical message, and call POST /sign-in with the signature proof.

Example: GET /v1.2/auth/sign-in?wallet=kdf&externalUserId=<uuid>

---

### 3. Email Safe wallet (wallet=email)

What it means?

Authenticates a user who signed up via the email-token flow. The server sends a new email OTP (30s) and returns a challenge. After verifying the OTP via POST /email/recover, the client retrieves its encrypted backup and re-derives the key to sign the challenge.

Next step: Verify OTP via POST /email/recover, then call POST /sign-in with the signature proof.

Example: GET /v1.2/auth/sign-in?wallet=email&externalUserId=<uuid>

---

### 4. EOA + EIP-7702 (wallet=7702)

What it means?

Authenticates a user who registered via their EOA (external wallet). The server looks up the existing EOA_7702 signer for the given address and domain, then generates a SIWE-like challenge. The user signs it with their wallet (MetaMask, WalletConnect, etc.) to prove ownership.

Next step: Sign challenge with the wallet and call POST /sign-in with { wallet: "7702", address, signature, nonce }.

Example: GET /v1.2/auth/sign-in?wallet=7702&address=0xCa33...

---

### Response enrichment (POST sign-in, step 2)

When completing sign-in via POST /sign-in (step 2), you can include optional boolean flags in the request body to receive additional data alongside the JWT tokens:

| Flag | Type | What it adds |

|------|------|-------------|

| includeBalance | boolean | balance — all monitored token balances for the wallet on the requested chain, including tokens with zero balance. This effectively gives you the full list of the user's watched tokens/addresses. |

| includeTransactions | boolean | transactions — paginated transaction history for the wallet |

| includeUserdata | boolean | userdata — same aggregated payload format as GET /v1.2/users/me |

This avoids extra API calls after sign-in — you get JWT tokens + enriched data in a single response. Use chainId in the POST body to select which chain to query (defaults to the platform's default chain).

Example POST body (passkeys mode with enrichment):

{
  "credential": { "id": "...", "rawId": "...", "type": "public-key", "response": { ... } },
  "chainId": 421614,
  "includeBalance": true,
  "includeUserdata": true
}

See POST /sign-in for full details and response examples.

---

### Automatic multi-chain Safe expansion (passkey & KDF modes)

When a user signs in with a passkey or KDF wallet (walletMode=SAFE_4337), the API automatically checks whether their Safe wallet exists on all active chains configured in the platform.

How it works:

  • After sign-in, the API compares the signer's existing Safes against all chains marked isActive=true and isSafeWallet=true.
  • For any missing chain, the deterministic Safe address is computed and persisted — no on-chain deployment needed (counterfactual).
  • The new Safe addresses are registered in BCReader for balance/transaction monitoring.
  • This runs asynchronously (non-blocking) — the sign-in response is returned immediately.

For integrators:

  • After sign-in, call GET /v1.2/users/me/address to retrieve the full list of 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.
GET
/v1.2/auth/sign-in

Query Parameters

walletstring

Authentication mode: passkeys (default), kdf, email, or 7702

Value in: "passkeys" | "kdf" | "email" | "7702"
externalUserIdstring

[KDF / Email mode] User identifier from a previous sign-up

flowDeprecatedstring

[Deprecated] Use wallet=kdf instead

Value in: "pin-kdf"
addressstring

[7702 mode only] EOA address (0x-prefixed, 42 chars)

chainIdnumber

[7702 mode only] Target blockchain chain ID

curl -X GET "https://passkeys-testnet.ibex.fi/v1.2/auth/sign-in?wallet=passkeys&externalUserId=%3Cstring%3E&flow=pin-kdf&address=%3Cstring%3E&chainId=0"

Sign-in challenge (format depends on wallet mode)

{
  "summary": "wallet=passkeys (default)",
  "value": {
    "credentialRequestOptions": {
      "rpId": "passkeys-prat1.ibex.fi",
      "challenge": "base64url-challenge",
      "timeout": 60000,
      "allowCredentials": [
        {
          "id": "base64url-cred-id",
          "type": "public-key"
        }
      ],
      "userVerification": "required"
    }
  }
}