> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endstate.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet

> Provision a customer wallet from the browser with the useWallet hook and the provider wallet config: the getIdentityToken callback gated on sign-in, and error handling.

Customers who bring no address of their own get one from the Endstate wallet. The wallet runs in a hidden iframe on an Endstate origin; the SDK mounts that frame, establishes a session from your identity credential, and reports the account. Key material never reaches your page.

In React, `useWallet` reads that account and its provisioning status, and the `EndstateProvider` you already wrap your tree in takes the wallet config. The wallet provisions and reports the account and nothing more - it exposes no signing surface of its own, because a claim is signed by Endstate and a transfer is broadcast by the current owner's own signer. The account address is the whole of what a customer's claim needs: it is the recipient of [a claim](/sdks/web/claim), the step the [browser quickstart](/sdks/web/quickstart) walks end to end. Capturing and verifying the tap that opens that claim is separate and carries none of the wallet's requirements - see [Tap capture](/sdks/web/tap-capture).

## How provisioning works

The wallet is not a library that runs in your page. It is a document Endstate hosts, loaded into a hidden frame, that your page talks to over a narrow, origin-checked message bridge:

* **The frame is on an Endstate origin.** Once the wallet is enabled, the SDK appends a hidden iframe pointed at `https://wallet.endstate.io`. Because the wallet's own document owns the key material, that material is isolated from your page by the browser's origin boundary. Your page can ask for an account and a session; it can never read a private key.
* **The origins are compiled in.** The frame origins ship inside the published bundle and are not configurable. Every message the bridge sends is addressed to that one origin, and every message it accepts is checked against it. This is a security property of the design, not a default you can override.
* **There is no signing surface.** The account address is the whole of what the wallet exposes. Nothing in the API accepts bytes to sign or a transaction to authorize.

<Note>
  The wallet is document-scoped - it lives entirely client-side. In React, the
  provider subtree that owns the wallet must be a client component (`"use
      client"`); it never mounts on the server. In Vanilla, `createWallet` throws if
  `window` or `document` is absent.
</Note>

## Provisioning the wallet

Wrap your tree in `EndstateProvider` once and hand it the wallet config; `useWallet` then reads the account anywhere inside. The config is an `EndstateWalletConfig`: `getIdentityToken` supplies the identity credential each session is established from, and `enabled` gates provisioning on your sign-in state. There is no environment to set.

`useWallet()` returns `{ account, status, error, ready, refresh, expiresAt }`:

* **`account`** - the `WalletAccount` (`{ address }`), or `null` until provisioning finishes.
* **`status`** - a `WalletStatus`: `"unconfigured"`, `"idle"`, `"provisioning"`, `"ready"`, or `"error"`. It stays `"unconfigured"` while the wallet is not enabled, moves through `"provisioning"` as the frame mounts and the first session is established, and lands on `"ready"` once the account exists.
* **`error`** - the failure when `status` is `"error"`, otherwise `null`.
* **`ready()`** - resolves with the `WalletAccount` once it exists; await it where you first need the address.
* **`refresh()`** - establishes a fresh session and resolves with the account.
* **`expiresAt`** - a `Date` for when the current session lapses, or `null` before it is ready.

The Vanilla path is the same lifecycle without a provider: `createWallet` returns an `EndstateWallet` immediately, with the frame already loading behind it, and you `await wallet.ready()` where the address is needed.

<CodeGroup>
  ```tsx React theme={null}
  "use client";
  import { EndstateProvider, useWallet } from "@endstate-sdk/web/react";

  // Wrap your tree once. The wallet config takes getIdentityToken and enabled;
  // getIdentityToken is called only once enabled is true (here, after sign-in).
  function App() {
    return (
      <EndstateProvider
        publishableKey="end_pk_..."
        wallet={{
          getIdentityToken: () => mintIdentityToken(),
          // Your auth state - getIdentityToken runs only once this is true.
          enabled: isSignedIn,
        }}
      >
        <WalletAddress />
      </EndstateProvider>
    );
  }

  // Read the account anywhere inside the provider.
  function WalletAddress() {
    const { account, status, error } = useWallet();
    if (status === "unconfigured") return <SignInPrompt />; // enabled is still false
    if (status === "idle" || status === "provisioning") return <Spinner />;
    if (status === "error") return <Failed message={error?.message} />;
    return <code>{account?.address}</code>; // status is "ready"
  }
  ```

  ```ts Vanilla theme={null}
  import { createWallet } from "@endstate-sdk/web";

  // Mount the frame and start session establishment at page load; first-time
  // setup can take seconds, so do not put this inside a user action.
  const wallet = createWallet({
    publishableKey: "end_pk_...",
    // Called fresh for every session establishment; never return a cached value.
    getIdentityToken: () => mintIdentityToken(),
  });

  // Elsewhere, where the address is needed:
  const { address } = await wallet.ready();
  ```
</CodeGroup>

The wallet config takes:

<ParamField path="getIdentityToken" type="() => string | Promise<string>" required>
  Supplies the identity credential each session is established from. Called
  fresh every time, because the credential is single-use and short-lived, and
  only once `enabled` is true.
</ParamField>

<ParamField path="enabled" type="boolean">
  Gates provisioning on your sign-in state. While it is false the wallet stays
  `unconfigured` and `getIdentityToken` is never called; set it true once the
  customer is signed in and the wallet provisions.
</ParamField>

<ParamField path="container" type="HTMLElement">
  Where the hidden frame is appended. Defaults to `document.body`.
</ParamField>

<ParamField path="timeouts" type="{ frameReadyMs?: number; sessionMs?: number }">
  Wait budgets in milliseconds. Defaults: `frameReadyMs` 5000, `sessionMs`
  30000\.
</ParamField>

## Gating on sign-in

`getIdentityToken` runs only once `enabled` is true. Point `enabled` at your sign-in state - with BetterAuth, `enabled: session != null`, or your own `isSignedIn` flag - and the wallet stays `unconfigured` for signed-out visitors, calls nothing, and provisions the moment they sign in. Because the callback fires only behind that gate, a signed-out user never triggers a mint against your identity endpoint, and the credential is requested exactly when a session is needed.

## The identity credential

`getIdentityToken` is the one integration point you own. It returns a dedicated, single-use identity credential for the signed-in customer: a short-lived JWT, expiring in about a minute, that carries the customer's identity and is neither a session token nor a token used for anything else. That short life is what makes handing it to a browser acceptable - it is useless anywhere else, and anything else is useless here.

Endstate expects a JWT signed by your registered key, carrying the customer's identity. Its decoded payload:

```json theme={null}
{
  "iss": "https://auth.brand.example",
  "aud": "endstate:user",
  "sub": "your-stable-customer-id",
  "email": "customer@brand.example",
  "jti": "unique-per-token",
  "iat": 1755640000,
  "exp": 1755640060
}
```

Because it is single-use, the callback is invoked fresh for every session establishment - the first time the wallet provisions, and every refresh - so it must never return a cached value. It must resolve with a non-empty string; anything else throws a `TypeError`. It runs under the session wait budget, so a slow backend times out as `wallet.timeout` rather than hanging.

Where the token comes from depends on how your customers sign in:

* **Bring your own auth.** Your backend mints and signs it from your own login. See [Bring your own auth](/external-auth) for the exact claim contract - the accepted algorithms, the 300-second age ceiling, and the JWKS Endstate verifies against - and how to register your issuer.
* **Endstate-managed identity.** Endstate issues the credential against the customer's Endstate sign-in, so the page must be an Endstate-hosted surface. You still return it through the same callback.

## Refreshing and expiry

A session is time-bounded. In React, `useWallet().expiresAt` tells you when it lapses and `refresh()` establishes a new one; in Vanilla the same pair is `wallet.sessionExpiresAt()` and `wallet.refreshSession()`. Either calls `getIdentityToken` again for a fresh single-use credential, so your callback must be ready to issue another. Check `expiresAt` to refresh ahead of expiry rather than waiting for a call to fail, and treat a refresh as the remedy for `wallet.session_expired`: when a call fails with that code, refresh and retry.

```tsx theme={null}
const { refresh, expiresAt } = useWallet();
// Ahead of expiry, or after a wallet.session_expired failure:
await refresh();
```

## Error handling

Every wallet failure carries an `EndstateWalletError` with a stable `code`. Branch on `code`, never on `message` - messages are for humans and may change. In React the error surfaces on `useWallet().error`; in Vanilla the lifecycle methods reject with it. Either way it is the same object.

```ts theme={null}
import { EndstateWalletError } from "@endstate-sdk/web";

// `error` is useWallet().error in React, or the rejection from a lifecycle
// method in Vanilla. Branch on its stable `code`.
if (error instanceof EndstateWalletError) {
  switch (error.code) {
    case "wallet.session_expired":
      // Refresh and retry: refresh() in React, wallet.refreshSession() in Vanilla.
      break;
    case "wallet.timeout":
      // No response within the wait budget; retry or surface a failure.
      break;
    default:
    // Wallet and API codes pass through verbatim.
  }
}
```

`code` is one of three kinds:

* A `wallet.*` code from the frame, such as `wallet.session_expired`.
* An API error code, forwarded verbatim from the Endstate API.
* A code this package produced locally: `wallet.timeout` (no response within budget), `wallet.destroyed` (teardown settled the call), and - when the wallet's reply could not be used - `wallet.unsupported_protocol_version` or `wallet.internal_error`.

`wallet.unsupported_protocol_version` means the frame speaks a protocol this build does not; upgrade `@endstate-sdk/web`.

<ResponseField name="code" type="string">
  The stable, branchable identifier for the failure.
</ResponseField>

<ResponseField name="message" type="string">
  Human-readable detail. Do not branch on it.
</ResponseField>

<ResponseField name="docUrl" type="string">
  Documentation for this code, when the failure carried one.
</ResponseField>

<ResponseField name="requestId" type="string">
  Present when the failure originated at the Endstate API. Quote it in support
  requests.
</ResponseField>

<ResponseField name="details" type="Record<string, unknown>">
  Structured context for the failure, when the wallet supplied any.
</ResponseField>

## Next steps

<CardGroup cols={2}>
  <Card title="Claim ownership" icon="hand" href="/sdks/web/claim">
    Send the item to the account address this wallet provisions.
  </Card>

  <Card title="createWallet" icon="wallet" href="/sdks/web/reference/wallet/createWallet">
    The Vanilla signature and options for mounting a wallet frame.
  </Card>

  <Card title="EndstateWalletError" icon="triangle-alert" href="/sdks/web/reference/wallet/EndstateWalletError">
    Every code the wallet can throw and what to do with it.
  </Card>

  <Card title="Bring your own auth" icon="key" href="/external-auth">
    Mint the identity credential from your own login.
  </Card>
</CardGroup>
