Skip to main content
This guide wires the full pairing station flow: your operator finishes an internal process, taps a pre-encoded chip on a reader plugged into their computer, and your backend writes the pair through the API. The Reader SDK handles the hardware; you write two small pieces of glue. The flow is always two-legged: the reader runs in the browser, and your API key (end_sk_…) stays on your server. Neither half can do the other’s job - that separation is what keeps the key out of operator machines.

Prerequisites

  • An active collection for the units being paired - you have its collection_id and contract.status is "active".
  • @endstate-sdk/reader in your operator UI (install) and a supported reader - or test chips to build against first.
  • Encoded chips in hand. Each tap of one yields a chip_id and a fresh single-use e value (see Chips).
1

Create the unit (your backend)

Each physical item gets a unit. Create it when your internal process finishes - external_id is your own identifier, so you can find the unit again without storing Endstate ids.
Keep the returned unit.id - the tap that follows pairs against it.
2

Read the tap (operator UI)

The reader identifies the chip and hands you exactly what the pairing call needs. Send it straight to your backend - never to the Endstate API from the browser.The browser prompt rules split this into two paths, and they must stay split: anything that can prompt has to run inside a real click handler.
Leave the button clickable after a failure. NotFoundError only means the operator dismissed the chooser, and NetworkError clears as soon as whatever holds the reader releases it - see Troubleshooting.e is single-use and short-lived - forward it immediately, never queue or retry with a stale one. Include c whenever the tap provides it.An operator pairs their reader once, not once per unit. Use start() and stopScanning() between units and leave the connection open: stopScanning() does not release the device, so the next unit needs no prompt. Full model in Connecting once, not every time.On a phone rather than a USB reader there is no device to authorize, so connect() never prompts - but start() raises the Android NFC permission prompt unless it has already been granted. That is the whole reason the load-time path checks permission() before scanning and the click path does not have to. Readers with no queryable permission (the USB ones) report undefined, which the ?? "granted" above treats as “safe to scan”. See Readers and environments.
3

Pair the chip (your backend)

Your /api/pair handler makes the one call that binds the chip to the unit:
The response embeds a snapshot of the paired unit, including its issuance status - poll the unit until collection.token.status is active, or let waitUntilIssued do it.Two errors are worth handling specifically:
  • chip.already_paired (409) - the chip is bound to a different unit. A re-tap of an already-paired chip against its own unit is idempotent and succeeds.
  • chip.invalid_e_value (422) - the e didn’t decode. Usually a stale or replayed value: have the operator tap again.
4

Confirm with a verify (optional)

A second tap run through POST /v1/taps (chip_id and e in the body) proves the pair end-to-end - the response names the unit the chip now belongs to. This is the same call your field verification uses; see Verify a unit.

Build it without hardware first

The whole flow runs with zero hardware: create chips with is_test: true, generate tap credentials with POST /v1/test-helpers/taps, and drive the operator UI with the Reader SDK’s mock mode. Swap in the real reader and encoded chips at the end - nothing else changes.