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_idandcontract.statusis"active". @endstate-sdk/readerin 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_idand a fresh single-useevalue (see Chips).
1
Create the unit (your backend)
Each physical item gets a unit. Create it when your internal process
finishes - Keep the returned
external_id is your own identifier, so you can find the unit
again without storing Endstate ids.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 The response embeds a snapshot of the paired unit, including its issuance
status - poll the unit until
/api/pair handler makes the one call that binds the chip to the unit: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) - theedidn’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 withis_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.
