Mobile Device Integration#
Guide for mobile app and POS terminal developers integrating with the AuxCORE device API.
Current version: v2.4.0 | Last updated: 2026-10-26 | See Changelog at the end of this page
⚠️ v2.0.0 is a breaking change. The registration flow requires a
POST /device/test-connectionstep before the device becomes active. See Changelog for migration details.ℹ️ v2.3.0:
POST /device/test-connectionnow validates the merchant gateway configuration the device will use (config only — no processor call, no money moved) and returns a specific failurereason. Its error codes changed — see Test Connection and the Changelog.🆕 v2.4.0 — multi-merchant support. A user who belongs to several merchants can switch merchant without logging out, and without re-entering TPN + pairing code for a merchant they already registered on. New:
GET /device/session, multi-merchant storage contract, andPOST /device/logoutclarified as Unregister Device — not user sign-out.
Overview#
Mobile apps communicate with the device backend via a dedicated set of endpoints under /api/v1/device. These use device-scoped authentication (a device JWT) rather than user credentials, so the app operates independently after the initial pairing.
Integration Flow#
1. Admin creates a device in the portal → TPN + pairing code, tied to one merchant
2. Admin shares TPN and pairing code with the device operator
3. App calls POST /auth/login → userJWT
└─ multi-merchant user → POST /auth/select-merchant → userJWT scoped to merchant M
4. App calls GET /device/session → "is my stored token for M still usable?"
└─ reusable: true → skip to step 7, do NOT ask for TPN
└─ no stored token, or reusable: false → continue to step 5
5. App calls POST /device/register → receives deviceToken (JWT), device status = REGISTERED
6. App calls POST /device/test-connection → validates gateway configuration
→ on success: device status = ACTIVE
(skipped when step 5 returns nextStep: "none")
7. App stores deviceToken under merchant M → secure storage, keyed by merchantId
8. App calls GET /device/parameters → loads configuration
9. If posLite.enabled is true, app calls GET /device/products → current branch catalog
10. App calls GET /device/heartbeat periodically → detects config changes
11. When paramVersion changes, app calls GET /device/parameters again
12. Switch merchant: POST /auth/switch-merchant → new userJWT, then back to step 4
13. If device.paymentAdjustmentType != "NONE", call GET /transactions/fee-preview before
displaying price to customer → always use server-computed price, never calculate in app
14. Before each sale in dual-pricing mode (pricingModel = cashdiscount), call
GET /transactions/fee-preview → displays cash / credit / ACH pricesThe login/session and merchant-switching portions are the v2.4.0 flow. The session check is what
removes the repeated TPN prompt — see Multi-merchant device storage.
Dual pricing (cash / credit / ACH display)#
Merchants in dual-pricing mode (pricingModel: cashdiscount) require the POS to show posted cash price and credit (card) price before payment — a compliance requirement in states such as New York. AuxCORE computes these server-side; do not calculate adjustments in the app.
Use the prices.cash, prices.card, and prices.ach objects from the response. When dualPricingRequired is true, all three labels must be shown when ACH is enabled.
Full reference: Fee Preview API
Device Status Lifecycle#
Admin creates device
│
▼
PENDING ──── App calls /register ────► REGISTERED ──── /test-connection pass ────► ACTIVE
│ │ │
│ admin disable re-register ok admin disable
▼ (refresh token) ▼
INACTIVE ◄──────────────────────────────────────────────────────────────────────── INACTIVE
│ │
└──────────────────────── admin suspend ────────────────────────────────────────► SUSPENDED| Status | Meaning | Can process payments? |
|---|---|---|
PENDING |
Admin created, app not yet registered | ❌ No |
REGISTERED |
App registered, test-connection not yet passed | ❌ No |
ACTIVE |
Fully activated — test passed | ✅ Yes |
INACTIVE |
Deactivated (unregister or admin action) | ❌ No |
SUSPENDED |
Locked by admin | ❌ No |
ARCHIVED |
Archived (soft-deleted) by admin; sessions revoked. Unarchiving restores it to INACTIVE |
❌ No |
There is no separate
UNREGISTEREDstatus — unregistering a device puts it inINACTIVE, from which it can be registered again with the same TPN + pairing code.
Multi-merchant device storage#
A merchant user may belong to several merchants and register a different device at each. Each Device has its own TPN + Pairing Code, and after registration the Device becomes Active.
The app owns the memory of which device belongs to which merchant. Store a map in secure storage:
merchantId → deviceTokenRules the app must follow:
| Rule | Behaviour |
|---|---|
| Entering a merchant for the first time | No stored token → show Device Registration and call POST /device/register |
| Returning to a merchant | Look up the stored token, call GET /device/session, and enter directly when reusable: true — do not ask for TPN again |
| Switching merchant | Call POST /api/v1/auth/switch-merchant first (new access token), then run the GET /device/session check for that merchant |
| Signing out | Keep the map. Sign-out ends the user session only; devices stay ACTIVE so the next login re-enters without a TPN prompt |
| Unregistering a device | Call POST /device/logout and delete that merchant's entry from the map |
| Multiple active devices on one merchant | Only ever replay the token you stored. Never adopt another device just because it is ACTIVE on the same merchant |
The server never picks a device for the app. GET /auth/my-merchants reports deviceRegistration.hasActiveDevice per merchant so the picker can show which merchants still need a TPN — that is a display hint; GET /device/session is the authority.
Recovering a lost token. If the app is reinstalled or storage is cleared, register again with the same TPN + pairing code. The device is still ACTIVE, and re-registration is allowed for the user who registered it originally — no admin action needed. A different user gets 400 Device already registered.
Typical merchant-switch sequence#
GET /api/v1/auth/my-merchants → list + hasActiveDevice per merchant
POST /api/v1/auth/switch-merchant {B} → new access token scoped to Merchant B
GET /api/v1/device/session → with the stored device token for B
reusable: true → go to Home / Payment
reusable: false → show Device Registration (TPN + pairing code)Base URL#
https://uat.auxcore.netAuthentication#
Required Headers (All Requests)#
| Header | Value | Description |
|---|---|---|
X-Tenant-ID |
your-tenant-id |
Identifies the tenant. Through the API gateway it is overwritten with the tenantId from the user JWT, so it must match the token |
Content-Type |
application/json |
For POST/PUT requests |
Dual-Header Pattern#
The mobile app uses two separate headers for two separate concerns:
| Header | Purpose | When to send |
|---|---|---|
Authorization: Bearer <user_jwt> |
WHO is calling — user identity, permissions, tenant | Login flow + all payment/notification APIs |
X-Device-Token: <device_token> |
WHERE the call originates — device identity, state, config | Device-only endpoints (required) + payment APIs (optional enrichment) |
Step 1 — Registration#
Requires a user JWT (merchant user must be logged in via POST /api/v1/auth/login).
Authorization: Bearer <user_jwt>
X-Tenant-ID: your-tenant-idStep 2 — Device-Only Endpoints (/device/me, /device/parameters, /device/heartbeat)#
Require both headers — user JWT for identity + device token for device context.
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-idStore the
deviceTokenin secure on-device storage (Android Keystore / iOS Keychain). This token does not expire on a timer — it stays valid until the device is suspended, deleted, or re-paired.
Step 3 — Payment & Notification APIs (/transactions/*, /payment-links, etc.)#
Send both headers. The user JWT authenticates the call; the device token enriches it with device context (device-level credentials, limits, transaction type restrictions).
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-idImportant: If
X-Device-Tokenis included, it must be valid. An invalid or revoked device token returns401(a suspended device returns403 DEVICE_SUSPENDED) even if the user JWT is valid. Omit the header entirely for calls that do not originate from a registered device (e.g. Virtual Terminal use cases).On payment APIs the device token is checked for a live session and a non-suspended device only — the
ACTIVEstatus is not re-checked there, so gate payments in the app on a successfultest-connection/GET /device/session.
Header Summary by Endpoint#
| Endpoint | Authorization |
X-Device-Token |
Required Status |
|---|---|---|---|
GET /auth/my-merchants |
✅ Required (user JWT) | ❌ Not applicable | — |
POST /auth/switch-merchant |
✅ Required (user JWT) | ❌ Not applicable | — |
POST /device/register |
✅ Required (user JWT) | ❌ Not applicable | PENDING / REGISTERED / INACTIVE / ACTIVE (owner re-pair) |
POST /device/test-connection |
✅ Required (user JWT) | ✅ Required | REGISTERED |
GET /device/session |
✅ Required (user JWT) | ✅ Required | Any status |
GET /device/me |
✅ Required (user JWT) | ✅ Required | ACTIVE |
GET /device/parameters |
✅ Required (user JWT) | ✅ Required | ACTIVE |
GET /device/products |
✅ Required (user JWT) | ✅ Required | ACTIVE |
GET /device/heartbeat |
✅ Required (user JWT) | ✅ Required | ACTIVE |
POST /device/logout |
✅ Required (user JWT) | ✅ Required | ACTIVE |
POST /transactions/sale |
✅ Required | ⚠️ Optional (send for device context) | Not suspended |
POST /transactions/auth |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/token-sale |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/{id}/refund |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/{id}/refund/partial |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/{id}/void |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/{id}/capture |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/ach-sale |
✅ Required | ⚠️ Optional | Not suspended |
POST /transactions/external-payment |
✅ Required | ✅ Required | Not suspended |
GET /transactions |
✅ Required | ⚠️ Optional (filters to device's own transactions) | Not suspended |
GET /transactions/{id} |
✅ Required | ⚠️ Optional | Not suspended |
GET /transactions/fee-preview |
✅ Required | ❌ Not applicable | — |
POST /merchants/verify-pin |
✅ Required | ⚠️ Optional | Not suspended |
POST /payment-links, GET /payment-links |
✅ Required | ⚠️ Optional | Not suspended |
Endpoints#
List My Merchants#
Merchants the signed-in user can work in, each flagged with whether this user already has an ACTIVE device registered there. Use it to build the merchant picker.
Auth: User JWT. No device token.
| Query | Default | Description |
|---|---|---|
includeDeviceStatus |
true |
Set false to skip the per-merchant device lookup |
Response (200 OK):
{
"success": true,
"data": {
"currentMerchantId": "merchant_123",
"merchants": [
{
"merchantId": "merchant_123",
"companyName": "Acme Coffee",
"role": "merchant_admin",
"status": "active",
"deviceRegistration": {
"hasActiveDevice": true,
"activeDeviceCount": 1,
"device": {
"deviceId": "550e8400-e29b-41d4-a716-446655440001",
"deviceLabel": "Counter 1",
"tpnLast4": "4651",
"status": "ACTIVE",
"lastActiveAt": "2026-08-10T09:14:02.000Z"
}
}
}
]
}
}Devices registered by other staff at the same merchant are never counted here — a colleague's ACTIVE device does not make the merchant ready for this user.
hasActiveDeviceis a display hint for the picker badge, computed server-side. It can disagree with what the app has in secure storage (storage cleared, or the token revoked after the list was fetched). Always callGET /device/sessionafter switching to decide what to show.
Switch Merchant#
Changes the active merchant without logging out. Returns a fresh access token scoped to the new merchant — replace the stored userJWT with it.
Auth: User JWT. No device token.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
merchantId |
string | Yes | A merchant from GET /auth/my-merchants |
curl -X POST "https://uat.auxcore.net/api/v1/auth/switch-merchant" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{ "merchantId": "merchant_456" }'Response (200 OK): same shape as POST /auth/select-merchant.
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "Qm9vZ3VzUmVmcmVzaFRva2VuRXhhbXBsZQ...",
"tokenType": "Bearer",
"expiresIn": 86400,
"user": {
"id": "8d2f6b1e-4a7c-4c1e-9b3a-2f5e6d7c8a90",
"email": "cashier@example.com",
"fullName": "Jane Cashier",
"role": "merchant_admin",
"merchantId": "merchant_456",
"mfaEnabled": false,
"permissions": ["transactions.view", "transactions.refund"]
},
"features": { "invoicing": true },
"navigation": []
}
}token, refreshToken and tokenType are omitted when the deployment uses HttpOnly auth cookies — the tokens are then set as cookies instead. expiresIn is the access-token lifetime in seconds (24 hours by default). role is the role the user holds at the new merchant. permissions is omitted when empty.
| Status | Code | Meaning |
|---|---|---|
400 |
VALIDATION_ERROR |
merchantId missing or not a string |
401 |
UNAUTHORIZED |
The user has no access to that merchant, the merchant is closed, or the account cannot authenticate |
404 |
NOT_FOUND |
The user record no longer exists |
429 |
RATE_LIMITED |
More than 120 requests in 15 minutes from one IP (bucket shared with /auth/refresh and /auth/select-merchant) |
Error bodies use { "success": false, "error": { "code": "...", "message": "..." } }.
After switching, send the new userJWT on every subsequent call. Using the previous token returns DEVICE_CONTEXT_MISMATCH on device routes.
POST /auth/select-merchantis a different endpoint: it completes login and requires the 10-minuteselectionTokenreturned by/auth/login. It cannot be used to switch merchant later in the session.
Register Device#
Pairs the app to a device record provisioned in the portal. Call this once per device installation (or after a pairing code refresh).
Auth: User JWT (Authorization: Bearer <user_jwt>)
The merchant comes from the user JWT, so switch merchant before registering a device for another merchant. The user in that JWT is recorded as the device's owner:
- Only that user may register the device again while it is
ACTIVE— which is how an app recovers after losing its stored token. The pairing code is still required. - Any other user attempting to register an
ACTIVEdevice gets400 Device already registered. Ask admin to deactivate or refresh the pairing code. - Devices registered before this ownership tracking existed reject every re-registration while
ACTIVE, as before.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
tpn |
string | Yes | 12-digit numeric TPN provided by the merchant admin (validated as exactly 12 digits) |
pairingCode |
string | Yes | 6-digit pairing code provided by the merchant admin (must match the device record exactly) |
appVersion |
string | No | Your app's version string, e.g. "2.1.0" |
osVersion |
string | No | Device OS version, e.g. "14.0" |
osPlatform |
enum | No | ANDROID | IOS |
Example:
curl -X POST "https://uat.auxcore.net/api/v1/device/register" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"tpn": "483920174651",
"pairingCode": "619274",
"appVersion": "2.1.0",
"osVersion": "14.0",
"osPlatform": "ANDROID"
}'Response (201 Created) — Android + NearPay example:
{
"success": true,
"data": {
"device": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"tenantId": "your-tenant-id",
"merchantId": "merchant_123",
"branchId": "branch-456",
"deviceLabel": "Drive-Thru Terminal",
"deviceNumber": 1042,
"tpn": "483920174651",
"manufacturer": "ANDROID_PHONE",
"deviceType": "TAP_ON_PHONE",
"paymentMode": "NEARPAY",
"merchantGatewayId": null,
"gateway": null,
"templateId": null,
"mid": null,
"terminalId": null,
"iposTpn": null,
"iposTpnCode": null,
"nearpayTid": "TID-NP-001",
"sunmiSN": null,
"paymentAdjustmentType": "FLAT",
"paymentAdjustmentValue": "1.5000",
"paymentAdjustmentCardValue": "2.0000",
"paymentAdjustmentAchValue": "0.5000",
"autoSettleEnabled": true,
"settleAt": "17:00",
"status": "REGISTERED",
"skipTestConnection": false,
"pairingCode": "619274",
"appVersion": "2.2.0",
"osVersion": "14.0",
"osPlatform": "ANDROID",
"registeredAt": "2026-04-14T10:00:00.000Z",
"registeredByUserId": "8d2f6b1e-4a7c-4c1e-9b3a-2f5e6d7c8a90",
"assignedUserId": null,
"isShared": true,
"lastActiveAt": "2026-04-14T10:00:00.000Z",
"txCount": 0,
"txVolumeUsd": "0.00",
"createdBy": "3b9e1c22-7d4f-4e8a-a1b2-c3d4e5f6a7b8",
"updatedBy": null,
"createdAt": "2026-04-10T00:00:00.000Z",
"updatedAt": "2026-04-14T10:00:00.000Z",
"deletedAt": null
},
"deviceToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"jwtNearpayDevice": "eyJhbGciOiJSUzI1NiJ9...",
"nextStep": "test-connection",
"testConnectionRequired": true
}
}Device object fields:
| Field | Type | Notes |
|---|---|---|
id |
string (UUID) | Internal device ID |
tenantId |
string | Tenant identifier |
merchantId |
string | Merchant identifier |
branchId |
string | null | Branch this device belongs to; null = top-level merchant |
deviceLabel |
string | Human-readable label |
deviceNumber |
number | null | Per-merchant sequence number; the portal shows it as e.g. DEV-1042 |
tpn |
string | 12-digit Terminal Processor Number |
manufacturer |
string | Built-in ANDROID_PHONE | APPLE_PHONE | SUNMI, or the name of an equipment provider from the provider catalog |
deviceType |
string | Built-in TAP_ON_PHONE | CARD_READER | POS_TERMINAL, or a provider device model name |
paymentMode |
enum | AUXVAULT | IPOS | NEARPAY |
merchantGatewayId |
string (UUID) | null | Merchant gateway selected for this device |
gateway |
string | null | Provider code of the selected merchant gateway, e.g. TsysSierra, NMI, FluidPay; null when none is linked |
templateId |
string | null | Parameter template applied at creation |
mid |
string | null | Merchant ID / MID for gateway auth |
terminalId |
string | null | Terminal ID / TID for gateway auth |
iposTpn |
string | null | iPOSpays TPN — present when paymentMode = IPOS |
iposTpnCode |
string | null | iPOSpays secondary code — present when paymentMode = IPOS |
nearpayTid |
string | null | NearPay Terminal ID — present when paymentMode = NEARPAY |
sunmiSN |
string | null | Sunmi serial number — present when manufacturer = SUNMI |
paymentAdjustmentType |
enum | NONE | FLAT | PERCENTAGE |
paymentAdjustmentValue |
decimal string | null | Default/single adjustment value; null when type is NONE |
paymentAdjustmentCardValue |
decimal string | null | Card-specific adjustment value; null when type is NONE |
paymentAdjustmentAchValue |
decimal string | null | ACH-specific adjustment value; null when type is NONE |
autoSettleEnabled |
boolean | Whether auto batch settlement is configured |
settleAt |
string | null | Settlement time "HH:MM" (24h); null when disabled |
status |
enum | REGISTERED immediately after register (ACTIVE when skipTestConnection is true) |
skipTestConnection |
boolean | If true, device skips test-connection and is immediately ACTIVE |
pairingCode |
string | The pairing code used for registration |
appVersion |
string | null | App version sent during registration |
osVersion |
string | null | OS version sent during registration |
osPlatform |
enum | null | ANDROID | IOS |
registeredAt |
ISO timestamp | First registration time |
registeredByUserId |
string | null | User who registered the app on this device (the owner for re-registration rules) |
assignedUserId |
string | null | Team member the merchant assigned the device to; null while shared |
isShared |
boolean | Whether anyone at the location may use the device |
lastActiveAt |
ISO timestamp | Most recent activity |
txCount |
number | Total approved transactions |
txVolumeUsd |
decimal string | Total volume in USD |
createdBy / updatedBy |
string | null | Portal user who created / last edited the device record |
createdAt |
ISO timestamp | Device record creation time |
updatedAt |
ISO timestamp | Last update time |
deletedAt |
timestamp | null | Soft-delete timestamp; null = active |
The encrypted device API key is never returned.
jwtNearpayDevice: A signed RS256 JWT for the NearPay SDK. Present (non-null) only when paymentMode = "NEARPAY" and a Terminal ID is configured. Store it in secure storage alongside deviceToken and pass it to the NearPay SDK during initialization. null for all other payment modes.
Save the deviceToken immediately — store it in secure storage. Use it as the X-Device-Token header for the next required step: POST /device/test-connection.
⚠️ Device is NOT yet active.
status = REGISTEREDmeans the app has paired successfully, but the device must passPOST /device/test-connectionbefore it can process payments. Navigate to the Test Connection screen immediately after receiving this response.
Error Responses:
Errors from this endpoint use { "success": false, "message": "..." } (no error.code).
| HTTP | Message | Cause |
|---|---|---|
400 |
"tpn and pairingCode are required" |
A required field is missing |
400 |
"tpn must be a 12-digit numeric code" |
TPN is not exactly 12 digits |
400 |
"Invalid osPlatform value" |
osPlatform is not ANDROID or IOS |
400 |
"merchantId not found in token — merchant user login required" |
The user JWT is not scoped to a merchant — select or switch merchant first |
400 |
"Invalid TPN" |
TPN not found or does not belong to the authenticated user's merchant |
400 |
"Invalid pairing code" |
Pairing code does not match the device record |
400 |
"Device is not available for registration" |
Device is SUSPENDED or ARCHIVED — admin must take action |
400 |
"Device already registered. Ask admin to deactivate or refresh the pairing code." |
Device is ACTIVE and was registered by a different user (or has no recorded owner) |
500 |
"Registration failed" |
Unexpected server error |
Side effects:
- Device status transitions to
REGISTERED(not ACTIVE — must complete test-connection), or straight toACTIVEwhenskipTestConnectionis set (nextStep: "none",testConnectionRequired: false) registeredByUserIdis set to the calling user- Any existing active sessions are revoked immediately
registeredAtis set on first registration; preserved on re-registrationlastActiveAtupdated
Test Connection#
Validates that the merchant payment gateway configuration this device will use is set up correctly — there is an ACTIVE gateway of the device's type, with complete credentials accepted by the gateway adapter (plus a VAR sheet for TSYS). This is config validation only: no transaction is sent to the processor and no money moves. On success, the device transitions from REGISTERED to ACTIVE, enabling payment processing.
Must be called after every
POST /device/register(whentestConnectionRequired = true) before the device can process transactions.
Changed in v2.3.0 — this endpoint previously validated device-level credentials and pinged the processor. It now validates the merchant gateway config and returns a specific failure
reason. Error codes changed (see below).
Auth: User JWT + device token — both required.
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-idRequest Body: None.
Example:
curl -X POST "https://uat.auxcore.net/api/v1/device/test-connection" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"Response (200 OK — Success):
{
"success": true,
"message": "Connection validated successfully",
"data": {
"deviceId": "550e8400-e29b-41d4-a716-446655440001",
"status": "ACTIVE",
"gatewayType": "TsysSierra",
"configStatus": "VALID",
"processorStatus": "CONNECTED",
"validatedAt": "2026-06-25T10:30:00Z",
"testDurationMs": 142
},
"timestamp": "2026-06-25T10:30:00Z"
}| Field | Type | Notes |
|---|---|---|
gatewayType |
string | Gateway the device resolves to: IPOS for paymentMode = IPOS, NearPay for NEARPAY, otherwise the provider code stored on the device (gateway, e.g. TsysSierra, NMI, FluidPay; older devices may carry a legacy label). New in v2.3.0 |
configStatus |
"VALID" |
Gateway config is complete. New in v2.3.0 |
processorStatus |
"CONNECTED" |
Retained for backward compatibility with v2.2 clients |
Already active (idempotent): if the device is already ACTIVE, the endpoint returns 200 with alreadyActive: true (not an error) — safe for clients that always call it after registration.
{
"success": true,
"message": "Device is already active — no test-connection required",
"data": { "deviceId": "550e8400-...", "status": "ACTIVE", "alreadyActive": true }
}After success: Device is now ACTIVE. Navigate to the main dashboard — the device can process payments.
Error Responses:
| HTTP | Code | reason |
Cause | Retryable |
|---|---|---|---|---|
422 |
GATEWAY_NOT_CONFIGURED |
NO_ACTIVE_GATEWAY |
No active gateway of the device's type for this merchant | ❌ No |
422 |
GATEWAY_NOT_CONFIGURED |
NO_CREDENTIALS |
Gateway has no saved credentials | ❌ No |
422 |
GATEWAY_NOT_CONFIGURED |
INVALID_CREDENTIALS |
Credentials incomplete/invalid for the gateway | ❌ No |
422 |
GATEWAY_NOT_CONFIGURED |
MISSING_VAR_SHEET |
TSYS gateway has no VAR sheet configured | ❌ No |
422 |
GATEWAY_NOT_CONFIGURED |
INVALID_GATEWAY_CONFIG |
Gateway config rejected for any other reason | ❌ No |
400 |
GATEWAY_NOT_SUPPORTED |
— | No payment adapter for the device's resolved gateway | ❌ No |
400 |
NO_DEVICE_GATEWAY |
— | AUXVAULT device has no merchant gateway linked — an admin must assign one. The body says retryable: true, but retrying without that fix will not succeed |
❌ No |
403 |
DEVICE_NOT_REGISTERED |
— | Device is PENDING, INACTIVE or otherwise not REGISTERED — call /register first |
❌ No |
403 |
DEVICE_CONTEXT_MISMATCH |
— | The device token belongs to a different tenant/merchant than the user JWT | ❌ No |
404 |
DEVICE_NOT_FOUND |
— | Device not found for this tenant/merchant | ❌ No |
503 |
PROCESSOR_UNREACHABLE |
— | The validation service was unreachable | ✅ Yes |
500 |
INTERNAL_SERVER_ERROR |
— | Unexpected backend error (body also carries requestId, supportEmail, durationMs) |
✅ Yes |
DEVICE_NOT_REGISTERED and DEVICE_CONTEXT_MISMATCH come from the route guard and use the nested shape { "success": false, "error": { "code": "DEVICE_NOT_REGISTERED", "message": "...", "currentStatus": "PENDING" } }. All other errors use the flat shape below.
422 GATEWAY_NOT_CONFIGURED body example:
{
"success": false,
"error": "GATEWAY_NOT_CONFIGURED",
"message": "No active TsysSierra gateway is configured for this merchant",
"userMessage": "No active TsysSierra gateway is configured for this merchant",
"code": 422,
"retryable": false,
"reason": "NO_ACTIVE_GATEWAY",
"gatewayType": "TsysSierra",
"timestamp": "2026-06-25T10:30:00Z"
}Retry guidance:
GATEWAY_NOT_CONFIGURED(422) is not retryable by the app — an administrator must fix the merchant gateway configuration in the portal (add the VAR sheet, set credentials, activate a gateway). Display themessageand a "contact your administrator" hint.PROCESSOR_UNREACHABLE(503) is retryable — show a Retry button; retry after 10–30 seconds.- Always display the
requestId(if present) for support reference. It is echoed from yourX-Request-IDheader on500responses, so send one.
Side effects:
- On success: device status transitions from
REGISTERED→ACTIVE. - Every attempt (pass or fail) is logged in the
device_test_attemptstable.
NearPay devices (
paymentMode = NEARPAY) are validated locally: NearPay settles through the device SDK, so there is no server-side gateway credential. Config is valid when the device has a Terminal ID (nearpayTid) and the server can mint the NearPay JWT — otherwise a422 GATEWAY_NOT_CONFIGUREDis returned.
Check Device Session#
Answers "can I keep using the device token I already stored for this merchant?" Call it when entering a merchant — on app start, after sign-in, and after every merchant switch — so the app knows whether to skip Device Registration.
Auth: User JWT + device token — both required. The merchant comes from the user JWT; the device comes from the device token.
Unlike /device/me and /device/parameters, this endpoint has no ACTIVE requirement. A device that can no longer be used is a normal 200 answer, not a 403, so the app can branch on the result instead of parsing an error.
Response (200 OK) — reusable:
{
"success": true,
"data": {
"reusable": true,
"status": "ACTIVE",
"device": { "id": "550e8400-...", "deviceLabel": "Drive-Thru Terminal", "status": "ACTIVE", "...": "full device object, as in /device/register" }
}
}Response (200 OK) — not reusable:
{
"success": true,
"data": {
"reusable": false,
"reason": "NOT_ACTIVE",
"status": "INACTIVE",
"device": { "id": "550e8400-...", "status": "INACTIVE" }
}
}reason |
Meaning | App action |
|---|---|---|
NOT_ACTIVE |
Device was unregistered, deactivated, suspended or archived | Show Device Registration |
CONTEXT_MISMATCH |
This token belongs to a different merchant | Use the token stored for the current merchant, or register |
REGISTERED_BY_ANOTHER_USER |
The device is owned by a different user | Show Device Registration; the other user must unregister or an admin must deactivate |
DEVICE_NOT_FOUND |
The device record no longer exists | Delete the stored token and show Device Registration |
device is the full device object for reusable: true and NOT_ACTIVE, and null for CONTEXT_MISMATCH, REGISTERED_BY_ANOTHER_USER and DEVICE_NOT_FOUND. status is the device's current status in every case except DEVICE_NOT_FOUND, where it is null.
A user JWT that is not scoped to a merchant returns 400 with "merchantId not found in token — merchant user login required".
Two cases are answered before the handler runs, and the app must treat both exactly like reusable: false — drop the stored token and show Device Registration:
401 Unauthorized— the device token is missing, invalid or revoked (for example after an admin deactivation, an archive, or a pairing-code refresh).403 DEVICE_SUSPENDED— the device was suspended by an admin.
curl "https://uat.auxcore.net/api/v1/device/session" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"Get Current Device#
Returns the current device record for the authenticated device. Use this to verify connectivity and device metadata on app startup.
Auth: User JWT + device token — both required.
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-idExample:
curl "https://uat.auxcore.net/api/v1/device/me" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"Response (200 OK):
The response spreads all device fields directly into data, plus nearpayJWT at the same level.
⚠️ Note on NearPay JWT key name:
POST /device/registerreturns the NearPay JWT asdata.jwtNearpayDevice. This endpoint returns it asdata.nearpayJWT. Both carry the same RS256-signed token.
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"tenantId": "your-tenant-id",
"merchantId": "merchant_123",
"branchId": "branch-456",
"deviceLabel": "Drive-Thru Terminal",
"deviceNumber": 1042,
"tpn": "483920174651",
"manufacturer": "ANDROID_PHONE",
"deviceType": "TAP_ON_PHONE",
"paymentMode": "NEARPAY",
"merchantGatewayId": null,
"gateway": null,
"templateId": null,
"mid": null,
"terminalId": null,
"iposTpn": null,
"iposTpnCode": null,
"nearpayTid": "TID-NP-001",
"sunmiSN": null,
"paymentAdjustmentType": "FLAT",
"paymentAdjustmentValue": "1.5000",
"paymentAdjustmentCardValue": "2.0000",
"paymentAdjustmentAchValue": "0.5000",
"autoSettleEnabled": true,
"settleAt": "17:00",
"status": "ACTIVE",
"skipTestConnection": false,
"pairingCode": "619274",
"appVersion": "2.2.0",
"osVersion": "14.0",
"osPlatform": "ANDROID",
"registeredAt": "2026-04-14T10:00:00.000Z",
"registeredByUserId": "8d2f6b1e-4a7c-4c1e-9b3a-2f5e6d7c8a90",
"assignedUserId": null,
"isShared": true,
"lastActiveAt": "2026-06-16T10:30:00.000Z",
"txCount": 42,
"txVolumeUsd": "3810.00",
"createdBy": "3b9e1c22-7d4f-4e8a-a1b2-c3d4e5f6a7b8",
"updatedBy": null,
"createdAt": "2026-04-10T00:00:00.000Z",
"updatedAt": "2026-06-16T10:30:00.000Z",
"deletedAt": null,
"nearpayJWT": "eyJhbGciOiJSUzI1NiJ9..."
}
}Key fields reference:
| Field | Type | Description |
|---|---|---|
manufacturer |
string | Built-in ANDROID_PHONE | APPLE_PHONE | SUNMI, or an equipment provider name |
deviceType |
string | Built-in TAP_ON_PHONE | CARD_READER | POS_TERMINAL, or a provider device model name |
paymentMode |
enum | AUXVAULT | IPOS | NEARPAY — the payment gateway type |
gateway |
string | null | Provider code of the linked merchant gateway (e.g. TsysSierra); null when none is linked |
mid |
string | null | Merchant ID / MID for gateway credential validation |
terminalId |
string | null | Terminal ID / TID for gateway credential validation |
iposTpn |
string | null | iPOSpays TPN — present when paymentMode = IPOS |
iposTpnCode |
string | null | iPOSpays secondary code — present when paymentMode = IPOS |
nearpayTid |
string | null | NearPay Terminal ID — present when paymentMode = NEARPAY |
sunmiSN |
string | null | Sunmi serial number — present when manufacturer = SUNMI |
paymentAdjustmentType |
enum | NONE | FLAT | PERCENTAGE |
paymentAdjustmentValue |
decimal string | null | Default/single adjustment amount; null when type is NONE |
paymentAdjustmentCardValue |
decimal string | null | Card-specific adjustment amount; null when type is NONE |
paymentAdjustmentAchValue |
decimal string | null | ACH-specific adjustment amount; null when type is NONE |
autoSettleEnabled |
boolean | Whether auto batch settlement is configured |
settleAt |
string | null | Settlement time "HH:MM" (24h); null when disabled |
skipTestConnection |
boolean | Whether this device skips the test-connection step on registration |
nearpayJWT |
string | null | RS256 JWT for NearPay SDK initialization; non-null only when paymentMode = NEARPAY and TID is configured |
Get Device Parameters#
Loads the full configuration for this device. Call this on app startup and whenever the heartbeat reports a new paramVersion.
Auth: User JWT + device token — both required. Device must be ACTIVE.
Example:
curl "https://uat.auxcore.net/api/v1/device/parameters" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"Response (200 OK):
{
"success": true,
"data": {
"version": 7,
"signatureThreshold": "0.00",
"isViewAllMerchantTransaction": false,
"transactions": {
"saleEnabled": true,
"refundEnabled": true,
"voidEnabled": true,
"preauthEnabled": false
},
"requirePin": {
"pinVoidEnabled": false,
"pinRefundEnabled": true
},
"paymentTypes": {
"tapToPayEnabled": true,
"keyedInEnabled": false,
"qrCodeEnabled": false,
"paymentLinkEnabled": false,
"achTransferEnabled": false,
"cashEnabled": true
},
"limits": {
"perTransaction": "500.00",
"daily": "5000.00",
"weekly": null,
"monthly": null
},
"receipt": {
"headerMsg": "Welcome!",
"footerMsg": "Thank you!",
"disclaimer": null,
"autoPrintEnabled": true,
"printMerchantCopy": true,
"smsEnabled": true,
"emailEnabled": true,
"signatureCapture": false
},
"posLite": {
"enabled": true
},
"productCart": {
"enabled": true
},
"tips": {
"allowTips": true,
"presets": [10, 15, 20, 25],
"maxAmount": "100.00",
"maxPercentage": 30
},
"tax": {
"enabled": true,
"enableRemove": false,
"removePasswordProtected": false,
"tax1": { "label": "Sales Tax", "value": "8.25" },
"tax2": { "label": null, "value": null }
},
"inputFields": [
{
"key": "billing_name",
"label": "Cardholder Name",
"group": "general",
"type": "text",
"display": true,
"required": true,
"order": 1
}
],
"shipping": {
"enabled": false,
"sameAsBillingVisible": false
}
}
}Parameter Sections:
| Section | Description |
|---|---|
version |
Monotonically increasing integer; compare to locally cached version to detect changes |
signatureThreshold |
Decimal string; amount above which signature capture is required ("0.00" = always) |
isViewAllMerchantTransaction |
Whether the cashier can view all merchant transactions (not just this device's) |
transactions |
Which transaction types the device may perform |
requirePin |
Whether a merchant PIN is required to authorize void or refund |
paymentTypes |
Accepted payment input methods |
limits |
Per-transaction and rolling amount limits (null = no limit) |
receipt |
Receipt delivery and display options |
tips |
Tip prompting configuration |
tax |
Tax calculation and display |
inputFields |
Which billing/AVS fields to show and which are required |
shipping |
Whether to show shipping address fields |
Returns 404 with "Device parameters not found" when the device has no parameter record.
Heartbeat#
A lightweight keep-alive that returns the current parameter version. Poll this to detect config changes without fetching the full parameter payload.
Auth: User JWT + device token — both required. Device must be ACTIVE.
Example:
curl "https://uat.auxcore.net/api/v1/device/heartbeat" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"Response (200 OK):
{
"success": true,
"data": {
"paramVersion": 7,
"serverTime": "2026-04-14T10:30:00.000Z"
}
}Usage pattern:
App startup:
1. GET /device/parameters → store locally with version = 7
Every N minutes:
2. GET /device/heartbeat → returns { paramVersion: 8 }
3. If paramVersion != local version:
GET /device/parameters → refresh local configRecommended heartbeat interval: Every 60–120 seconds when the app is in the foreground; longer when in the background.
Side effect: Updates lastUsedAt on the device session (fire-and-forget, non-blocking).
paramVersion is null when the device has no parameter record.
Unregister Device (/device/logout)#
Releases this device from the merchant: the session is revoked and the device becomes INACTIVE. Call it only when the user deliberately unregisters the device, or when the app is being reset.
⚠️ Despite the endpoint name, this is not user sign-out. Signing a user out must not call this endpoint. Sign-out ends the user session only — the device stays
ACTIVEand the app keeps its storedmerchantId → deviceTokenmap, so the next login re-enters the merchant without a TPN prompt (see Multi-merchant device storage).After unregistering, also delete that merchant's entry from the stored map. The device can be registered again immediately with the same TPN + pairing code — no admin action required.
Auth: User JWT + device token — both required.
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-idExample:
curl -X POST "https://uat.auxcore.net/api/v1/device/logout" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"Response (200 OK):
{
"success": true,
"data": {
"message": "Device logged out"
}
}After calling this endpoint:
- Clear the stored
deviceTokenfrom secure storage. - The device status is now
INACTIVE— the app cannot process payments until re-registered. - To reactivate: log in (
POST /auth/login) and callPOST /device/registeragain with the same TPN + pairing code. No admin action is required.
Note: If you want to fully disable the device and prevent re-registration, ask an admin to suspend the device from the portal instead.
Error Responses:
| HTTP | Message | Cause |
|---|---|---|
401 |
UNAUTHORIZED |
Missing, invalid, or already-revoked X-Device-Token (nested error body) |
403 |
DEVICE_NOT_ACTIVE |
Device is not ACTIVE (route guard; nested error body with currentStatus) |
403 |
DEVICE_SUSPENDED |
Device suspended by an admin |
403 |
DEVICE_CONTEXT_MISMATCH |
Device token belongs to a different merchant than the user JWT |
400 |
"Device is not active" |
Flat { success, message } body; only reachable if the status changes between the guard and the update |
Side effects:
- Device status transitions to
INACTIVE - Current device session is revoked immediately (
DEVICE_LOGOUT)
Token Lifecycle#
When the Token is Revoked#
The device JWT becomes invalid in the following situations:
| Reason | Who Triggers It | Action Required |
|---|---|---|
NEW_REGISTRATION |
New registration on same device | Re-register if this device was displaced |
PAIRING_REFRESHED |
Admin refreshes pairing code | Obtain new TPN + pairing code from admin and re-register |
SUSPENDED |
Admin suspends the device, or moves it out of ACTIVE to any status (e.g. INACTIVE) |
Contact admin, or re-register once the device is registrable again |
DELETED |
Admin deletes the device | Contact admin |
DEVICE_LOGOUT |
App calls POST /device/logout |
Re-register with same TPN + pairing code (no admin action needed) |
ARCHIVED |
Admin archives the device | Contact admin — an unarchived device returns as INACTIVE and must be re-registered |
An admin deactivating a device now revokes its sessions immediately — the stored token stops working the moment the device leaves
ACTIVE, it is not merely blocked at the route.
Detecting Revocation#
When the token is revoked, all device API calls (/device/session, /device/me, /device/parameters, /device/heartbeat, /device/logout, /device/test-connection) return:
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Device token has been revoked"
}
}message is one of "Device token is required in X-Device-Token header", "Invalid device token", "Invalid token type", "Device session not found", "Device token has been revoked" or "Device not found".
HTTP Status: 401 Unauthorized
Your app should catch 401 responses on device endpoints, clear the stored deviceToken, and present the registration screen to the operator.
For payment APIs (/transactions/*, etc.) where X-Device-Token was included, a revoked or unknown device token returns the same 401 UNAUTHORIZED body as above. A token that cannot be decoded at all returns:
{
"success": false,
"error": {
"code": "INVALID_DEVICE_TOKEN",
"message": "Device token is malformed"
}
}If the device token cannot be checked because the merchant service is unreachable, payment APIs return 503 SERVICE_UNAVAILABLE ("Could not validate device token") — retry. POST /merchants/verify-pin reports every invalid device token as 401 INVALID_DEVICE_TOKEN.
HTTP Status: 401 Unauthorized — treat the same way: clear token, show registration screen.
Implementation Guide#
Android#
// Store device token securely
val sharedPreferences = EncryptedSharedPreferences.create(
"device_prefs",
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences.edit().putString("device_token", deviceToken).apply()
// Device-only endpoints (me, parameters, heartbeat) — use both headers
val userJwt = sharedPreferences.getString("user_jwt", null)
val deviceToken = sharedPreferences.getString("device_token", null)
val deviceRequest = Request.Builder()
.url(deviceEndpointUrl)
.addHeader("Authorization", "Bearer $userJwt")
.addHeader("X-Device-Token", deviceToken ?: "")
.addHeader("X-Tenant-ID", tenantId)
.build()
// Payment/notification endpoints — use both headers
val userJwt = sharedPreferences.getString("user_jwt", null)
val paymentRequest = Request.Builder()
.url(transactionUrl)
.addHeader("Authorization", "Bearer $userJwt")
.addHeader("X-Device-Token", deviceToken ?: "")
.addHeader("X-Tenant-ID", tenantId)
.build()iOS (Swift)#
// Store device token in Keychain
let tokenData = deviceToken.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "device_token",
kSecValueData as String: tokenData
]
SecItemAdd(query as CFDictionary, nil)
// Device-only endpoints (me, parameters, heartbeat) — use both headers
var deviceRequest = URLRequest(url: deviceEndpointURL)
deviceRequest.setValue("Bearer \(userJwt)", forHTTPHeaderField: "Authorization")
deviceRequest.setValue(deviceToken, forHTTPHeaderField: "X-Device-Token")
deviceRequest.setValue(tenantId, forHTTPHeaderField: "X-Tenant-ID")
// Payment/notification endpoints — use both headers
var paymentRequest = URLRequest(url: transactionURL)
paymentRequest.setValue("Bearer \(userJwt)", forHTTPHeaderField: "Authorization")
paymentRequest.setValue(deviceToken, forHTTPHeaderField: "X-Device-Token")
paymentRequest.setValue(tenantId, forHTTPHeaderField: "X-Tenant-ID")Startup Checklist#
Implement the following sequence when your app launches:
1. Read userJWT + devices{} map from secure storage
└─ userJWT not found / expired → show Login screen
└─ Resolve the active merchantId M from the userJWT
2. GET /device/session (can I reuse the token stored for M?)
Headers: Authorization: Bearer <userJWT> + X-Device-Token: devices[M]
└─ devices[M] missing → show Registration screen (enter TPN + pairing code)
└─ 401 on userJWT → refresh JWT first (POST /auth/refresh), retry
└─ 401 / 403 on device → delete devices[M], show Registration screen
└─ reusable: false → delete devices[M], show Registration screen
(reason NOT_ACTIVE / CONTEXT_MISMATCH /
REGISTERED_BY_ANOTHER_USER / DEVICE_NOT_FOUND)
└─ reusable: true → continue to step 3, do NOT prompt for TPN
3. GET /device/parameters (load or refresh config)
Headers: Authorization: Bearer <userJWT> + X-Device-Token: devices[M]
└─ Store paramVersion locally
4. Refresh userJWT if needed
└─ POST /auth/refresh with refreshToken
└─ The refreshed token keeps merchant M — no need to re-select
└─ Store new userJWT in secure storage
5. Start heartbeat loop (every 60–120s)
Headers: Authorization: Bearer <userJWT> + X-Device-Token: devices[M]
└─ GET /device/heartbeat
└─ If paramVersion changed → GET /device/parameters
└─ If 401 → delete devices[M], prompt re-registration
└─ If 403 DEVICE_NOT_ACTIVE → navigate to Test Connection screen
6. For payment operations (transactions, payment-links):
└─ Send both: Authorization: Bearer <userJWT> + X-Device-Token: devices[M]
└─ If 401 UNAUTHORIZED / INVALID_DEVICE_TOKEN on the device token → delete devices[M], prompt re-registration
└─ If 401 on userJWT (TOKEN_EXPIRED) → refresh JWT, retry once
└─ If 403 DEVICE_SUSPENDED → show suspended screen
└─ If 503 SERVICE_UNAVAILABLE → device check unavailable, retry
7. On merchant switch:
└─ GET /auth/my-merchants (list + hasActiveDevice badge per merchant)
└─ POST /auth/switch-merchant → store the new userJWT, M becomes the new merchant
└─ Go back to step 2 with devices[new M]
└─ Do NOT touch the device of the merchant being left — it stays ACTIVE
8. On user sign-out:
└─ Clear userJWT + refreshToken only
└─ KEEP the devices{} map — signing out does not release any device
└─ Do NOT call POST /device/logout
└─ Navigate to Login screen
(next login re-enters each merchant without a TPN prompt)
9. On "Unregister this device" (explicit user action only):
└─ POST /device/logout (both headers — requires ACTIVE status)
└─ Delete devices[M] from the map
└─ (Device is now INACTIVE — re-register with the same TPN + pairing code to reactivate)Steps 8 and 9 replace the old single "On user logout" step. Previously the checklist told the app to call
/device/logoutand clear the token at sign-out; doing that now breaks the reuse behaviour, because the device drops toINACTIVEand the next login has to ask for the TPN again.
Entering a merchant (first time and every time after)#
The same path runs on first launch, after sign-in, and after every merchant switch. Only the left branch prompts for a TPN.
App start / sign-in / merchant switch
│
▼
POST /auth/login (+ /auth/select-merchant if multi-merchant)
or POST /auth/switch-merchant → userJWT scoped to merchant M
│
▼
devices[M] in secure storage?
│
┌────┴─────────────────────┐
no yes
│ │
│ ▼
│ GET /device/session
│ │
│ ┌──────────────┴──────────────┐
│ reusable: false reusable: true
│ 401 / 403 │
│ │ │
│ delete devices[M] │
▼ ▼ │
Registration screen → enter TPN + pairing code
│ │
▼ │
POST /device/register │
│ │
├─ Error → show message + retry │
│ │
▼ │
nextStep == "test-connection" ? │
│ │
┌────┴────┐ │
yes no │
│ │ │
▼ │ │
POST /device/test-connection │
│ │ │
┌─┴──┐ │ │
Pass Fail │ │
│ └→ show error + Retry │
▼ │ │
status = ACTIVE │
│ │ │
└────┬────┘ │
▼ │
store devices[M] = deviceToken │
│ │
└──────────────┬───────────────────────┘
▼
Main Dashboard (ready for payments)Error Reference#
Standard Error Format#
Device controller errors (register, me, parameters, heartbeat, logout, session) use a flat body:
{
"success": false,
"message": "Human-readable description"
}Route guards (device token, device status, user JWT) and the transaction service use a nested body — branch on error.code:
{
"success": false,
"error": { "code": "DEVICE_NOT_ACTIVE", "message": "Device is not yet activated. Complete test-connection first." }
}POST /device/test-connection uses its own flat body with error, message, code (HTTP status) and retryable — see Test Connection.
Common Errors#
| HTTP | Code | Message | Recommended Action |
|---|---|---|---|
400 |
— | "tpn must be a 12-digit numeric code" |
TPN must be exactly 12 digits |
400 |
— | "Invalid TPN" |
TPN not found or does not belong to the merchant in the user JWT |
400 |
— | "Invalid pairing code" |
Verify the 6-digit code with admin |
400 |
— | "Device is not available for registration" |
Device is SUSPENDED or ARCHIVED — contact admin |
400 |
— | "Device already registered..." |
Device is ACTIVE — ask admin to deactivate or refresh pairing code |
422 |
GATEWAY_NOT_CONFIGURED |
"No active TsysSierra gateway is configured for this merchant" |
test-connection: merchant gateway config incomplete. reason ∈ NO_ACTIVE_GATEWAY | NO_CREDENTIALS | INVALID_CREDENTIALS | MISSING_VAR_SHEET | INVALID_GATEWAY_CONFIG — admin must fix in portal (not app-retryable) |
400 |
GATEWAY_NOT_SUPPORTED |
"Gateway <gateway> is not supported" |
test-connection: the device's resolved gateway has no payment adapter — contact admin |
401 |
UNAUTHORIZED |
"Device token has been revoked" (or another device-token message) |
Device token revoked — clear deviceToken, show registration screen |
401 |
INVALID_DEVICE_TOKEN |
"Device token is malformed" |
Sent an undecodable X-Device-Token on a payment API (verify-pin also uses this code for invalid/revoked tokens) — clear deviceToken, re-register |
401 |
TOKEN_EXPIRED |
"Access token has expired. Please refresh your token." |
User JWT expired — call POST /auth/refresh, retry |
403 |
DEVICE_SUSPENDED |
"This device has been suspended" |
Contact admin |
403 |
DEVICE_NOT_REGISTERED |
"Device has not been registered yet" |
Call POST /device/register first, then retry test-connection |
403 |
DEVICE_CONTEXT_MISMATCH |
"Device token does not match authenticated merchant context" |
Use the device token stored for the current merchant (or switch merchant) |
403 |
DEVICE_NOT_ACTIVE |
"Device is not yet activated. Complete test-connection first." |
Navigate to Test Connection screen, call POST /device/test-connection |
404 |
— | "Device not found" |
Token is valid but device was deleted — contact admin |
200 |
alreadyActive |
"Device is already active — no test-connection required" |
Calling test-connection on an already-ACTIVE device returns 200 (idempotent, not an error) — go to dashboard |
422 |
LIMIT_EXCEEDED |
"Amount exceeds device per-transaction limit of $500.00" |
Amount exceeds the device's per-transaction limit, or would exceed its daily / weekly / monthly limit ("Amount would exceed device daily limit of $5000") |
403 |
TX_TYPE_DISABLED |
"sale is not enabled on this device" |
Transaction type (sale, auth, refund, void) disabled in device parameters — update via portal |
403 |
PAYMENT_TYPE_DISABLED |
"ACH transfers are not enabled on this device" |
ACH sale attempted on a device with achTransferEnabled: false |
400 |
DEVICE_CONTEXT_REQUIRED |
"A valid X-Device-Token is required for this endpoint" |
POST /transactions/external-payment called without a device token |
400 |
— | "Device is not active" |
Calling POST /device/logout on a device that is not ACTIVE |
503 |
PROCESSOR_UNREACHABLE |
"Failed to connect to payment processor" |
test-connection: the validation service was unreachable — retry after a short wait |
500 |
— | Server error | Retry with exponential backoff; alert if persistent |
Device Parameter Reference#
Top-level fields#
| Field | Type | Description |
|---|---|---|
version |
number | Parameter version. Increment detected via /device/heartbeat |
isViewAllMerchantTransaction |
boolean | If true, the cashier can see all merchant transactions, not only this device's |
signatureThreshold |
decimal string | Amount above which a signature is required; "0.00" = always require |
transactions Section#
| Field | Type | Description |
|---|---|---|
saleEnabled |
boolean | Whether Sale transactions are permitted |
refundEnabled |
boolean | Whether Refund transactions are permitted |
voidEnabled |
boolean | Whether Void transactions are permitted |
preauthEnabled |
boolean | Whether Pre-authorization is permitted |
requirePin Section#
| Field | Type | Description |
|---|---|---|
pinVoidEnabled |
boolean | Whether a merchant PIN must be entered to authorize a Void |
pinRefundEnabled |
boolean | Whether a merchant PIN must be entered to authorize a Refund |
paymentTypes Section#
| Field | Type | Description |
|---|---|---|
tapToPayEnabled |
boolean | NFC / Tap-to-pay |
keyedInEnabled |
boolean | Manual card entry |
qrCodeEnabled |
boolean | QR code payment |
paymentLinkEnabled |
boolean | Send-a-link payment |
achTransferEnabled |
boolean | ACH bank transfer |
cashEnabled |
boolean | Cash / check payment |
limits Section#
All values are decimal strings (e.g. "500.00") or null (no limit).
| Field | Description |
|---|---|
perTransaction |
Maximum single transaction amount |
daily |
Maximum cumulative daily volume |
weekly |
Maximum cumulative weekly volume |
monthly |
Maximum cumulative monthly volume |
tips Section#
| Field | Type | Description |
|---|---|---|
allowTips |
boolean | Whether tip prompt is shown |
presets |
number[] | null | Preset tip percentages (e.g. [10, 15, 20, 25]) |
maxAmount |
decimal string | Maximum tip amount in dollars |
maxPercentage |
number | Maximum tip percentage |
tax Section#
| Field | Type | Description |
|---|---|---|
enabled |
boolean | Whether tax is calculated |
enableRemove |
boolean | Whether cashier can remove tax |
removePasswordProtected |
boolean | Whether removing tax requires a password |
tax1.label |
string | Primary tax label (e.g. "Sales Tax") |
tax1.value |
decimal string | Primary tax rate as percentage (e.g. "8.25") |
tax2.label |
string | null | Secondary tax label (optional) |
tax2.value |
decimal string | null | Secondary tax rate (optional) |
receipt Section#
| Field | Type | Description |
|---|---|---|
headerMsg |
string | null | Custom header printed on receipt |
footerMsg |
string | null | Custom footer printed on receipt |
disclaimer |
string | null | Legal disclaimer text |
autoPrintEnabled |
boolean | Automatically print the receipt after a successful payment. false = do not print automatically (the cashier may still trigger a manual print). Defaults to true |
printMerchantCopy |
boolean | Whether to print a merchant copy |
smsEnabled |
boolean | Offer SMS receipt |
emailEnabled |
boolean | Offer email receipt |
signatureCapture |
boolean | Whether to capture digital signature |
posLite Section#
| Field | Type | Description |
|---|---|---|
enabled |
boolean | POS Lite — show the product cart (cart icon, product tiles, misc item) on this device. false = hide it; the app takes a bare amount as before. Defaults to true. A display switch only: the API does not refuse an itemised sale from a device that has it off. |
Configured per device by the merchant or an admin (Locations & Devices → device → Edit Parameters →
Transactions tab → Device → POS Lite). Changing it bumps version, so the heartbeat tells the app
to re-fetch.
productCart carries the same enabled value under the section's former name. It is deprecated:
read posLite; productCart is kept only for app builds released before the rename.
inputFields Section#
Each entry in inputFields describes a field on the payment form:
| Property | Type | Description |
|---|---|---|
key |
string | Unique field identifier |
label |
string | Display label |
group |
enum | general | avs | transaction | security | shipping |
type |
enum | text | email | phone | textarea | checkbox |
display |
boolean | Whether to show this field |
required |
boolean | Whether the field is mandatory |
order |
number | Render order (ascending) |
locked |
boolean | Optional. true when the merchant's Field Config requires the field — do not let the cashier change it |
Only render fields where display: true. Enforce required: true fields on form submission.
When the merchant saves their Field Config, the device column (terminals) or mobile column (phones) is applied
here: a required field becomes display: true, required: true, locked: true, a field switched off becomesdisplay: false. Billing Address drives the avs group; Shipping Address is not applicable to devices and leaves
the shipping group as configured per device.
Payment Flow by Device Type#
The combination of manufacturer, deviceType, and paymentMode (from /device/register or /device/me) determines the payment UI and SDK behavior:
| manufacturer | deviceType | paymentMode | CVV required? | Payment SDK | Notes |
|---|---|---|---|---|---|
ANDROID_PHONE |
TAP_ON_PHONE |
NEARPAY |
❌ No | NearPay Android SDK | NFC tap-to-pay; use nearpayTid + NearPay JWT |
APPLE_PHONE |
TAP_ON_PHONE |
IPOS |
❌ No | iPOSpays SDK | Apple flow via iposTpn |
SUNMI |
CARD_READER |
AUXVAULT |
✅ Yes | AuxCORE SDK | Card entered on Sunmi hardware |
SUNMI |
POS_TERMINAL |
NEARPAY |
❌ No | NearPay Android SDK | NFC tap on Sunmi POS; use nearpayTid + NearPay JWT |
SUNMI |
POS_TERMINAL |
AUXVAULT |
✅ Yes | AuxCORE SDK | Keyed entry on Sunmi POS |
NearPay SDK initialization#
When paymentMode = "NEARPAY":
- From
POST /device/register→ readdata.jwtNearpayDevice. - From
GET /device/me→ readdata.nearpayJWT(same RS256 token, different key name). - Read
nearpayTidfrom the device object. - Initialize:
NearPay.init(tid: nearpayTid, jwt: <jwtNearpayDevice|nearpayJWT>).
Persist whichever JWT you receive and reuse it until it expires or the device re-registers.
Price adjustment display#
When paymentAdjustmentType != "NONE", call GET /transactions/fee-preview before showing the price — always display the server-computed amount, never calculate the adjustment in the app. See Dual pricing.
Payment & Notification APIs from Device#
After device registration, the mobile app calls standard payment APIs using both headers. The server uses the device token to apply device-level rules and record which device processed each transaction.
Calling a Transaction#
curl -X POST "https://uat.auxcore.net/api/v1/transactions/sale" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "merchant_123",
"amount": 125.00,
"currency": "USD",
"pciToken": "pci_tok_9f2c4e7a1b3d"
}'When X-Device-Token is present, the server:
- Enforces device parameter limits (
perTransaction, plusdaily/weekly/monthlyvolume) —422 LIMIT_EXCEEDED - Validates the transaction type is enabled on this device (
saleEnabled,preauthEnabled,refundEnabled,voidEnabled) —403 TX_TYPE_DISABLED - Rejects ACH sales when
achTransferEnabledis off —403 PAYMENT_TYPE_DISABLED - Records the
deviceIdand the device's location (branchId,branchName) on the transaction for audit and reporting
Gateway routing still uses the merchant's gateway configuration; the device's mid / terminalId are only stamped on transactions recorded through POST /transactions/external-payment.
Cash Sale#
A cash (or check) sale is a normal sale with paymentMethod: "Cash" (or "Check", case-insensitive). It skips gateway routing and pricing and is recorded immediately as settled.
curl -X POST "https://uat.auxcore.net/api/v1/transactions/sale" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "merchant_123",
"amount": 21.65,
"baseAmount": 20.00,
"taxAmount": 1.65,
"tipAmount": 0,
"currency": "USD",
"paymentMethod": "Cash"
}'- Only
saleis supported for cash —POST /transactions/authwithpaymentMethod: "Cash"returns400 VALIDATION_ERROR. - The record gets
transactionIdCASH-<timestamp>-<suffix>,status: "settled",gatewayName: "Cash", and no convenience fee.baseAmountdefaults toamount - tipAmount - taxAmount. - Cash transactions cannot be voided (
400 "Void is not supported for Cash transactions. Please issue a refund instead."). Refund them withPOST /transactions/:id/refund— full refund only; a smalleramountreturns400 "Cash refunds support full refund only".
Record External SDK Payment#
For IPOS and NEARPAY devices the card is charged by the provider SDK on the device, not by AuxCORE. After the SDK approves, record the platform fee breakdown (which the provider webhook never sees) with this endpoint.
Auth: User JWT + device token — the device token is required (400 DEVICE_CONTEXT_REQUIRED without it). The merchant and device come from the device token.
| Field | Type | Required | Description |
|---|---|---|---|
gatewayTransactionId |
string | Yes | Transaction ID returned by the provider SDK |
paymentMode |
string | Yes | IPOS | NEARPAY (case-insensitive) |
amount |
number | Yes | Total charged; must be greater than 0 |
type |
string | No | sale (default) | auth |
baseAmount |
number | No | Defaults to amount - tipAmount - taxAmount - convenienceFee |
tipAmount |
number | No | Default 0 |
taxAmount |
number | No | Default 0 |
convenienceFee |
number | No | Default 0 |
currency |
string | No | Default USD |
cardLast4 |
string | No | Last 4 digits of the card |
cardBrand |
string | No | Card brand |
invoiceNumber |
string | No | Invoice number this payment settles |
invoiceId |
string | No | Invoice ID this payment settles |
orderId |
string | No | Your order reference |
metadata |
object | No | Free-form metadata |
curl -X POST "https://uat.auxcore.net/api/v1/transactions/external-payment" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"gatewayTransactionId": "NP-7781203345",
"paymentMode": "NEARPAY",
"amount": 54.00,
"baseAmount": 50.00,
"tipAmount": 4.00,
"cardLast4": "4242",
"cardBrand": "Visa"
}'Responses:
201 Created— no record existed yet; apendingtransaction is created (transactionIdNPAY-<gatewayTransactionId>orIPOS-<gatewayTransactionId>). The provider webhook later sets the final status.200 OK— the webhook already created the transaction; only the fee breakdown (and a missinginvoiceId) is filled in. Status and card data stay as the webhook set them.
Both return { "success": true, "data": { ...transaction } }. Validation failures return 400 VALIDATION_ERROR ("gatewayTransactionId is required", "paymentMode must be one of: IPOS, NEARPAY", "Valid amount is required", "type must be one of: sale, auth").
Itemised sales (POS cart)#
For the focused, shareable five-endpoint request contract and Postman verification matrix, see
POS Itemised Payment Contract.
A sale may carry the cart it sold, so the transaction detail and the receipt list the items and the
products count toward "most sold" (see Product Catalog, section "Tiles").
Send lineItems on any of:
POST /transactions/sale(card and cash) andPOST /transactions/authPOST /transactions/token-salePOST /transactions/ach-salePOST /transactions/external-payment(IPOS / NearPay)
Without lineItems a sale works exactly as before.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | No | product | misc. Defaults to product when productId is present, otherwise misc |
productId |
uuid | For product |
Device catalog product id (GET /device/products). Must be absent on a misc line |
name |
string | Yes | As shown on the receipt, max 150 characters. Copy the product name for a product line; free text for misc |
quantity |
number | Yes | Greater than 0, up to 3 decimal places (2.5 gallons) |
unit |
string | No | Send the product's unit code (gallon, hour, …; see Products). Receipts print it as its label (2.5 gallon); other text is printed as sent. Max 50 characters |
unitPrice |
number | Yes | 0 or greater, at most 2 decimal places |
lineTotal |
number | No | quantity × unitPrice rounded to cents. Computed when omitted; if sent it must match within $0.01 |
This type belongs to the transaction line, not the catalog item. A catalog-backed row usestype: "product" plus its productId whether GET /device/products returned catalog typeproduct or service; the server resolves the catalog type during preflight, and a service never
tracks inventory. Reserve type: "misc" for a free-text till row with no productId. Do not sendtype: "service" in lineItems.
Rules:
- At most 200 lines. An empty array is the same as no cart.
- The lines must add up to the sale's
baseAmount— the amount before tip, tax and convenience fee — within $0.01. SendbaseAmountexplicitly, or the server derives it asamount - tipAmount - taxAmount - convenienceFee. - A misc line ("Miscellaneous": a name and an amount typed at the till) is stored on the sale only. It never creates a catalog product and never counts toward most sold.
- The lines are a snapshot: editing or deleting the product later does not change the sale.
- Product quantities are aggregated across duplicate lines and checked against active catalog
state,maxQuantity, the current device branch, inventory policy, and server-calculated
Available. Card/cash sale, token sale, AUTH, and ACH submission fail closed before payment when
validation rejects the cart or the Invoice service is unavailable. allowZeroStockSale: falserejects insufficient Available before payment. WithallowZeroStockSale: true, Available 2 and quantity 5 can complete: inventory records deducted 2,
not deducted 3, and Available 0. The not-deducted 3 is immutable history; later Quantity In does
not consume it.- Service, misc, and untracked product lines need no stock location. A tracked product requires the
device's current active physical branch; pairing can remain valid even when that branch is absent.
curl -X POST "https://uat.auxcore.net/api/v1/transactions/sale" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "merchant_123",
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.50,
"paymentMethod": "Cash",
"lineItems": [
{ "productId": "3f2b8c1e-4d5a-4e6f-8a7b-9c0d1e2f3a4b", "name": "Diesel", "quantity": 2.5, "unit": "gallon", "unitPrice": 4.19 },
{ "type": "misc", "name": "Car wash", "quantity": 1, "unitPrice": 8.00 }
]
}'The transaction returned (and GET /transactions/:id) carries the normalised cart:
"lineItems": [
{ "type": "product", "productId": "3f2b8c1e-...", "name": "Diesel", "quantity": 2.5, "unit": "gallon", "unitPrice": 4.19, "lineTotal": 10.48 },
{ "type": "misc", "productId": null, "name": "Car wash", "quantity": 1, "unit": null, "unitPrice": 8, "lineTotal": 8 }
]The transaction list (GET /transactions) does not include lineItems; read them from the
detail. Email, printed and PDF receipts list the items; the SMS receipt does not.
Errors are 400 INVALID_LINE_ITEMS, checked before the card is charged. The message names the line
("lineItems[1]: quantity must have at most 3 decimal places") and error.details.line carries its
index; a total mismatch reports details.lineItemsTotal and details.baseAmount.
A cart over a product's per-order limit is 400 QUANTITY_LIMIT_EXCEEDED, also before any charge:
{
"success": false,
"error": {
"code": "QUANTITY_LIMIT_EXCEEDED",
"message": "\"Diesel\" is limited to 50 per order",
"details": {
"violations": [
{
"code": "QUANTITY_LIMIT_EXCEEDED",
"productId": "3f2b8c1e-...",
"message": "\"Diesel\" is limited to 50 per order",
"maxQuantity": 50,
"requested": 60
}
]
}
}
}For external-payment, send lineItems with the device's fee breakdown. If the provider webhook
already created the transaction, the cart is added to it. Because the money already exists, a
failed inventory preflight preserves the financial record; when an external SALE reaches a final
approved/settled status, the server queues inventory work when POS settlement is enabled. Once
Invoice accepts that work, unresolved stock or location outcomes become Pending Review. A terminal
delivery failure remains operational outbox work and never changes the payment result.
Payment finality and inventory#
The inventory effects below assume the server-side POS inventory settlement rollout is enabled.
The mobile client cannot read or control that setting and must not treat payment success as proof
of a completed stock deduction.
| Payment lifecycle | Physical inventory effect |
|---|---|
| Approved card/token SALE or completed cash-equivalent SALE | Queue one settlement |
| Approved AUTH | None |
| Successful CAPTURE | Queue one settlement |
| ACH accepted/pending | None |
| ACH settled | Queue one settlement |
| Confirmed external SALE | Queue one settlement; accepted stock/location conflicts become Pending Review |
| External AUTH | None until a successful CAPTURE |
Declined, failed, or velocity IN_REVIEW |
None; later approval queues once |
| Duplicate callback/finalizer | No additional deduction |
| Void, refund, reversal, dispute, ACH return, or customer return | No automatic restock |
Payment remains successful if inventory delivery or application fails. Staff can retry Invoice
Pending Review or correct a missing/inactive physical location; operational staff handle terminal
Transaction outbox failures separately. These actions are idempotent and must never create a
replacement payment. Physical returns require a separate Quantity In movement with Customer Return
reason. Refresh the device product catalog after a completed sale.
Querying Transactions from a Device#
curl "https://uat.auxcore.net/api/v1/transactions" \
-H "Authorization: Bearer USER_JWT_HERE" \
-H "X-Device-Token: DEVICE_TOKEN_HERE" \
-H "X-Tenant-ID: your-tenant-id"When X-Device-Token is present, results are filtered to this device only — unless the device parameter isViewAllMerchantTransaction is true, in which case they cover the whole merchant. Without the device token, results include all transactions for the merchant.
Related#
Changelog#
For mobile developers: This section tracks breaking changes, new endpoints, and behavior changes that require app updates. Review each entry before upgrading your integration.
v2.5.2 — 2026-10-14 — Field Config reaches the right device fields#
Type: Value fix — no change to the response shape.
- A merchant's Field Config now sets
inputFieldsby the field catalog: Billing Address drives theavs_*fields (it used to driveshipping_*), Customer Name drivescustomer_name, and Shipping Address no longer touchesshipping.enabledor theshipping_*fields. Values change on a device the next time the merchant saves Field Config, and the bumpedversionmakes the heartbeat re-fetch. locked(already sent on some entries) is now documented.
v2.5.1 — 2026-10-14 — Product cart renamed POS Lite#
Type: Rename — no breaking change.
GET /device/parametersreturns the cart switch asposLite.enabled.productCart.enabledstill returns the same value but is deprecated; readposLite.
v2.5.0 — 2026-10-14 — POS product cart (TNEW-350)#
Type: New optional fields — no breaking change.
GET /device/parametersreturnsproductCart.enabled(defaulttrue): show or hide the cart.- Sales accept an optional
lineItemscart (product and misc lines, decimal quantities) — see "Itemised sales (POS cart)" above. It is validated againstbaseAmount, stored on the transaction, shown on the portal and printed on email / print / PDF receipts. GET /productsaddsisFavorite,soldQuantity,unit/unitLabel, thefavoritefilter,sort, andview=most_soldfor the tile view — see Product Catalog. Favorites and units are set by the merchant in the web portal; the app only reads them.
v2.4.0 — 2026-08-10 — Merchant switching without re-entering TPN#
Type: New endpoints + behavior change
Affects: apps whose users belong to more than one merchant
What changed#
A user can now switch merchant without logging out, and returns to a merchant they previously registered on without re-entering TPN + Pairing Code — as long as that device is still ACTIVE.
| Endpoint | Change |
|---|---|
GET /api/v1/auth/my-merchants |
New. Merchants for the signed-in user, each with deviceRegistration.hasActiveDevice |
POST /api/v1/auth/switch-merchant |
New. Re-issues the access token scoped to another merchant |
GET /api/v1/device/session |
New. Reports whether the stored device token is still reusable — 200 with reusable + reason, never a 403 |
POST /api/v1/auth/refresh |
Now keeps the selected merchant. It previously reverted to the user's primary merchant |
GET /api/v1/auth/me |
Now returns the merchant the session is scoped to |
POST /device/register |
Records the registering user; that user may re-pair their own ACTIVE device after losing the stored token |
PATCH /merchants/{merchantId}/devices/{deviceId}/status |
Any move out of ACTIVE now revokes device sessions immediately, not only SUSPENDED |
Required app changes#
- Store
merchantId → deviceToken, not a single token. See Multi-merchant device storage. - Do not call
POST /device/logouton user sign-out. That endpoint unregisters the device and sets itINACTIVE, which breaks the "sign out then sign back in without a TPN prompt" behaviour. Keep the stored map across sign-out. - After switching merchant, call
GET /device/sessionbefore deciding whether to show Device Registration. - Replace the startup
GET /device/meprobe withGET /device/session—/device/meanswers403for a device that is merely inactive, which is an error path rather than a decision signal.
Documentation corrected in this release#
The Startup Checklist previously instructed the app to call POST /device/logout and clear the stored token on user sign-out. That guidance is now wrong and has been split into two separate steps — sign-out (step 8, keeps everything) and Unregister Device (step 9, the only step allowed to call /device/logout). The Integration Flow and the entry-flow diagram were rewritten to include login, merchant selection and the session check.
v2.3.0 — 2026-06-25 — Test Connection validates gateway config#
Type: Behavior change + error-code change
Affects: POST /device/test-connection response and error codes
What changed#
POST /device/test-connection previously validated device-level credentials and pinged the
processor with a hardcoded health URL. It now validates the merchant payment gateway
configuration the device will actually use at payment time — an ACTIVE gateway of the device's
type, with complete credentials accepted by the gateway adapter (plus a VAR sheet for TSYS).
Config validation only — no processor call, no money moved.
Response — additive fields#
{ "status": "ACTIVE", "gatewayType": "TsysSierra", "configStatus": "VALID", "processorStatus": "CONNECTED" }gatewayType(new),configStatus: "VALID"(new).processorStatusretained for backward compatibility.
Error codes — changed#
| Old (≤ v2.2) | New (v2.3) |
|---|---|
400 INVALID_CREDENTIALS / 400 CREDENTIALS_MISSING (device creds) |
422 GATEWAY_NOT_CONFIGURED with reason ∈ NO_ACTIVE_GATEWAY | NO_CREDENTIALS | INVALID_CREDENTIALS | MISSING_VAR_SHEET |
| — | 400 GATEWAY_NOT_SUPPORTED (no adapter for the resolved gateway) |
422 DEVICE_ALREADY_ACTIVE |
200 idempotent with alreadyActive: true |
503 PROCESSOR_UNREACHABLE (processor down) |
503 PROCESSOR_UNREACHABLE (validation service unreachable) |
Required app changes#
☐ Read gatewayType / configStatus from the success response (optional; additive)
☐ Handle 422 GATEWAY_NOT_CONFIGURED as NON-retryable — show reason + "contact administrator"
(admin must fix the gateway config / VAR sheet / credentials in the portal)
☐ Handle 400 GATEWAY_NOT_SUPPORTED
☐ Treat a 200 alreadyActive response as success (stop expecting 422 DEVICE_ALREADY_ACTIVE)
☐ Keep retry only for 503 PROCESSOR_UNREACHABLENearPay devices are validated locally (Terminal ID present + NearPay JWT mintable); see Test Connection and Payment Flow by Device Type.
v2.2.0 — 2026-06-16 — Device Config Redesign#
Type: ⚠️ Breaking (fields removed) + Additive (new fields)
Affects: POST /device/register response, GET /device/me response
Impact Assessment#
| Area | Impact | Action Required |
|---|---|---|
paymentMethod removed from response |
❌ Breaking | Remove reads; field is absent |
routingType removed from response |
❌ Breaking | Remove reads; field is absent |
processorId removed from response |
❌ Breaking | Remove reads; field is absent |
bin removed from response |
❌ Breaking | Remove reads; field is absent |
New: paymentAdjustmentType / Value / CardValue / AchValue |
✅ Additive | Read and apply for price display |
New: autoSettleEnabled / settleAt |
✅ Additive | Informational; no app action needed |
New: nearpayTid, sunmiSN, iposTpn, iposTpnCode |
✅ Additive | Use for SDK init per payment mode |
New: jwtNearpayDevice in register response |
✅ Additive | Pass to NearPay SDK when paymentMode = NEARPAY |
New: nearpayJWT in /device/me response |
✅ Additive | Pass to NearPay SDK when paymentMode = NEARPAY |
paymentMode renamed "Payment Gateway" in portal UI |
✅ No API change | Field name and enum values unchanged |
deviceType portal labels corrected |
✅ No API change | API enum values were always correct |
New SUNMI manufacturer enum value |
✅ Additive | Handle in switch/if statements |
New NEARPAY payment mode enum value |
✅ Additive | Handle in switch/if statements |
⚠️ Breaking: 4 fields removed from device responses#
The following columns were dropped from the database. They no longer appear in POST /device/register or GET /device/me responses:
| Removed Field | Was | Replacement |
|---|---|---|
paymentMethod |
"CRDT" | "ACH" | "BOTH" |
Moved to Merchant Gateway Config (future) |
routingType |
"SIMPLE" |
Moved to Merchant Gateway Config (future) |
processorId |
string | null | Moved to Merchant Gateway Config (future) |
bin |
string | null | Moved to Merchant Gateway Config (future) |
Required app changes:
// Remove these from Device model/struct:
paymentMethod ← absent from response
routingType ← absent from response
processorId ← absent from response
bin ← absent from responseNew: paymentAdjustmentType, paymentAdjustmentValue, paymentAdjustmentCardValue and paymentAdjustmentAchValue#
{
"paymentAdjustmentType": "FLAT",
"paymentAdjustmentValue": "1.5000",
"paymentAdjustmentCardValue": "2.0000",
"paymentAdjustmentAchValue": "0.5000"
}| Value | Meaning | How to Display |
|---|---|---|
NONE |
No adjustment | Show base price as-is |
FLAT |
Fixed dollar surcharge | displayPrice = baseAmount + parseFloat(value) |
PERCENTAGE |
Percentage surcharge | displayPrice = baseAmount * (1 + parseFloat(value) / 100) |
Pricing Model Specifics:#
flatpricing model (or no pricing model configured): UsepaymentAdjustmentValuefor single adjustment display.cashdiscountpricing model: UsepaymentAdjustmentCardValuefor credit card calculations andpaymentAdjustmentAchValuefor ACH payment calculations.
Display only — do not apply to the transaction amount. The server computes the final adjusted amount.
New: autoSettleEnabled and settleAt#
{ "autoSettleEnabled": true, "settleAt": "17:00" }Informational only. Settlement runs server-side; no mobile app action required.
New: jwtNearpayDevice (register) and nearpayJWT (me)#
When paymentMode = "NEARPAY", responses include a signed RS256 JWT for the NearPay SDK.
⚠️ Key name differs by endpoint:
POST /device/register→data.jwtNearpayDevice|GET /device/me→data.nearpayJWT
Store alongside deviceToken and pass to the NearPay SDK with nearpayTid.
New: nearpayTid, sunmiSN, iposTpn, iposTpnCode#
| Field | Present When |
|---|---|
nearpayTid |
paymentMode = NEARPAY |
sunmiSN |
manufacturer = SUNMI |
iposTpn |
paymentMode = IPOS |
iposTpnCode |
paymentMode = IPOS |
New enum values#
| Enum | New Value | Meaning |
|---|---|---|
manufacturer |
SUNMI |
Physical Android POS hardware |
paymentMode |
NEARPAY |
NearPay gateway; no CVV required |
No-impact: deviceType portal labels corrected#
The portal previously showed "Countertop" / "Mobile Terminal". Corrected to "Card Reader" / "POS Terminal" to match API enum values. The API values CARD_READER and POS_TERMINAL were always correct — no mobile app changes needed.
Migration checklist for mobile developers#
Breaking — must fix:
☐ Remove paymentMethod from Device model/struct
☐ Remove routingType from Device model/struct
☐ Remove processorId from Device model/struct
☐ Remove bin from Device model/struct
New fields — add to Device model/struct:
☐ paymentAdjustmentType: String ("NONE" | "FLAT" | "PERCENTAGE")
☐ paymentAdjustmentValue: String? (decimal string, e.g. "1.5000")
☐ paymentAdjustmentCardValue: String? (decimal string, e.g. "2.0000")
☐ paymentAdjustmentAchValue: String? (decimal string, e.g. "0.5000")
☐ autoSettleEnabled: Boolean
☐ settleAt: String? ("HH:MM" format)
☐ nearpayTid: String?
☐ sunmiSN: String?
☐ iposTpn: String?
☐ iposTpnCode: String?
New enum values:
☐ Add SUNMI to manufacturer enum
☐ Add NEARPAY to paymentMode enum
Logic:
☐ Apply paymentAdjustmentType/Value/CardValue/AchValue in price display before confirmation
☐ Pass jwtNearpayDevice (register) or nearpayJWT (me) + nearpayTid to NearPay SDK
☐ SUNMI + CARD_READER → no CVV input
☐ NEARPAY → no CVV inputv2.0.0 — 2026-06-02 — Device Activation Flow (Phase 1)#
Type: Breaking change
Affects: Registration flow, device status handling, app navigation after register
⚠️ Breaking: POST /device/register no longer activates the device#
Before:
POST /device/register → device.status = "ACTIVE" → app goes to dashboardAfter:
POST /device/register → device.status = "REGISTERED" → app must call test-connection
POST /device/test-connection (pass) → device.status = "ACTIVE" → app goes to dashboardRequired app changes:
- After
/registersucceeds, checkdata.testConnectionRequired === true - Navigate to a new Test Connection screen instead of the dashboard
- Call
POST /device/test-connection— show loading state - On success (
status = "ACTIVE") → navigate to dashboard - On failure → show error + Retry button
New: REGISTERED device status#
A new intermediate status has been added to the DeviceStatus enum:
| Status | Meaning | Can process payments? | Previous behavior |
|---|---|---|---|
PENDING |
Admin created, app not yet registered | ❌ No | Unchanged |
REGISTERED |
App registered, test-connection pending | ❌ No | New |
ACTIVE |
Fully activated | ✅ Yes | Previously set by /register |
INACTIVE |
Deactivated | ❌ No | Unchanged |
SUSPENDED |
Locked by admin | ❌ No | Unchanged |
App startup — handle new REGISTERED status:
GET /device/me
└─ status == "REGISTERED" → navigate to Test Connection screen ← NEW case
└─ status == "ACTIVE" → proceed to dashboard (unchanged)
└─ status == "SUSPENDED" → show suspended screen (unchanged)New: POST /device/test-connection endpoint#
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/device/test-connection |
| Auth | Authorization: Bearer <user_jwt> + X-Device-Token: <device_token> |
| Required device status | REGISTERED |
| Body | None |
New error codes to handle:
| HTTP | Code | When to show | Action |
|---|---|---|---|
403 |
DEVICE_NOT_REGISTERED |
Device is still PENDING |
Call /register first |
422 |
DEVICE_ALREADY_ACTIVE |
Device already ACTIVE |
Go directly to dashboard |
503 |
PROCESSOR_UNREACHABLE |
Processor down / network | Show retry; wait 10–30s |
400 |
INVALID_CREDENTIALS |
Credentials misconfigured | Show error; contact admin |
429 |
RATE_LIMITED |
Too many attempts | Show retryAfterSeconds countdown |
New: DEVICE_NOT_ACTIVE error on device endpoints#
Routes /me, /parameters, /heartbeat, /logout now return 403 DEVICE_NOT_ACTIVE if device is REGISTERED (not yet activated).
Handle this in your startup and heartbeat loops:
403 DEVICE_NOT_ACTIVE → navigate to Test Connection screenRegister response — new fields#
POST /device/register response now includes additional fields to guide the app:
{
"success": true,
"data": {
"device": { "status": "REGISTERED", ... },
"deviceToken": "...",
"nextStep": "test-connection",
"testConnectionRequired": true
}
}Migration checklist for mobile developers#
- Handle
status = "REGISTERED"in/device/meresponse (startup check) - Add Test Connection screen (UI + API call)
- Call
POST /device/test-connectionafter eachPOST /device/register - Handle
testConnectionRequired: truein register response - Handle
403 DEVICE_NOT_ACTIVEon/device/me, heartbeat and parameters (payment APIs do not re-checkACTIVE) - Handle new error codes:
DEVICE_NOT_REGISTERED,DEVICE_ALREADY_ACTIVE,PROCESSOR_UNREACHABLE,RATE_LIMITED - Show Retry button for
retryable: trueerrors on test-connection - Display
requestIdon error screens for support reference
v1.x.x — Prior to 2026-06-02#
Initial device registration flow. POST /device/register activated the device immediately upon successful TPN + pairing code validation (status = "ACTIVE").