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

# webNfcTapSource

> A tap source that scans with the device's own reader, where supported. A progressive enhancement over the redirect source, not the primary path.

```ts theme={null}
webNfcTapSource(options?: WebNfcTapSourceOptions): TapSource

const WEB_NFC_TAP_SOURCE_PRIORITY = 0
```

An in-page NFC scan, available on Android Chrome in a secure context. It reads a tap with the device's own NDEF reader and shapes it like every other tap source, so a core client dispatches to it through `captureTap()`. `defaultTapSources()` already includes it; register it directly only when you build the source list yourself.

This is a progressive enhancement, not a replacement for the redirect source. iOS has no in-page scanning, and some Android NFC stacks detect a tag but return an empty read, so treat any failure as a cue to fall back to opening the tap link. A scan needs a live user gesture to raise the permission prompt, so run the capture from a control the user pressed, never at page load.

## Example

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

const client = new EndstatePublicClient({
  publishableKey: "end_pk_live_...",
  tapSources: [webNfcTapSource()],
});

// Inside a user gesture, so the permission prompt can appear.
scanButton.onclick = async () => {
  try {
    const tap = await client.captureTap();
    if (tap) await client.verify(tap);
  } catch (error) {
    if (error instanceof WebNfcError) {
      // Branch on error.reason and fall back to opening the tap link.
    }
  }
};
```

## Parameters

<ParamField body="options" type="WebNfcTapSourceOptions">
  Optional configuration.
</ParamField>

<ParamField body="options.windowRef" type="() => WebNfcWindow | undefined">
  Test seam that supplies the window the source reads `NDEFReader` and the
  permissions API from. Defaults to the page's own `window`. Leave it unset in
  product code.
</ParamField>

## Returns

A [`TapSource`](/sdks/core/reference/taps) named `web-nfc` that a core client registers and dispatches to.

<ResponseField name="name" type="string" required>
  The source identifier, `web-nfc`. Pass it to `captureTap({ only: [...] })` to
  target this source, and match it against a captured tap's `source`.
</ResponseField>

<ResponseField name="priority" type="number" required>
  `WEB_NFC_TAP_SOURCE_PRIORITY`, which is `0`.
</ResponseField>

<ResponseField name="isAvailable" type="() => boolean" required>
  True when the device exposes `NDEFReader`. Synchronous on purpose, so
  `capture()` runs in the same task and the gesture that gates the permission
  prompt stays live.
</ResponseField>

<ResponseField name="capture" type="(options?) => Promise<TapResult | null>" required>
  Starts a scan and resolves with the captured tap. Resolves `null` for a
  readable tag that is not an Endstate chip, or when no reader is present.
  Rejects with a [`WebNfcError`](/sdks/web/reference/tap-sources/WebNfcError)
  when a scan fails in a way the interface should present distinctly.
</ResponseField>

## Priority

`WEB_NFC_TAP_SOURCE_PRIORITY` is `0`, below the redirect source's `20`. A tap already in the URL wins: on a page opened by a tap, the redirect source resolves from the URL before any scan starts. The scanner only runs when no higher-priority source captures.

<Note>
  Web NFC requires Android Chrome on a secure context (HTTPS or localhost).
  `isAvailable()` returns false everywhere else, and dispatch moves on to the
  next source.
</Note>

## See also

* [`webNfcPermissionState`](/sdks/web/reference/tap-sources/webNfcPermissionState) - gate the interface before offering a scan.
* [`WebNfcError`](/sdks/web/reference/tap-sources/WebNfcError) - the typed failures a scan rejects with.
