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

# useTapIntents

> For a tap page you host yourself: the open tap intents the tap can approve, with approve and cancel for the person, authorized by the tap session alone.

```ts theme={null}
function useTapIntents(): {
  tapIntents: readonly OpenTapIntent[];
  status: "idle" | "loading" | "ready" | "error";
  error: Error | null;
  refresh: () => Promise<readonly OpenTapIntent[]>;
  approve: (
    tapIntentId: string,
    args?: { tapCode?: string },
  ) => Promise<TapIntentOutcome>;
  cancel: (tapIntentId: string) => Promise<TapIntentOutcome>;
};
```

Reads `GET /v1/units/{unit_id}/tap-intents` with the active tap session once
per session, and again whenever you call `refresh()`. `approve` and `cancel`
call the session's approve and cancel endpoints. All three take the tap
session token only, so the page needs a publishable key and no server of its
own. See [Confirm a step with a tap](https://docs.endstate.io/guides/confirm-with-a-tap.md#if-you-host-your-own-tap-page).

The SDK does not retry `approve` or `cancel` after a failure, because a
repeated wrong code would spend one of the five tries. Only a `429
rate_limit.exceeded`, refused before anything runs, is retried. The hook sends
one request per tap intent at a time: a second `approve` while one is in
flight resolves with the first, and a `cancel` during a `approve` (or the
reverse) rejects. Do not retry them in a loop. Call `approve` only after the person confirms, never when the page loads.

## Example

```tsx theme={null}
"use client";
import { useState } from "react";
import { isEndstateApiError } from "@endstate-sdk/core/errors";
import { useTapIntents } from "@endstate-sdk/web/react";

function TapIntents() {
  const { tapIntents, approve, cancel } = useTapIntents();
  const [tapCode, setTapCode] = useState("");
  const [message, setMessage] = useState<string | null>(null);

  async function onApprove(id: string) {
    try {
      const { redirectUrl } = await approve(id, { tapCode });
      if (redirectUrl) window.location.assign(redirectUrl);
    } catch (error) {
      if (!isEndstateApiError(error)) throw error;
      setMessage(
        error.code === "tap_intent.code_incorrect"
          ? "Wrong code."
          : error.code === "tap_intent.tap_not_matched"
            ? "Tap the item again."
            : "This can no longer be confirmed.",
      );
    }
  }

  return (
    <ul>
      {tapIntents.map((tapIntent) => (
        <li key={tapIntent.id}>
          {tapIntent.title}
          {tapIntent.codeRequired && (
            <input
              inputMode="numeric"
              value={tapCode}
              onChange={(event) => setTapCode(event.target.value)}
            />
          )}
          <button onClick={() => onApprove(tapIntent.id)}>Confirm</button>
          <button onClick={() => cancel(tapIntent.id)}>Cancel</button>
        </li>
      ))}
      {message && <p>{message}</p>}
    </ul>
  );
}
```

## Returns

**tapIntents** `readonly OpenTapIntent[]`

One `{ id, title, codeRequired, redirectOrigin }` per open tap intent.
`redirectOrigin` is the origin of the tap intent's redirect URL, or `null`.
The list never carries a code. It is empty without a tap session, while the
first read loads, and when nothing is open. A row leaves the list when
`approve` or `cancel` resolves for it, or is refused because the tap intent
is closed.

**status** `"idle" | "loading" | "ready" | "error"`

`"idle"` without a tap session, `"loading"` while it reads, then `"ready"` or
`"error"`. A new tap reads as `"loading"` until its own answer lands.

**error** `Error | null`

The failure that moved `status` to `"error"`. A refused `approve` or
`cancel` is not shown here: it rejects the call.

**refresh** `() =\> Promise\<readonly OpenTapIntent[]\>`

Reads the list again for the current tap. Throws when there is no active
session.

**approve** `(tapIntentId: string, args?: \{ tapCode?: string \}) =\> Promise\<TapIntentOutcome\>`

Approves one tap intent. Pass `tapCode`, the six digits the person typed,
when the row has `codeRequired`. Resolves with
`{ id, status: "approved", redirectUrl }`. It does not navigate:
`redirectUrl` already has `tap_intent=<id>` added, and is `null` when the
tap intent has no redirect URL. Throws when there is no active session.

**cancel** `(tapIntentId: string) =\> Promise\<TapIntentOutcome\>`

Cancels one tap intent for the person, with no code. Resolves with
`{ id, status: "canceled", redirectUrl: null }`. Throws when there is no
active session.

## Errors

`approve` and `cancel` reject with the SDK error. Branch on `error.code`.

| Code | What to do |
| - | - |
| `tap_intent.code_required` | Ask for the code. No try was spent. |
| `tap_intent.code_incorrect` | Ask again. `error.response.error.details.attempts_remaining` says how many of the five tries are left. |
| `tap_intent.tap_not_matched` | The tap is too old or was made before the tap intent opened. Ask the person to tap the item again. |
| `tap_intent.busy` | Another request is changing the tap intent. Let the person try again. |
| `tap_intent.code_attempts_exhausted`, `tap_intent.expired`, `tap_intent.canceled`, `tap_intent.already_approved`, `tap_intent.already_consumed`, `tap_intent.not_found` | The tap intent is closed. The row has left the list. |
| `session_token.invalid_or_expired` | The tap session ended. Ask the person to tap the item again. |

These calls need the `@endstate-sdk/core` release that adds
[`session.tapIntents`](https://docs.endstate.io/sdks/core/reference/session-tapintents/list).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.