Skip to main content
EndstatePublicClient takes a publishable key (end_pk_...) and is safe to construct in page source. The key identifies your organization and grants no access by itself.
Never put a secret key (end_sk_...) in browser or Electron-renderer code, and never proxy one through a public endpoint. For server work, see Server.

What a publishable key can do

Exactly one thing: record a tap.
That is deliberate. The authority for anything further is not the key - it is the tap, which cannot be forged and cannot be replayed. Verifying returns a session scoped to that one unit, and the session is what authorizes claiming, transferring, and reading it. So the public client’s surface is small on purpose: A publishable key may not send ttl or dry_run. An untrusted browser does not get to choose how long its own credential lives.

Allow-list your origin first

The browser blocks the request before it reaches the API unless your page’s origin is on your browser allow-list. The symptom is an opaque network error rather than a useful response, so check this first when a call fails only in the browser. Core sends requests the way the API expects - it never sets credentials - so once the origin is listed there is nothing further to configure.

Capturing the tap

Core declares the tap-source interface and dispatches to it; it never bundles a reader. Register one and captureTap() runs the highest-priority source that can capture right now.
Sources are registered per client, so server rendering never shares one. The source a tap reports is an open string, so adding one is not a breaking change. The well-known names ship as constants - TAP_SOURCE_WEB_NFC, TAP_SOURCE_READER, TAP_SOURCE_REDIRECT, TAP_SOURCE_MANUAL, and TAP_SOURCE_UNKNOWN ("web-nfc", "reader", "redirect", "manual", "unknown"). For real hardware - desktop USB readers and Android phone NFC - use @endstate-sdk/reader and hand its output straight to verify():

Reading a tap redirect

On a page you host as a tap redirect destination, the values arrive as query parameters rather than in the path. tryParseTapUrl reads that shape too, on any path:
Or read the two parameters yourself and pass them straight to verify():
See Host your own verify page for the full flow, including the version where your own server holds the secret key.

Chip ids are normalized

Chip ids are matched either case and always returned uppercase. The SDK normalizes them for you, so never compare raw user input to an API response.