@endstate-sdk/web turns any of them into the chip_id and e that
verify() accepts.
In React it is automatic. Wrap your tree in EndstateProvider and the tap that
opened the page is captured and verified on mount (autoVerify), while
useTap() drives an in-page scan from a
button. Outside React, the same work is one call -
captureTap() - on a client you build
with createEndstateClient(). Both pick the source that matches how the tap
actually arrived, so most pages never branch.
The division of labor: @endstate-sdk/core owns the
client, the session, and the dispatch; this package supplies the browser sources
core dispatches to, plus the React provider and hooks that wrap them. This page
covers each way a tap can arrive, the user-gesture rule that gates in-page
scanning, how to present a failed scan, and finally
how captureTap chooses when more than one
is possible.
The React entry point is
@endstate-sdk/web/react, and react is an optional
peer (^19). The vanilla client factory and tap-source primitives import from
@endstate-sdk/web. Every React example below is a client component.Capture the tap that opened the page
The common case, and the one the quickstart golden path uses: the customer taps your product, the chip link opens your page, and the tap is already in the URL. The redirect source reads it - no scan, no permission, nothing to ask the customer - so it is safe to run the moment the page loads. In React that isEndstateProvider’s job: it captures and verifies the landing
tap on mount (autoVerify defaults to true), and
useSession() exposes the result.
status moves through capturing and verifying to verified, and item is
the verified unit, read with no extra request. Outside React, call
captureTap({ only: ["redirect"] })
yourself and hand the tap to verify.
- The page is the tap link’s direct destination - an Endstate chip URL
(
/verify/{chip_id}?e=or/u/{chip_id}?e=). - The page sits behind an Endstate hosted redirect - the values arrive as
endstate_pathId(the chip id) andendstate_equery parameters on your own URL. (endstate_chip_idis accepted as an alias so both spellings of the contract parse; the delivered name isendstate_pathId.)
window.location.href by default; pass a url option (a () => string) to
redirectTapSource() when
you need to parse a URL you hold yourself.
Let a customer scan an item in-page
When a customer is already on your page and wants to scan another item without leaving it, the Web NFC source reads a tag with the phone’s own reader. Reach for it for that in-page scan; leave the tap that opened the page to the redirect source above. In React,useTap() wraps it: scan()
runs the in-page scan and verifies the result (which lands on useSession()),
status reports idle, scanning, or error, and permission is the current
WebNfcPermissionState. Call
scan() from the control the customer pressed. Outside React, call
captureTap() from that same handler.
WebNfcError rather than a bare
null, so the interface can present a distinct message - and a foreign tag
never ends dispatch as this source’s final answer.
In-page scans need a user gesture
A scan starts a permission-gated NFC read, and the browser only allows the permission prompt to appear from inside a live user gesture. Run the scan from a control the customer pressed, never at page load or in an effect: calluseTap().scan() from an onClick in React, or captureTap() from the button’s
handler in vanilla JS.
The Web NFC source is intentionally synchronous in its isAvailable() check so
that capture() runs in the same task as the click, keeping the gesture that
gates the prompt live. Kick a scan off from a setTimeout, a useEffect, an
await that resolves later, or page load, and Chrome silently suppresses the
prompt.
Present the right message when a scan fails
Web NFC permission is granted per site, and a denial never re-prompts on its own; only the browser’s site settings can re-enable it. Gate the interface ahead of time. In React,useTap().permission is the current state; outside React,
webNfcPermissionState()
resolves to the same four values for the current origin.
granted- a scan reads without prompting.prompt- a scan started inside a user gesture will ask.denied- only the browser’s site settings can re-enable it; present that path rather than a retry.unsupported- the device has no usable reader; show the redirect-only interface.
WebNfcError whose
reason tells the interface exactly what to present - it lands on
useTap().error in React, or the captureTap() rejection in vanilla JS. This is
the one place a table is the right shape, because it is a reason-to-action
matrix:
permission-denied and permission-not-requested split a single browser
NotAllowedError by re-querying the permission after the failure. A prompt
state means the ask never landed (dismissed or suppressed), which is
recoverable from a clean control; anything else means the site is blocked and
only settings will change it.Accept a tap value you collected
When your own interface has the tap value already - a pasted link, a scanned QR code, a test flow - the manual source wraps it. You supply a provider; the source validates and normalizes the value through the same core parser as every other source, so it can never emit something the others would have rejected or cased differently. The manual source is not in the defaults; add it yourself. In React, includemanualTapSource in EndstateProvider’s tapSources and capture it with
useEndstateClient().captureTap({ only: ["manual"] }). Outside React, register
it on the client with registerTapSource. Either way an invalid URL throws
core’s ChipUrlError, so form validation can branch on it:
null/undefined/"" when there is nothing to submit (which captures as
null). At priority -10 the manual source runs last, so a genuine redirect or
scan always wins over a value sitting in an input.
How captureTap chooses a source
You rarely call the sources directly - you register them on a client (or hand them toEndstateProvider), and
captureTap() runs the
highest-priority source that reports itself available right now, returning
its TapResult or null when none has a tap to offer. The React provider
dispatches through the exact same order: autoVerify and useTap() are thin
wrappers over it. It does not merge sources or try them all: the winner is
whichever available source sits highest in the order. That order is the point - a
tap that already arrived in the URL should always beat a scan you would otherwise
have to ask the customer to perform.
defaultTapSources() returns the two sources a browser page wants, already in
priority order, and is what both EndstateProvider and createEndstateClient()
register when you pass no tapSources of your own:
redirectTapSource()at priority20- the tap already opened this page.webNfcTapSource()at priority0- an in-page scan, where the device supports one.manualTapSourceat priority-10- not in the defaults; add it when your interface collects a value directly, and it stays below both platform sources.
only:
name, a numeric priority, an
isAvailable() check, and a capture() method - core’s TapSource interface -
and the well-known names ship from core as constants: TAP_SOURCE_REDIRECT
("redirect"), TAP_SOURCE_WEB_NFC ("web-nfc"), and TAP_SOURCE_MANUAL
("manual").
The defaults are a starting point, not a fixed set. Pass your own array to
tapSources, or call registerTapSource to add one (for example a
@endstate-sdk/reader hardware source), and
captureTap() folds it into the same priority dispatch.Next steps
defaultTapSources
The redirect-then-scan pair a browser page registers by default.
redirectTapSource
Read the tap the redirect landed on this page.
webNfcTapSource
Scan with the device’s own reader, where supported.
webNfcPermissionState
The scan permission state for this origin.
manualTapSource
A tap source over a value your own interface collected.
Browser (core)
The TapSource seam and captureTap dispatch, from core’s side.

