@danidoble/webserial-pax
v1.0.0
Published
A strongly-typed, event-driven USB pax driver for the Web Serial API, built on top of webserial-core.
Readme
@danidoble/webserial-pax
A strongly-typed, event-driven PAX payment terminal driver built on top of webserial-core.
Handles the serial connection, JSON handshake, auto-reconnect, and message routing — so you only deal with clean, typed events.
Not tied to a single transport: swap in the WebUSB, Web Bluetooth, or WebSocket provider from webserial-core, or implement your own SerialProvider for any platform.
Requirements
webserial-core^2.1.0(peer dependency)- A compatible transport (see Providers):
- Web Serial API — Chrome / Edge 89+ (default, no extra setup)
- WebUSB — Chrome / Edge (via
WebUsbProvider) - Web Bluetooth — Chrome / Edge (via
createBluetoothProvider, Nordic UART Service) - WebSocket — any environment (via
createWebSocketProvider+ a bridge server) - Custom — any platform via your own
SerialProviderimplementation
Installation
# npm
npm install @danidoble/webserial-pax webserial-core
# pnpm
pnpm add @danidoble/webserial-pax webserial-core
# yarn
yarn add @danidoble/webserial-pax webserial-core
# bun
bun add @danidoble/webserial-pax webserial-core
webserial-coreis a peer dependency — it must be installed alongside this package.
Quick start
import { Pax } from '@danidoble/webserial-pax';
import type { SerialPortFilter } from '@danidoble/webserial-pax';
const filters: SerialPortFilter[] = [{ usbVendorId: 0x0ed3 }];
const pax = new Pax({ filters });
// Serial lifecycle
pax.on('serial:connecting', () => console.log('Opening port…'));
pax.on('serial:connected', () => console.log('Port open'));
pax.on('serial:disconnected', () => console.log('Disconnected'));
// PAX events
pax.on('pax:connected', ({ name }) => console.log('PAX ready:', name));
pax.on('pax:payment', (data) => console.log('Payment event:', data));
pax.on('pax:error', (data) => console.error('PAX error:', data));
// Opens a port picker dialog (requires a user gesture)
await pax.connect();
// Set credentials for MIT login
pax.server = 'PROD';
pax.businessId = 'your-business-id';
pax.encryptionKey = 'your-encryption-key';
pax.apiKey = 'your-api-key';
// Perform a sale (blocks until terminal responds)
const approved = await pax.sendSale({ amount: 100.00, reference: 'ORDER-001' });
console.log('Approved:', approved);
// Retrieve a voucher
const voucher = await pax.sendVoucher({ folio: 12345 });
console.log('Voucher:', voucher);
// Refund
await pax.sendRefund({ amount: 100.00, folio: '12345', auth: '654321' });Serial settings
The constructor pre-configures the following defaults — no extra setup needed:
| Setting | Value |
| ------------------ | ------------------ |
| Baud rate | 115 200 |
| Data bits | 8 |
| Stop bits | 1 |
| Parity | none |
| Flow control | none |
| Buffer size | 32 768 B |
| Parser | delimiter (\r\n) |
| Command timeout | 5 000 ms |
| Auto-reconnect | ✓ |
| Reconnect interval | 1 500 ms |
| Handshake timeout | 4 000 ms |
Providers
By default the library uses the browser's native Web Serial API (navigator.serial). You can replace this with any of the built-in providers from webserial-core, or write your own.
Web Serial API (default)
No setup required — works out of the box in Chrome / Edge 89+.
import { Pax } from '@danidoble/webserial-pax';
const pax = new Pax({ filters: [{ usbVendorId: 0x0ed3 }] });
await pax.connect();WebUSB (WebUsbProvider)
import { Pax, WebUsbProvider } from '@danidoble/webserial-pax';
const pax = new Pax({
filters: [{ usbVendorId: 0x0ed3 }],
provider: new WebUsbProvider(),
});
await pax.connect();Web Bluetooth (createBluetoothProvider)
import { Pax, createBluetoothProvider } from '@danidoble/webserial-pax';
const pax = new Pax({ provider: createBluetoothProvider() });
await pax.connect();WebSocket (createWebSocketProvider)
import { Pax, createWebSocketProvider } from '@danidoble/webserial-pax';
const pax = new Pax({
provider: createWebSocketProvider('ws://localhost:8080'),
});
await pax.connect();Global provider (AbstractSerialDevice.setProvider)
Set a provider once for all device instances instead of per-instance. Import AbstractSerialDevice directly from webserial-core:
import { AbstractSerialDevice, WebUsbProvider } from 'webserial-core';
import { Pax } from '@danidoble/webserial-pax';
AbstractSerialDevice.setProvider(new WebUsbProvider());
const pax = new Pax({ filters: [{ usbVendorId: 0x0ed3 }] });
await pax.connect();Custom provider
Implement the SerialProvider interface to target any platform:
import type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-pax';
import { Pax } from '@danidoble/webserial-pax';
const myProvider: SerialProvider = {
async requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort> {
// return a SerialPort-compatible object
},
async getPorts(): Promise<SerialPort[]> {
// return previously authorised ports
},
};
const pax = new Pax({ provider: myProvider });
await pax.connect();API
Constructor
new Pax(options?: PaxOptions)| Option | Type | Default | Description |
| ----------------- | --------------------- | ------- | ------------------------------------------------- |
| filters | SerialPortFilter[] | [] | USB filters shown in the port picker dialog. |
| provider | SerialProvider | — | Custom serial transport (WebUSB, WebSocket, etc). |
| polyfillOptions | SerialDeviceOptions | — | Low-level serial settings override. |
Properties
pax.server // get / set — 'PROD' | 'QA' | 'DEV'
pax.businessId // get / set — string | null
pax.encryptionKey // get / set — string | null
pax.apiKey // get / set — string | nullMethods
pax.connect()
Opens the port picker and performs the handshake. Inherited from AbstractSerialDevice.
pax.sendConnectMessage()
Sends a CONNECT command manually.
pax.sendSale(options)
Performs a full sale flow: login → init → payment. Resolves to true if approved, false otherwise. Throws if a sale is already in progress.
| Option | Type | Default | Description |
| ----------- | ---------------- | ------- | -------------------------------------------- |
| amount | number | 0 | Sale amount. Must be greater than 0. |
| reference | string \| null | null | Alphanumeric reference (allows - and _). |
const approved = await pax.sendSale({ amount: 150.00, reference: 'ORDER-42' });pax.sendVoucher(options)
Requests the last voucher by folio. Resolves to the voucher data or rejects on timeout (10 s).
| Option | Type | Description |
| ------- | -------- | ---------------------------------------------- |
| folio | number | Folio number returned after a successful sale. |
const voucher = await pax.sendVoucher({ folio: 12345 });pax.sendRefund(options?)
| Option | Type | Default | Description |
| -------- | -------------------------- | ------- | ---------------------------- |
| amount | number | 0 | Refund amount (> 0). |
| folio | number \| string \| null | null | Original transaction folio. |
| auth | number \| string \| null | null | Original transaction auth. |
await pax.sendRefund({ amount: 150.00, folio: '12345', auth: '654321' });pax.sendInfo()
Requests device information. Emits pax:info.
pax.sendKeepAlive()
Sends a keep-alive ping. Emits pax:keep-alive.
pax.sendRestartApp()
Requests the PAX app to restart. Emits pax:reset-app.
pax.sendGetConfig()
Requests the device configuration. Emits pax:get-config.
pax.sendHideButtons()
Hides the terminal's on-screen buttons.
pax.sendShowButtons()
Shows the terminal's on-screen buttons.
pax.sendDemo()
Triggers demo mode on the terminal.
pax.sendProductionQR()
Reads a QR code in production mode.
pax.sendQualityAssuranceQR()
Reads a QR code in QA mode.
pax.sendExit()
Sends an exit command to the terminal.
pax.sendCustomCode(obj)
Sends a raw JSON command. The object must have an action key.
await pax.sendCustomCode({ action: 'CUSTOM_CMD', extra: 'value' });pax.softReload()
Resets in-progress sale / voucher state without reconnecting. Useful for recovering from stale operations.
pax.cancelSaleRequestInProcess()
Cancels an in-progress sale that is still awaiting a response.
pax.isConnected()
Returns true when the port is open and the handshake has completed.
Commands (static helpers)
All commands are available as standalone static methods for building raw payloads:
import { Commands } from '@danidoble/webserial-pax';
Commands.connection() // CONNECT handshake
Commands.expectedResponseConnection() // expected CONNECT response
Commands.makeSale({ amount: 100 }) // PAYMENT
Commands.makeSale({ amount: 100, reference: 'X' }) // PAYMENT with reference
Commands.getVoucher({ folio: 12345 }) // GETVOUCHER
Commands.refund({ amount: 100, folio: '1', auth: '2' }) // REFUND
Commands.info() // DEVICEINFO
Commands.keepAlive() // KEEPALIVE
Commands.restartApp() // RESETAPP
Commands.getConfig() // GETCONFIG
Commands.hideButtons() // HIDEBUTTONS
Commands.showButtons() // SHOWBUTTONS
Commands.forceHide() // FORCEHIDE
Commands.forceShow() // FORCESHOW
Commands.demo() // DEMO
Commands.exit() // EXIT
Commands.init() // INIT
Commands.readQR() // READQR (production)
Commands.readQR({ type: 'QA' }) // READQR (QA)
Commands.loginMit({ server, business_id, encryption_key, api_key })
Commands.custom({ action: 'MY_ACTION' }) // custom payloadEvents
Core events (from webserial-core)
| Event | Payload | Description |
| ------------------------ | ----------------------------- | ------------------------------------------------- |
| serial:connecting | instance | Port is being opened. |
| serial:connected | instance | Port opened successfully. |
| serial:disconnected | instance | Port closed or device unplugged. |
| serial:reconnecting | instance | Auto-reconnect attempt in progress. |
| serial:data | data: string, instance | Parsed string frame received from the device. |
| serial:sent | data: string, instance | Data written to the port. |
| serial:error | error: Error, instance | An error occurred during communication. |
| serial:need-permission | instance | No authorised port found; user must grant access. |
| serial:timeout | command: string, instance | A queued command timed out. |
PAX events
| Event | Payload | Description |
| -------------------- | --------------------------------------- | ------------------------------------------------------------ |
| pax:connected | { name, request, status } | Handshake succeeded; PAX is ready. |
| pax:init | { name, request, status } | INIT response received. |
| pax:init-app | { name, request, status } | INITAPP response received. |
| pax:login | Record<string, unknown> | LOGIN response received. |
| pax:voucher | Record<string, unknown> | LASTVOUCHER response received. |
| pax:info | Record<string, unknown> | DEVICEINFO response received. |
| pax:keep-alive | { name, request, status } | KEEPALIVE response received. |
| pax:reset-app | { name, request, status } | RESETAPP response received. |
| pax:get-config | Record<string, unknown> | GETCONFIG response received. |
| pax:buttons-status | { name, request, hidden: boolean } | Button visibility changed. |
| pax:payment | Record<string, unknown> | Any payment-related response (process, card data, result). |
| pax:error | Record<string, unknown> | An ERROR response was received. |
| pax:refund | Record<string, unknown> | REFUND response received. |
| pax:message | Record<string, unknown> \| string | Emitted for every incoming message (parsed or raw string). |
TypeScript
All events and method signatures are fully typed. The package ships with .d.mts / .d.cts declaration files — no extra @types package required.
Commonly used types and all built-in providers are re-exported so you do not need to import directly from webserial-core:
import {
Pax,
Commands,
WebUsbProvider,
createBluetoothProvider,
createWebSocketProvider,
} from '@danidoble/webserial-pax';
import type {
PaxOptions,
MakeSaleOptions,
GetVoucherOptions,
RefundOptions,
LoginMitOptions,
ServerType,
SerialPortFilter,
SerialDeviceOptions,
SerialEventMap,
SerialProvider,
SerialPolyfillOptions,
} from '@danidoble/webserial-pax';