> ## 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.

# Quickstart

> Drop <EndstateProvider> into your React app - or wire the vanilla client - to capture a tap, verify it, and let a customer claim your product from a browser page that holds only a publishable key, with @endstate-sdk/web.

A customer taps your product's chip and lands on your page, or scans it in-page. From there you confirm the item is genuine, show them what they tapped, provision a wallet for them, and let them **claim ownership** - all from a page that carries only a publishable key. No backend is involved in the tap, the verify, or the claim.

`@endstate-sdk/web` is the browser half of that flow. It captures the tap from a redirect or Web NFC, hands it to [`@endstate-sdk/core`](/sdks/core/quickstart) to open a [session](/sdks/core/tap-sessions), and provisions the customer's wallet. Key material never enters your page, and this version exposes no signing surface.

The whole flow is one line: **tap -> verify -> read the item -> provision a wallet -> claim**. In React it is one provider and three hooks; the vanilla client runs the same steps by hand. This quickstart leads with React and shows the vanilla path alongside it.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @endstate-sdk/core @endstate-sdk/web react
  ```

  ```bash bun theme={null}
  bun add @endstate-sdk/core @endstate-sdk/web react
  ```

  ```bash pnpm theme={null}
  pnpm add @endstate-sdk/core @endstate-sdk/web react
  ```
</CodeGroup>

`@endstate-sdk/core` is a peer dependency - it is the API client the tap sources and hooks plug into. `react` is an optional peer (`^19`), needed only for the React entrypoint at `@endstate-sdk/web/react`; the vanilla client at `@endstate-sdk/web` needs neither React nor any runtime dependency. Both Endstate packages are ESM-only.

## Prerequisites

* A **publishable key** (`end_pk_...`). It is safe in browser source; a secret key is not, and is rejected here by type and at runtime. [Credentials](/credentials) covers the difference.
* Your page's origin on your organization's [allowed-origins list](/settings/cors-origins), for both the API and the wallet frame. Until it is listed the browser blocks the request before it reaches Endstate.
* A way to mint an **identity token** for your signed-in user - a fresh, single-use token your own backend signs. It is yours to build, not part of the SDK - see [the identity credential](/sdks/web/wallet#the-identity-credential) and [Bring your own auth](/external-auth).

## The golden path

One provider, three hooks. `<EndstateProvider>` wraps your app - it builds the browser client, provisions the wallet, and verifies the landing tap on mount - and a child component reads the verified item, resolves the wallet address, and claims in a single call. The vanilla client in the second tab runs the same steps by hand.

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

  // Wrap your app once. The provider builds the browser client, provisions the wallet,
  // and (autoVerify, on by default) captures + verifies the landing tap on mount.
  function App() {
    return (
      <EndstateProvider
        publishableKey="end_pk_..."
        wallet={{
          // A fresh, single-use token proving your signed-in user's identity, minted by
          // your own backend. Called on every session, so never return a cached value.
          getIdentityToken: () => mintIdentityToken(),
          // Your auth state - getIdentityToken runs only once this is true.
          enabled: isSignedIn,
        }}
      >
        <Claim />
      </EndstateProvider>
    );
  }

  // A child reads the verified tap, the wallet address, and drives the claim.
  function Claim() {
    const { item, status: sessionStatus } = useSession();
    const { account } = useWallet();
    const { claim, status, data } = useClaim();

    // sessionStatus runs "idle" -> "capturing" -> "verifying" -> "verified".
    if (sessionStatus !== "verified" || !item) {
      return <p>Tap your item to begin.</p>;
    }

    return (
      <div>
        <h1>{item.name}</h1>
        {/* claim() with no args hands the item to the wallet address. status drives the button. */}
        <button
          disabled={!account || status === "claiming" || status === "settling"}
          onClick={() => claim()}
        >
          Claim
        </button>
        {status === "claimed" && <p>Claimed. {data?.id}</p>}
        {(status === "expired" || status === "failed") && (
          <p>That did not go through - tap again to retry.</p>
        )}
      </div>
    );
  }
  ```

  ```ts Vanilla theme={null}
  import { type TapSession } from "@endstate-sdk/core";
  import {
    createEndstateClient,
    defaultTapSources,
    createWallet,
  } from "@endstate-sdk/web";

  // A browser client holding only the publishable key, wired to the browser tap sources.
  const client = createEndstateClient({
    publishableKey: "end_pk_...",
    tapSources: defaultTapSources(),
  });

  // 1. Provision the wallet on PAGE LOAD (not inside the click): createWallet mounts the
  // frame and starts session establishment immediately; first-time setup can take seconds.
  const wallet = createWallet({
    publishableKey: "end_pk_...",
    // A fresh, single-use token proving your signed-in user's identity, minted by
    // your own backend. Called on every session, so never return a cached value.
    getIdentityToken: () => mintIdentityToken(),
  });

  async function claimOnTap(): Promise<void> {
    // 2. Capture a tap. Call inside a user gesture so an in-page NFC scan can prompt.
    const tap = await client.captureTap();
    if (!tap) return; // no tap on this page (direct visit / nothing to scan)

    // 3. Verify against Endstate: records the tap and opens a session bound to it.
    const session: TapSession = await client.verify(tap);

    // 4. Read the verified item straight off the session - no extra request.
    const item = session.item; // TapResponse["unit"] | null
    if (!item) return;
    console.log("Genuine item:", item.id, item.name);

    // 5. Resolve the customer's wallet address (provisioning started at step 1).
    const account = await wallet.ready(); // WalletAccount { address }

    // 6. Claim ownership into the wallet address. unit_id is bound to the session
    // scope automatically; only `to` (the recipient) is supplied.
    const claim = await session.claims.create({ to: account.address });
    console.log("Claim:", claim.id, claim.status); // "claiming" first

    // (optional) Wait for settlement: "claimed" | "expired" | "failed".
    const settled = await session.claims.waitUntilSettled(claim.id);
    console.log("Settled:", settled.status);
  }
  ```
</CodeGroup>

Walking the React flow:

1. **Wrap once with `<EndstateProvider>`.** It builds the browser client, provisions the [wallet](/sdks/web/wallet) from your `getIdentityToken` callback, and - because `autoVerify` is on by default - captures the landing tap and verifies it on mount. `wallet.enabled` gates provisioning on whether a customer is signed in; while it is `false` the wallet stays `unconfigured`.
2. **Read the verified item with `useSession()`.** Its `status` moves `idle` -> `capturing` -> `verifying` -> `verified` (or `error`), and `item` is the verified unit (`{ id, external_id, name, attributes, collection }`) once `status` is `verified`. It is read straight off the [session](/sdks/core/tap-sessions) with no extra request.
3. **Resolve the wallet with `useWallet()`.** `account` is a `WalletAccount` (`{ address }`) once the wallet reports ready; its `status` runs `unconfigured` -> `idle` -> `provisioning` -> `ready`. Customers who bring no address of their own get one this way.
4. **Claim with `useClaim()`.** `claim()` with no arguments hands the item to the wallet address, so `to` defaults to the address the provider provisioned. Its `status` drives the button: `claiming` then `settling`, landing on `claimed`, `expired`, or `failed`. `claim()` defaults to Endstate submitting the write for you; the alternate [`client_broadcast`](/sdks/core/reference/session-claims/create) execution mode returns a payload for your own signer instead.

The vanilla tab runs the same steps by hand: `createEndstateClient(...)` builds the client, [`captureTap()`](/sdks/web/tap-capture) reads the tap inside a user gesture, [`client.verify(tap)`](/sdks/core/tap-sessions) opens the session, `session.item` is the verified unit, [`wallet.ready()`](/sdks/web/wallet) resolves the address, and [`session.claims.create({ to })`](/sdks/core/reference/session-claims/create) claims it. `verify` derives its idempotency key from the tap, so a page reload replays the original response instead of spending the single-use credential.

<Note>
  The provider provisions the wallet as it mounts, in parallel with capturing
  and verifying the tap, so the address is usually ready by the time a customer
  taps Claim. Only the claim waits on the wallet; capture, verify, and read do
  not touch it. In the vanilla flow this is why `createWallet` runs at page load
  and only `wallet.ready()` blocks the claim.
</Note>

## Capturing more than the landing tap

The golden path reads the tap that opened the page. To let a customer already on your page scan another item in-page with Web NFC, `useTap()` exposes `scan()`, its `status`, and the Web NFC `permission` state; the vanilla client does the same through `captureTap()`. Either way, see [Tap capture](/sdks/web/tap-capture) for the source priority model, the user-gesture rule that gates in-page scanning, and how to present each Web NFC permission state.

## Next steps

<CardGroup cols={2}>
  <Card title="Tap capture" icon="scan-line" href="/sdks/web/tap-capture">
    In-page scanning, the source priority model, and Web NFC permissions.
  </Card>

  <Card title="Wallet" icon="wallet" href="/sdks/web/wallet">
    The createWallet lifecycle, the identity credential, and error handling.
  </Card>

  <Card title="session.claims.create" icon="arrow-right-left" href="/sdks/core/reference/session-claims/create">
    The claim request body, execution modes, and error codes.
  </Card>

  <Card title="Bring your own auth" icon="key" href="/external-auth">
    Issue an identity token for your signed-in user from your own login.
  </Card>
</CardGroup>
