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

# Tap sessions

> Turn a tap into a session scoped to that unit, so the session token is never something your own code has to carry.

A tap proves someone is holding a specific physical item. `verify()` records
that tap and hands back a **session** scoped to it: every call the session
makes carries the session credential, so the
[session token](/concepts/session-tokens) never has to be threaded through
your own code.

## Verify a tap

`verify()` takes the `chip_id` and `e` from the tap and returns the session:

```ts theme={null}
import { EndstatePublicClient, publishableKey } from "@endstate-sdk/core";

const endstate = new EndstatePublicClient({
  publishableKey: publishableKey(process.env.NEXT_PUBLIC_ENDSTATE_KEY),
});

const session = await endstate.verify({ chip_id: chipId, e, c });

session.item; // the verified unit - no extra request
session.scope; // what this session authorizes
session.expiresAt; // when it stops working
```

It never takes a URL. To get a tap out of one, parse it first with
`tryParseTapUrl`, which returns `null` rather than throwing:

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

const tap = tryParseTapUrl(window.location.href);
if (!tap) return renderTapPrompt();

const session = await endstate.verify(tap);
```

That handles both shapes a tap arrives in: an Endstate tap URL
(`/verify/{chip_id}?e=` or `/u/{chip_id}?e=`), and a [tap
redirect](/concepts/tap-redirects) destination on your own domain, where the
values arrive as `endstate_chip_id` and `endstate_e` query parameters instead.

<Note>
  Keeping the parse separate is deliberate. "Is there a tap on this page?" is an
  offline question whose answer is often no - a direct visit, or a reload after
  the query was stripped - so it belongs in a branch rather than a `try` around
  a network call. See [Host your own verify page](/guides/host-verify-page) for
  the full flow.
</Note>

<Warning>
  The `e` value is [single-use](/concepts/chips#the-e-value). Never cache one
  and never replay one yourself. `verify()` derives its idempotency key from the
  tap, so a page refresh replays the original response instead of spending the
  credential and reporting a false "already scanned".
</Warning>

## Act on the session

The session exposes exactly what a tap authorizes:

| Resource                | Methods                                                                                                                                                                                         |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.claims`        | [`create`](/sdks/core/reference/session-claims/create) · [`get`](/sdks/core/reference/session-claims/get) · [`waitUntilSettled`](/sdks/core/reference/session-claims/waitUntilSettled)          |
| `session.transfers`     | [`create`](/sdks/core/reference/session-transfers/create) · [`get`](/sdks/core/reference/session-transfers/get) · [`waitUntilSettled`](/sdks/core/reference/session-transfers/waitUntilSettled) |
| `session.sessionTokens` | [`current`](/sdks/core/reference/session-tokens/current) · [`revoke`](/sdks/core/reference/session-tokens/revoke)                                                                               |

## Sessions cannot be refreshed

A new session needs a new tap. There is no refresh, by design - the session's
authority comes from someone having physically held the item.

```ts theme={null}
session.isExpired(); // local clock check
session.msUntilExpiry(); // milliseconds remaining, or null
await session.revoke(); // end it early
```

<Note>
  `isExpired()` is a local clock check, for interface copy only. The SDK never
  gates a request on it: a device clock is not authoritative. The definitive
  answer is a `401 session_token.invalid_or_expired` from the API.
</Note>

## Adopting a session you already hold

If your server issued the session, or you kept the token across a page reload,
adopt it rather than re-verifying:

```ts theme={null}
const session = endstate.session(token);

// Fill in expiry and scope, which a bare token does not carry.
await session.refreshScope();
```

## Where the tap comes from

In a browser, core can capture the tap for you through a registered tap source,
or take the output of [`@endstate-sdk/reader`](/sdks/reader/quickstart)
directly - see [Browser](/sdks/core/browser). On a server, you already have
`chip_id` and `e` from whatever forwarded them to you.
