> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Authenticate embedded wallets with Turnkey, Portal, or Privy, and call the Rain API with a Client Session Token: wallet first, then the Rain API.

Two independent things authenticate in an embedded wallet integration. Handle the wallet first: authenticate it and read its address before you create the user and mint a session in the Rain API, because creating the user's application requires the wallet address.

1. Wallet authentication (user → wallet provider): the user proves control of their wallet directly to the provider (Turnkey, Portal, or Privy). This happens outside Rain, through the vendor's own SDK, and produces the authenticated material you hand to the Rain SDK. It keeps the wallet non-custodial: Rain never sees key material.
2. Rain session (app → Rain API), optional: if your app needs to call user-scoped Rain endpoints directly instead of proxying through your backend, a short-lived **Client Session Token (CST)** authenticates it. The SDK's [built-in Rain API client](#built-in-rain-api-client) mints and caches CSTs for you, or your backend can mint one and forward it. Otherwise, keep Rain calls on your backend with your API key. See [Backend API key vs. Client Session Token](/sdks/embedded-wallets-common-flows#backend-api-key-vs-client-session-token).

The full flow looks like this:

<div className="wf-diagram">
  <div className="diagram-shell">
    <svg id="auth-flow" role="img" aria-label="The authentication flow: the app authenticates the wallet with its provider outside Rain, sends the wallet address to your backend, which creates the user and mints a Client Session Token via the Rain API, forwards it to the app, and the app then makes user-scoped calls to the Rain API directly." viewBox="0 0 1080 824" width="1080" height="824" style={{width: "100%", height: "auto"}}><defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" className="marker-fill-default" /></marker></defs><line x1="150" y1="70" x2="150" y2="812" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><line x1="400" y1="70" x2="400" y2="812" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><line x1="650" y1="70" x2="650" y2="812" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><line x1="900" y1="70" x2="900" y2="812" className="lane-line" stroke-width="1.5" stroke-dasharray="2 6" /><path d="M 150 168 C 150 184, 400 184, 400 200" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 400 268 C 400 284, 150 284, 150 300" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 150 368 C 150 384, 650 384, 650 400" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 650 468 C 650 484, 900 484, 900 500" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 900 568 C 900 584, 650 584, 650 600" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 650 668 C 650 684, 150 684, 150 700" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><foreignObject x="56" y="16" width="188" height="54"><div className="lane-head"><span className="ico">📱</span><span className="nm">Mobile App</span></div></foreignObject><foreignObject x="306" y="16" width="188" height="54"><div className="lane-head"><span className="ico">🔑</span><span className="nm">Wallet Provider</span></div></foreignObject><foreignObject x="556" y="16" width="188" height="54"><div className="lane-head"><span className="ico">🖥️</span><span className="nm">Your Backend</span></div></foreignObject><foreignObject x="806" y="16" width="188" height="54"><div className="lane-head rain"><span className="ico">🌧️</span><span className="nm">Rain API</span></div></foreignObject><foreignObject x="56" y="100" width="188" height="68"><div className="card action"><span className="bn">1</span><span className="ct"><span className="tag">App → Provider</span><span className="lab">Authenticate (passkey, OTP, or OAuth)</span></span></div></foreignObject><foreignObject x="306" y="200" width="188" height="68"><div className="card action"><span className="bn">2</span><span className="ct"><span className="tag">Provider → App</span><span className="lab">Authenticated session + wallet address</span></span></div></foreignObject><foreignObject x="56" y="300" width="188" height="68"><div className="card action"><span className="bn">3</span><span className="ct"><span className="tag">App → Backend</span><span className="lab">Send wallet address</span></span></div></foreignObject><foreignObject x="556" y="400" width="188" height="68"><div className="card action"><span className="bn">4</span><span className="ct"><span className="tag">Backend → Rain API</span><span className="lab mono">Create user + mint session (Api-Key)</span></span></div></foreignObject><foreignObject x="806" y="500" width="188" height="68"><div className="card action"><span className="bn">5</span><span className="ct"><span className="tag">Rain API → Backend</span><span className="lab">Client Session Token (CST)</span></span></div></foreignObject><foreignObject x="556" y="600" width="188" height="68"><div className="card action"><span className="bn">6</span><span className="ct"><span className="tag">Backend → App</span><span className="lab">Forward CST</span></span></div></foreignObject><foreignObject x="56" y="700" width="188" height="68"><div className="card action"><span className="bn">7</span><span className="ct"><span className="tag">App → Rain API</span><span className="lab mono">Bearer cst\_…</span></span></div></foreignObject></svg>
  </div>
</div>

<Note>
  Authentication happens entirely outside Rain, through the provider's own SDK. Your wallet provider secures the key material — for example, in a secure enclave (TEE) or as an encrypted key share on the user's device. Rain never sees the key material and stores only wallet addresses.
</Note>

The steps above, in text:

| # | From → To          | Message                                             |
| - | ------------------ | --------------------------------------------------- |
| 1 | App → Provider     | Authenticate (passkey, OTP, or OAuth), outside Rain |
| 2 | Provider → App     | Authenticated session + wallet address              |
| 3 | App → Backend      | Send wallet address                                 |
| 4 | Backend → Rain API | Create user + mint session token (`Api-Key`)        |
| 5 | Rain API → Backend | Client Session Token (CST)                          |
| 6 | Backend → App      | Forward CST                                         |
| 7 | App → Rain API     | User-scoped calls (`Authorization: Bearer cst_…`)   |

<Info>
  The two headers are different: your primary API key goes in the `Api-Key` header (backend only); a CST is a `cst_`-prefixed bearer token the app sends as `Authorization: Bearer <cst>`. The Client SDK sets the bearer header for you. For what the app can call with a CST, see [Common flows](/sdks/embedded-wallets-common-flows#what-runs-where).
</Info>

## Wallet providers

Provider authentication happens in your app, through the vendor's own SDK. Each Rain adapter receives the authenticated material and resolves it into a wallet client.

The Turnkey and Privy adapters probe the wallet when you resolve the client, so an unusable session fails fast at `rain.provider(...)`. Portal validates its session lazily, on the first operation.

Here's how they compare:

| Provider              | Module (iOS / Android)               | Config                                   | Capabilities                |
| --------------------- | ------------------------------------ | ---------------------------------------- | --------------------------- |
| **Turnkey** (managed) | `RainCore` / `rain-core-android`     | `TurnkeyConfig(turnkey, walletAddress?)` | Multi-chain, biometric gate |
| **Portal**            | `RainPortal` / `rain-portal-android` | `PortalConfig(sessionToken, chainId?)`   | Export, recovery            |
| **Privy**             | `RainPrivy` / `rain-privy-android`   | `PrivyConfig(privy, walletAddress?)`     | Export, recovery            |

### Turnkey (managed program)

Under the managed Turnkey program, Rain provisions and configures Turnkey for you and gives you an `organizationId` and `authProxyConfigId`. Authenticate with Turnkey's SDK, using a passkey (recommended: seamless and phishing-resistant) or an email or SMS one-time passcode. Then hand the authenticated `TurnkeyContext` to the adapter. The snippets below call Turnkey's own SDK, not Rain's:

<CodeGroup>
  ```kotlin Android theme={null}
  import com.turnkey.core.TurnkeyContext
  import com.turnkey.core.models.OtpType
  import com.turnkey.core.models.TurnkeyConfig

  // 1. Initialize Turnkey with the org + auth-proxy config Rain gives you
  TurnkeyContext.initSuspend(app, TurnkeyConfig(organizationId, authProxyConfigId))
  TurnkeyContext.awaitReady()

  // 2. Send an email one-time passcode
  val otp = TurnkeyContext.initOtp(otpType = OtpType.OTP_TYPE_EMAIL, contact = email)

  // 3. Verify the code: creates (first time) or resumes the user's Turnkey session
  TurnkeyContext.loginOrSignUpWithOtp(
      otpId = otp.otpId,
      otpCode = userEnteredCode,
      otpEncryptionTargetBundle = otp.otpEncryptionTargetBundle,
      contact = email,
      otpType = OtpType.OTP_TYPE_EMAIL,
  )
  // TurnkeyContext is now authenticated. Hand it to Rain in "Set up a wallet".
  ```

  ```swift iOS theme={null}
  // Authenticate with Turnkey's Swift SDK (passkey or OTP), then hand the
  // authenticated TurnkeyContext to Rain when you build the SDK.
  //   Proxy middleware: https://docs.turnkey.com/sdks/swift/proxy-middleware
  //   Passkeys:         https://docs.turnkey.com/sdks/swift/register-passkey
  ```
</CodeGroup>

Then register the adapter when you [build the SDK](/sdks/embedded-wallets-setup): `TurnkeyProvider(TurnkeyConfig(turnkey: turnkeyContext))`. Omit `walletAddress` to use the first Ethereum account from the Turnkey context, or pass an explicit address to override it. Don't mutate the context (re-auth, logout, wallet switch) while Rain calls are in flight. Rebuild the SDK after such changes.

If your app pins `bcprov-jdk18on`, align it with `bcprov-jdk15to18`: on Android, the core module forces that version to resolve a conflict between Turnkey's and web3j's crypto dependencies.

### Portal

Supply a valid [Portal](https://www.portalhq.io/) session token; the adapter constructs and initializes Portal itself from the token and the SDK's RPC endpoints (mapped to CAIP-2 `eip155:<chainId>` form). On iOS, backup storage (iCloud, Keychain, password) is configured for you.

<CodeGroup>
  ```swift iOS theme={null}
  import RainPortal

  .register(PortalProvider(PortalConfig(sessionToken: portalSessionToken)))
  ```

  ```kotlin Android theme={null}
  import com.rain.sdk.portal.PortalConfig
  import com.rain.sdk.portal.PortalProvider

  .register(PortalProvider(PortalConfig(sessionToken = portalSessionToken)))
  ```
</CodeGroup>

Portal doesn't validate the session at resolve time: on iOS an empty token throws `RainSDKError.unauthorized` at resolution, and an invalid or expired token surfaces as [`RAIN_501`](/sdks/embedded-wallets-error-reference) on the first operation. On Android, `chainId` optionally sets the default chain (falls back to `RainChain.AVALANCHE_MAINNET` when configured, else the first configured chain).

To reach Portal-only features (backup, recovery) on iOS without importing PortalSwift into your Rain code, use the creation hook:

```swift theme={null}
PortalProvider(PortalConfig(sessionToken: token), onPortalCreated: { portal in
    self.portal = portal   // call portal.backupWallet / recoverWallet directly
})
```

### Privy

Your app owns [Privy](https://www.privy.io/) authentication end to end. Initialize the Privy SDK (on Android, in `Application.onCreate()`), log the user in (SMS, email, OAuth, or passkey), and make sure an embedded Ethereum wallet exists. Then hand the authenticated `Privy` singleton to the adapter.

<CodeGroup>
  ```swift iOS theme={null}
  import RainPrivy

  guard let user = privy.user else { /* log in with Privy first */ }
  if user.embeddedEthereumWallets.isEmpty {
      try await user.createEthereumWallet()
  }

  .register(PrivyProvider(PrivyConfig(privy: privy)))
  ```

  ```kotlin Android theme={null}
  import com.rain.sdk.privy.PrivyConfig
  import com.rain.sdk.privy.PrivyProvider

  val user = privy.getUser() ?: error("log in with Privy first")
  if (user.embeddedEthereumWallets.isEmpty()) user.createEthereumWallet()

  .register(PrivyProvider(PrivyConfig(privy = privy)))
  ```
</CodeGroup>

Omit `walletAddress` to use the user's first embedded Ethereum wallet. Signing and broadcasting route through Privy's EIP-1193 embedded-wallet provider; balance and fee reads go through the RPC endpoints you configured on the builder, not through Privy.

* If no user is logged in, the adapter throws [`RAIN_201`](/sdks/embedded-wallets-error-reference).
* If there's no embedded wallet, it throws [`RAIN_404`](/sdks/embedded-wallets-error-reference).
* `getTransactions` returns an empty result, since Privy has no history endpoint.

Continue with [Set up a wallet](/sdks/embedded-wallets-setup) to build the SDK, resolve the wallet, and read its address.

## Built-in Rain API client

Independently of wallet auth, `RainSdk` includes a client for the Rain issuing API. Configure it once and the SDK fetches the user's [collateral contracts](/sdks/embedded-wallets-funding#read-the-users-collateral-details) and [admin withdrawal signatures](/sdks/embedded-wallets-withdraw-collateral) itself, minting and caching the required short-lived CST automatically. These calls never resolve or authenticate a wallet provider.

Configure it at build time, or later on the built instance:

<CodeGroup>
  ```swift iOS theme={null}
  let rain = try RainSdk.builder()
      .rpcEndpoints(endpoints)
      .register(provider)
      .rainApiEnvironment(.dev)          // default
      .rainApiCredentials(apiKey: apiKey, userId: userId)
      .build()

  // or after build():
  rain.configureRainApi(apiKey: apiKey, userId: userId)
  precondition(rain.isRainApiConfigured)
  ```

  ```kotlin Android theme={null}
  val rain = RainSdk.builder()
      .rpcEndpoints(endpoints)
      .register(provider)
      .rainApiEnvironment(RainApiEnvironment.Dev)          // default
      .rainApiCredentials(apiKey = apiKey, userId = userId)
      .build()

  // or after build():
  rain.configureRainApi(apiKey = apiKey, userId = userId)
  check(rain.isRainApiConfigured)
  ```
</CodeGroup>

Three environments are available:

| Environment                                     | Base URL                   |
| ----------------------------------------------- | -------------------------- |
| `.dev` / `RainApiEnvironment.Dev` (default)     | `https://api-dev.rain.xyz` |
| `.production` / `RainApiEnvironment.Production` | `https://api.rain.xyz`     |
| `.custom` / `RainApiEnvironment.Custom`         | your value                 |

<Warning>
  The SDK exchanges the `apiKey` you pass for a short-lived CST on-device. Never embed your primary `Api-Key` in a production binary: deliver a key scoped to this use from your backend at runtime, or keep signature fetching server-side and pass the result to [`withdrawCollateral`](/sdks/embedded-wallets-withdraw-collateral) yourself. Credentials are held in memory only and never persisted. See [Client side considerations](/reference/authenticating-with-the-api#client-side-considerations).
</Warning>

### Session handling

The SDK exchanges your `Api-Key` for a CST (`POST /v1/issuing/users/{userId}/sessions`) and sends it as `Authorization: Bearer <cst>` on data calls.

* The token is cached and reused while valid, and re-minted within 60 seconds of expiry.
* A `401`/`403` invalidates the cached token and retries the mint exactly once before surfacing [`RAIN_202`](/sdks/embedded-wallets-error-reference).
* Calling any built-in API method without configuring credentials throws [`RAIN_104`](/sdks/embedded-wallets-error-reference).
* Non-auth HTTP failures surface as [`RAIN_302`](/sdks/embedded-wallets-error-reference) with the status code.

## Mint a Rain session token (backend)

If you'd rather not put any API key in the app, your backend can mint the CST instead and forward it. The user must exist in Rain before you can mint a session, so create the user's application first (backend, with the wallet address; see [Common flows](/sdks/embedded-wallets-common-flows#the-end-to-end-flow)). Then exchange your primary `Api-Key` for a user-scoped Client Session Token and return the token to your app.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api-dev.rain.xyz/v1/issuing/users/{userId}/sessions" \
       -H "Api-Key: <YOUR_API_KEY>"
  ```

  ```ts TypeScript theme={null}
  const res = await fetch(
    `https://api-dev.rain.xyz/v1/issuing/users/${userId}/sessions`,
    { method: "POST", headers: { "Api-Key": process.env.RAIN_API_KEY! } },
  );

  const { token, expiresAt } = await res.json();
  // token: the "cst_..." Client Session Token to forward to your app
  // expiresAt: ISO-8601 expiry (tokens are short-lived, about 1 hour)
  ```
</CodeGroup>

The token is scoped to `userId` and expires after roughly an hour. Your app sends it as `Authorization: Bearer <cst>`. When it expires, mint a new one: the SDK surfaces an expired provider session as [`RAIN_201`](/sdks/embedded-wallets-error-reference) and an invalid API key or session as [`RAIN_202`](/sdks/embedded-wallets-error-reference).

Use the sandbox host `https://api-dev.rain.xyz/v1` while you build, and `https://api.rain.xyz/v1` for production. CST issuance is enabled per tenant: contact [platform@rain.xyz](mailto:platform@rain.xyz) if a `403` says it isn't enabled for your account.

## What's next

<Columns cols={2}>
  <Card title="Set Up a Wallet" icon="wallet" href="/sdks/embedded-wallets-setup">
    Build the SDK and create the user's wallet.
  </Card>

  <Card title="Common flows" icon="route" href="/sdks/embedded-wallets-common-flows">
    See the full backend-vs-app integration path.
  </Card>
</Columns>
