Skip to main content
A PassEntry terminal is a customer-facing device running the PassEntry Reader app in Customer mode. It can be a countertop payment terminal or card reader such as a PAX, Sunmi or Urovo device, a tablet, or a phone. It reads Apple Wallet and Google Wallet passes over NFC or QR, shows the customer what is happening, and hands the pass to your POS. Your POS decides what to do with it and calls the PassEntry API to validate the pass, add points or redeem points. Your POS pairs with a terminal by its ID, tells it when to read a pass and receives the pass back. Nothing about your checkout changes: your existing payment device still takes the payment.

How It Works

1

The terminal comes online

The terminal connects to the PassEntry relay and registers its Terminal ID, a 6-character code shown on the device.
2

Your POS logs in as a reader

Your POS signs in to the PassEntry API with a Reader ID and password, exactly as the PassEntry Reader app does, and keeps that session alive with a heartbeat.
3

Your POS pairs with the terminal

Your POS connects to the relay and pairs with the terminal by Terminal ID.
4

Your POS asks for a pass

When a customer is ready, your POS sends start_capture. The terminal shows a scan screen such as “Earn 50 points”.
5

The customer taps or scans

The terminal reads the pass and sends captured with the pass identifier to your POS.
6

Your POS updates the pass and shows the result

Your POS calls the Create scan endpoint to validate the pass, add points or redeem points, then sends 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.
The relay only forwards messages. It never reads passes, awards points or validates anything. Your POS owns the business logic.
Want to see it working before reading the detail? Jump to the example. It logs in, pairs with a terminal, asks for a QR scan and adds 10 points in under 80 lines of JavaScript.

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.
A reader’s configuration decides what your interface should offer. The login response reports it: 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 in passentry_reader, and a scan needs both readerId and scanType.
Keep the session alive with a heartbeat every 5 minutes, or the token expires after 30 (Send a heartbeat). Read the pass identifier off a customer’s phone with a PassEntry terminal by adding the relay, which is what the rest of this guide covers.

1. Set Up the Terminal

1

Install the app

Install the PassEntry NFC/QR Pass Scanner app from the Apple App Store, Google Play Store, PAX MAXSTORE, Sunmi App Store or Urovo App Store.
2

Switch to Customer mode

Open the app. On the login screen, tap Mode: Standalone ▾ at the bottom and choose Customer. Do not log in. The app switches to a customer-facing idle screen showing the PassEntry logo, and remembers this choice across restarts and updates.
3

Note the Terminal ID

Tap the grey gear icon in the top-right corner of the idle screen. The menu shows Terminal ID: XXXXXX, for example 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.
4

Leave it connected

The terminal connects to 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.
The login screen with the Mode menu open, showing Standalone, Merchant and CustomerThe Customer mode idle screen with the gear menu open, showing the Terminal ID
Type the Terminal ID exactly as shown, in uppercase. Pairing is case-sensitive. The ID never contains 0, O, 1 or I.
To leave Customer mode, open the gear menu and choose Log out.

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

The body must be wrapped in passentry_reader. Any other wrapper returns 404 Invalid credentials, exactly as if the Reader ID did not exist.
Keep three things from the response:
  • 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).
A wrong password returns 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

Each heartbeat marks the reader as online in the Dashboard and returns a new token and refresh token. Switch to the new token straight away: the previous one stops working the moment the heartbeat succeeds. Run the heartbeat on a 5 minute timer for as long as the POS is open, which keeps you well inside the 30 minute expiry.

Recover from a 401

If an API call returns 401, 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 the identifier and data fields are themselves JSON encoded as strings.

Open the socket

string
required
Your Reader ID, for example till-04. It identifies this POS connection and is recorded as the owner of any terminal you pair with.
string
required
Your current reader token from login or the last heartbeat.
Send the sub-protocol header 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.
Connect from your backend. The relay accepts connections with no Origin header (server-side WebSocket clients) or an Origin of https://pos.passentry.com. A browser page on any other origin is rejected with HTTP 404 during the handshake.

Pair with the terminal

Subscribe to RelayChannel with the Terminal ID:
The relay answers with {"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 the ping 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:
The relay also sends {"type":"ping","message":<epoch>} frames every few seconds. Those need no reply.

Send and receive

Send a message to the terminal with the to_reader action. Everything in data except action is forwarded unchanged:
Messages from the terminal arrive wrapped in your subscription identifier. Dispatch on message.type and ignore any type you do not recognise:
Closing the socket or sending an 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 the flow value and the API call you make differ.
1

Start the capture

With no 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.The terminal scan screen with an Earn 50 points banner above the camera view
2

Receive the pass

The terminal sends one 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.
3

Call the PassEntry API

Use 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.
The response always has the same shape: a 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.
A redeem reads “You successfully redeemed 50 points. Current balance: 225” with 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:
The two validation failures are 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).
Repeat posts are not deduplicated. There is no idempotency key on POST /api/v1/scans, and a reader session cannot look a scan up afterwards, so a retry after a timeout will move the balance twice. If a call times out you do not know whether it landed: read the balance back with Get loyalty balance before retrying, and guard the button against a double press at the till.
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.
4

Send the result

Send result within 15 seconds of receiving captured. The terminal shows the outcome for 3 seconds and returns to idle.
On success the terminal shows the flow’s success screen with your 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.
Pass validationResponse.readerInstructions.message from the API response straight through as message. The customer then sees the same wording as your POS, for example “50 points added to your balance. Current balance: 275”.
Green success screen reading Added Points 50 with the message 50 points added to your balanceRed failure screen reading Not enough points
If no result arrives within 15 seconds, the terminal returns to idle without telling the customer whether anything happened. If the API call is slow or errors, send a failure result at your own deadline and reconcile afterwards.

Cancel a capture

Send 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 the passData 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.
A typical redeem flow is: capture the pass, read the balance, decide the discount, then send the Create scan request with the negative 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 return 403 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 each start_capture. They are already in the readerConfiguration object of your login response. Map them into readerConfig like this:
With both 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.
If you include readerConfig at all, it must contain privateKey, googleCollectorId and applePassTypeId. A readerConfig missing any of them makes the whole start_capture unreadable and the terminal ignores it. For QR only, leave readerConfig out.The private key lets a device decrypt your customers’ pass values. Only send it over the TLS WebSocket, only to terminals you operate, and never log it.

Message Reference

Messages are JSON objects with a type field. Unknown fields are ignored on both sides.

POS to terminal

object
Arms the terminal to read one pass.
object
The outcome to show the customer. Must arrive within 15 seconds of captured.
object
Aborts the current capture and returns the terminal to idle. Ignored when the terminal is already idle.

Terminal to POS

object
The customer presented a pass. Sent once per start_capture.
Other types may appear in future. Ignore any you do not recognise.

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 the ws package.
What you should see:
  1. The reader shows as logged in on the Readers page.
  2. The terminal leaves the logo screen and shows “Earn 10 points” above a camera view.
  3. Hold the pass’s QR code up to the camera. The terminal shows Processing.
  4. The script prints balance is now ….
  5. 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.
  6. The customer’s phone receives a wallet notification: “You earned 10 points.”

Troubleshooting

The Reader ID or password is wrong, or the reader is a VTAP reader rather than a PassEntry Reader. Check the Readers page.
The token expired (30 minutes without a heartbeat), you kept using the old token after a heartbeat rotated it, or the same reader was signed in somewhere else. Use the token from the latest heartbeat, exchange the refresh token, or sign in again. Give each POS its own reader.
You connected from a browser page on another origin. Connect from your backend.
Wrong or lowercase Terminal ID, or the terminal is offline. Check the gear menu, confirm the device shows the idle logo screen, wait 10 seconds and retry.
One of three things: another POS paired with the same Terminal ID since, readerConfig is missing a credential field, or flow or mode is not one of the uppercase values above.
The 15 second window passed. Reply faster, or send a failure result at your own deadline.
Your POS stopped waiting without sending cancel.
readerId must be the Reader ID you logged in with. A reader token cannot record scans against another reader.
passId must be a pass UUID or external ID. Custom NFC or barcode values need mapping first.
Not enough points on the pass. Send a failure result with a clear message.
Loyalty balance is not enabled on the pass template.
If you are still stuck, email [email protected] with the Reader ID, the Terminal ID, the time of the transaction and the frames your POS sent and received.