How It Works
The terminal comes online
Your POS logs in as a reader
Your POS pairs with the terminal
Your POS asks for a pass
start_capture. The terminal shows a scan screen such as “Earn 50 points”.The customer taps or scans
captured with the pass identifier to your POS.Your POS updates the pass and shows the result
result. The terminal shows a success or failure screen and returns to idle. Every API call the integration uses is listed in The API calls behind the integration.Before You Start
- A PassEntry Reader for each till, created in the Readers page of the Dashboard. Give it a name, a Reader ID such as
till-04, a password, and enable the modes you plan to use (Scan, Redeem, Adding). Your POS logs in with these credentials. - A device to act as the terminal: a payment terminal or card reader with the PassEntry Reader app available in its app store, or any tablet or phone with NFC or a camera.
- A backend service in your POS that can hold a WebSocket connection and make HTTPS calls. Connect from a server, not from a browser page.
- A pass identifier to test against, for a pass with loyalty enabled on its template. Make a test pass in the Dashboard and note its UUID or external ID. Get it before you write any code: a reader session cannot look passes up or create one, so without an identifier you can sign in, pair a terminal and arm a capture, and still have nothing to complete a single API call with. If someone else runs the account, ask them for one.
scanEnabled, addPointsEnabled and redeemEnabled for what the reader may do, and
qrEnabled and nfcEnabled for how it can read a pass. Read all five at sign-in and show only
what applies.The first three are yours to enforce, one per operation: scanEnabled for a plain Scan,
addPointsEnabled for an Accrue, redeemEnabled for a Redeem. Reading a balance is not gated by
any of them. The API does not refuse a call because a mode is switched off, so a reader with
addPointsEnabled: false can still accrue and get an ordinary 200. The two read flags are
enforced by the terminal.A reader is signed in from one place at a time, and each login invalidates the previous session, so
give the POS its own reader rather than sharing one with a physical PassEntry Reader device.Quickstart
Everything below is the full guide. If you would rather see it working first, these four commands are the whole integration with nothing left out. They need a Reader ID, its password, and a pass identifier (see Before You Start). No terminal is involved. Every field shown is required. Three of them are easy to miss: the body must be wrapped inpassentry_reader, and a scan needs both readerId and scanType.
1. Set Up the Terminal
Install the app
Switch to Customer mode
Note the Terminal ID
2NJE4Y. It stays the same until the app is uninstalled, so write it on the device or store it against the till in your POS.Leave it connected
wss://pos.passentry.com/cable as soon as it enters Customer mode, keeps that connection open and reconnects on its own after a network drop. It needs outbound access to pos.passentry.com on port 443, a stable Wi-Fi or Ethernet connection and mains power.

2. Log In as the Reader
Your POS authenticates the same way the PassEntry Reader app does: it signs in with the reader’s ID and password, receives a short-lived token, and sends a heartbeat every 5 minutes to keep the session alive and rotate the token. The four calls are Reader login, Reader heartbeat, Refresh reader token and Reader logout.Sign in
token: the bearer token for every API call and for the relay connection. It expires after 30 minutes.refreshToken: lets you obtain a new token without the password. It expires after 6 weeks.readerConfiguration: the NFC credentials you will send to the terminal (see Reading passes over NFC).
401 and an unknown Reader ID returns 404, both with {"errors": ["Invalid credentials"]}. A body wrapped in the wrong key returns the same 404 as an unknown Reader ID, so if you get a 404 for a reader you know exists, check that your JSON is wrapped in passentry_reader before you look anywhere else.
Send a heartbeat every 5 minutes
Recover from a 401
If an API call returns401, the token has expired or the reader was signed in elsewhere. Exchange the refresh token for a new pair, or sign in again with the password if the refresh token is also rejected:
Sign out
When the till closes, release the reader session:3. Connect and Pair
The relay speaks the Action Cable protocol over a WebSocket. Every frame is JSON, and theidentifier and data fields are themselves JSON encoded as strings.
Open the socket
till-04. It identifies this POS connection and is recorded as the owner of any terminal you pair with.Sec-WebSocket-Protocol: actioncable-v1-json. The relay replies with {"type":"welcome"} once the socket is open. The socket stays valid while it is open; use whichever token is current when you reconnect.
Pair with the terminal
Subscribe toRelayChannel with the Terminal ID:
{"type":"confirm_subscription", ...} when the terminal is online and you now own it, or {"type":"reject_subscription", ...} when the Terminal ID is blank or the terminal is not connected. A rejection has no error body. If the terminal has just been switched on, wait a few seconds and retry.
Only one POS should drive a terminal at a time. If a second POS subscribes to the same Terminal ID, the relay hands ownership to the newest subscriber and the earlier one stops receiving the terminal’s messages.
Keep the pairing alive
Ownership expires after 60 seconds without activity. Call theping action every 20 to 30 seconds while paired. This is separate from the 5 minute API heartbeat above: the API heartbeat keeps your reader session alive, the relay ping keeps your claim on the terminal alive.
The reply arrives wrapped in your subscription, like every other message from the relay, so read it from message.type rather than the top level:
{"type":"ping","message":<epoch>} frames every few seconds. Those need no reply.
Send and receive
Send a message to the terminal with theto_reader action. Everything in data except action is forwarded unchanged:
message.type and ignore any type you do not recognise:
unsubscribe command releases the terminal. If the socket drops mid-transaction, reconnect, wait for welcome, re-subscribe, send cancel and start the capture again.
4. Run a Scan, Accrue or Redeem
All three flows follow the same four steps. Only theflow value and the API call you make differ.
Start the capture
readerConfig, the terminal opens the camera and scans a QR code. To read passes over NFC, include readerConfig with the credentials from your login response (see Reading passes over NFC). NFC needs both: the credentials from you, and nfcEnabled on the reader itself. With both in place the terminal offers the camera and NFC at once for these flows, so send readerConfig whenever the reader supports NFC and let the customer choose.For Accrue and Redeem, amountMinor is the number of points shown to the customer: “Earn 50 points” or “Redeem 50 points” above the scan screen, and again on the success screen. It is display only. The API call decides the real amount.
Receive the pass
captured per start_capture:passData is the value read from the pass. Over NFC it is the pass’s NFC value, which defaults to the pass UUID. Over QR it is the barcode content, which also defaults to the pass UUID. If your pass template uses a custom NFC value or barcode, map it to a pass in your own system first, or use third-party validation with the Log scan endpoint.mode is how the terminal read the pass: NFC or QR. Older terminal builds omit it, so treat a missing mode as QR. Use it to set scanType in the next step.This is the method the customer actually used, which may not be the one you asked for: when the reader’s configuration allows both, the terminal offers the camera and NFC together.Call the PassEntry API
passData as passId in a Create scan request, authenticated with your current reader token. readerId is your Reader ID. Set scanType from the capture mode: nfc when it was NFC, otherwise barcode.passId accepts either the pass UUID or the pass’s external ID, so if your passes were issued with your own customer number as the external ID you can pass that directly. The response carries both identifiers as scanInfo.passId and scanInfo.passExtId, which is the easiest way to link the scan back to a customer record in your POS.One call covers all three. A positive balance accrues, a negative one redeems, and no
loyalty object at all is a plain Scan (which adds a stamp to a stamp-card pass). message is
the wallet notification the customer receives.Send whole numbers. This integration deals in whole points. A template can be configured for
decimal balances instead, in which case the balance comes back as a float; if that is your
setup, read Loyalty first.validationResponse with the outcome and the message
to show, and a scanInfo record of the scan. loyalty is present only when points changed, and
passExtId is null unless the pass was issued with an external ID.operation
redeem; a plain Scan reads “Scan was validated successfully” with no loyalty object, or
“Your card was stamped successfully” with operation stamp on a stamp card.A pass that fails validation still returns 200, with status: "failed" and the reason in statusDetail and readerInstructions.message. The scan is recorded and no points change:Pass is not active (the pass is expired, deactivated or nullified) and Pass already used within validity period (a repeat scan inside the validity window set on your reader configuration).Requests that cannot be processed at all return an error status with an errors array.Every response carries rate limit headers — x-rate-limit-limit, x-rate-limit-remaining and
x-rate-limit-reset (a Unix timestamp), scoped per endpoint by x-rate-limit-url. The
allowance is 10 per endpoint and refills continuously, so a till serving customers will not
approach it. A burst can: fired back to back from one process, reads exhaust it and the API
starts returning 429. Watch x-rate-limit-remaining and back off on a 429 rather than
retrying immediately, particularly in anything that loops.Send the result
result within 15 seconds of receiving captured. The terminal shows the outcome for 3 seconds and returns to idle.message as the heading, or a default of “Pass Valid”, “Points Added” or “Points Redeemed” when you omit it. Accrue and Redeem also show the points amount from start_capture. On failure it shows an “invalid pass” screen with your message.

Cancel a capture
cancel when staff back out of the transaction, and when your own timeout expires. The terminal’s scan screen has no timeout of its own, so if your POS stops waiting after 60 seconds and does not say so, the terminal stays armed and a later tap goes nowhere. Sending a new start_capture while one is in progress replaces it.
A timeout tells you nothing about why. The customer may not have presented anything, another POS may have taken the terminal, or your start_capture may never have arrived, since to_reader is not acknowledged. All three look identical: no frame, no error, no socket close. Treat a timeout as “unknown”, ask the cashier to try again, and do not report to them that the customer walked away.
5. The API Calls Behind the Integration
The reader token from step 2 authenticates every call. Create scan is covered in step 4; the two balance calls and end-of-day reconciliation are below.Check a balance before redeeming
Use thepassData from captured as the pass ID. The balance is returned in meta.loyalty.balance, as an integer or a decimal depending on the pass template.
balance and finish with result. Keep all three inside the 15 second window, or send result as soon as the scan call returns and read the balance beforehand.
Adjust a balance without a tap
Update loyalty balance changes a pass’s balance directly and sends the customer a wallet notification. Use it when there is no customer at the terminal, for example refunding points after a returned item or correcting a mistake. It does not record a scan or apply the reader’s validity period, so keep Create scan for anything that happens at the till.balance adds when positive and subtracts when negative. Send overrideBalance instead to set an absolute value. The response has the same shape as Get loyalty balance, with the new balance in meta.loyalty.balance. A non-numeric balance returns 400 with {"errors": ["Balance must be a number"]}.
Reconciliation and reporting
Scan history is not available to a reader session. List scans and Get scan return403 Reader is not authorized to perform this action for a reader token. For end-of-day reconciliation, call them from your backend with an organisation API key and filter by readerId, passId or createdAfter. Every scan your POS created in step 4 appears there with its scanId, so store that ID against your transaction when the Create scan response arrives.
Reading Passes Over NFC
Reading a wallet pass over NFC requires credentials that identify your entity to Apple Wallet and Google Wallet and decrypt the value the pass returns. Because a terminal in Customer mode has no account of its own, your POS sends these credentials with eachstart_capture.
They are already in the readerConfiguration object of your login response. Map them into readerConfig like this:
nfcEnabled and qrEnabled set, the terminal shows a toggle between NFC and camera, starting on the method named in mode. A device without NFC hardware uses the camera when qrEnabled is true.
Message Reference
Messages are JSON objects with atype field. Unknown fields are ignored on both sides.
POS to terminal
captured.Terminal to POS
start_capture.Example
A minimal Node.js client that logs in, heartbeats, pairs, asks for a QR scan and adds 10 points. It needs Node 18 or later and thews package.
- The reader shows as logged in on the Readers page.
- The terminal leaves the logo screen and shows “Earn 10 points” above a camera view.
- Hold the pass’s QR code up to the camera. The terminal shows Processing.
- The script prints
balance is now …. - The terminal shows Added Points 10 with the API’s message, “10 points added to your balance. Current balance: …”, for three seconds, then returns to the logo.
- The customer’s phone receives a wallet notification: “You earned 10 points.”
Troubleshooting
Login returns 401 or 404 Invalid credentials
Login returns 401 or 404 Invalid credentials
API calls return 401 Access denied. Please, sign in again.
API calls return 401 Access denied. Please, sign in again.
Handshake fails with HTTP 404
Handshake fails with HTTP 404
Subscription is rejected
Subscription is rejected
Paired, but the terminal ignores start_capture
Paired, but the terminal ignores start_capture
readerConfig is missing a credential field, or flow or mode is not one of the uppercase values above.Terminal returned to idle before I sent result
Terminal returned to idle before I sent result
Terminal stays on the scan screen
Terminal stays on the scan screen
cancel.API returns 404 Reader does not exist
API returns 404 Reader does not exist
readerId must be the Reader ID you logged in with. A reader token cannot record scans against another reader.API returns 404 Pass does not exist
API returns 404 Pass does not exist
passId must be a pass UUID or external ID. Custom NFC or barcode values need mapping first.API returns 422 on a redeem
API returns 422 on a redeem
API returns 412
API returns 412