Developer dashboard

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-connection step before the device becomes active. See Changelog for migration details.

ℹ️ v2.3.0: POST /device/test-connection now validates the merchant gateway configuration the device will use (config only — no processor call, no money moved) and returns a specific failure reason. 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, and POST /device/logout clarified 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#

text
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 prices

The 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.

GET/api/v1/transactions/fee-preview?baseAmount=100&tipAmount=0

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#

text
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 UNREGISTERED status — unregistering a device puts it in INACTIVE, 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:

text
merchantId → deviceToken

Rules 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#

text
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#

text
https://uat.auxcore.net

Authentication#

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).

http
Authorization: Bearer <user_jwt>
X-Tenant-ID: your-tenant-id

Step 2 — Device-Only Endpoints (/device/me, /device/parameters, /device/heartbeat)#

Require both headers — user JWT for identity + device token for device context.

http
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-id

Store the deviceToken in 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.

Send both headers. The user JWT authenticates the call; the device token enriches it with device context (device-level credentials, limits, transaction type restrictions).

http
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-id

Important: If X-Device-Token is included, it must be valid. An invalid or revoked device token returns 401 (a suspended device returns 403 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 ACTIVE status is not re-checked there, so gate payments in the app on a successful test-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.

GET/api/v1/auth/my-merchants

Auth: User JWT. No device token.

Query Default Description
includeDeviceStatus true Set false to skip the per-merchant device lookup

Response (200 OK):

List My Merchantsjson
{
  "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.

hasActiveDevice is 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 call GET /device/session after 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.

POST/api/v1/auth/switch-merchant

Auth: User JWT. No device token.

Request Body:

Field Type Required Description
merchantId string Yes A merchant from GET /auth/my-merchants
Switch Merchantbash
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.

Switch Merchantjson
{
  "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-merchant is a different endpoint: it completes login and requires the 10-minute selectionToken returned 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).

POST/api/v1/device/register

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 ACTIVE device gets 400 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:

Register Devicebash
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:

Register Devicejson
{
  "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 = REGISTERED means the app has paired successfully, but the device must pass POST /device/test-connection before 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 to ACTIVE when skipTestConnection is set (nextStep: "none", testConnectionRequired: false)
  • registeredByUserId is set to the calling user
  • Any existing active sessions are revoked immediately
  • registeredAt is set on first registration; preserved on re-registration
  • lastActiveAt updated

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 (when testConnectionRequired = 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).

POST/api/v1/device/test-connection

Auth: User JWT + device token — both required.

Test Connectionhttp
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-id

Request Body: None.

Example:

Test Connectionbash
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):

Test Connectionjson
{
  "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.

Test Connectionjson
{
  "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:

Test Connectionjson
{
  "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 the message and 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 your X-Request-ID header on 500 responses, so send one.

Side effects:

  • On success: device status transitions from REGISTERED → ACTIVE.
  • Every attempt (pass or fail) is logged in the device_test_attempts table.

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 a 422 GATEWAY_NOT_CONFIGURED is 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.

GET/api/v1/device/session

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:

Check Device Sessionjson
{
  "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:

Check Device Sessionjson
{
  "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.
Check Device Sessionbash
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.

GET/api/v1/device/me

Auth: User JWT + device token — both required.

Get Current Devicehttp
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-id

Example:

Get Current Devicebash
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/register returns the NearPay JWT as data.jwtNearpayDevice. This endpoint returns it as data.nearpayJWT. Both carry the same RS256-signed token.

Get Current Devicejson
{
  "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.

GET/api/v1/device/parameters

Auth: User JWT + device token — both required. Device must be ACTIVE.

Example:

Get Device Parametersbash
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):

Get Device Parametersjson
{
  "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.

GET/api/v1/device/heartbeat

Auth: User JWT + device token — both required. Device must be ACTIVE.

Example:

Heartbeatbash
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):

Heartbeatjson
{
  "success": true,
  "data": {
    "paramVersion": 7,
    "serverTime": "2026-04-14T10:30:00.000Z"
  }
}

Usage pattern:

Heartbeattext
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 config

Recommended 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 ACTIVE and the app keeps its stored merchantId → deviceToken map, 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.

POST/api/v1/device/logout

Auth: User JWT + device token — both required.

Unregister Device (/device/logout)http
Authorization: Bearer <user_jwt>
X-Device-Token: <device_token>
X-Tenant-ID: your-tenant-id

Example:

Unregister Device (/device/logout)bash
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):

Unregister Device (/device/logout)json
{
  "success": true,
  "data": {
    "message": "Device logged out"
  }
}

After calling this endpoint:

  1. Clear the stored deviceToken from secure storage.
  2. The device status is now INACTIVE — the app cannot process payments until re-registered.
  3. To reactivate: log in (POST /auth/login) and call POST /device/register again 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:

json
{
  "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:

json
{
  "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#

kotlin
// 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)#

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:

text
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/logout and clear the token at sign-out; doing that now breaks the reuse behaviour, because the device drops to INACTIVE and 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.

text
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:

json
{
  "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:

json
{
  "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 becomes
display: 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":

  1. From POST /device/register → read data.jwtNearpayDevice.
  2. From GET /device/me → read data.nearpayJWT (same RS256 token, different key name).
  3. Read nearpayTid from the device object.
  4. 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#

bash
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, plus daily / weekly / monthly volume) — 422 LIMIT_EXCEEDED
  • Validates the transaction type is enabled on this device (saleEnabled, preauthEnabled, refundEnabled, voidEnabled) — 403 TX_TYPE_DISABLED
  • Rejects ACH sales when achTransferEnabled is off — 403 PAYMENT_TYPE_DISABLED
  • Records the deviceId and 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.

POST/api/v1/transactions/sale
Cash Salebash
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 sale is supported for cash — POST /transactions/auth with paymentMethod: "Cash" returns 400 VALIDATION_ERROR.
  • The record gets transactionId CASH-<timestamp>-<suffix>, status: "settled", gatewayName: "Cash", and no convenience fee. baseAmount defaults to amount - tipAmount - taxAmount.
  • Cash transactions cannot be voided (400 "Void is not supported for Cash transactions. Please issue a refund instead."). Refund them with POST /transactions/:id/refund — full refund only; a smaller amount returns 400 "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.

POST/api/v1/transactions/external-payment

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
Record External SDK Paymentbash
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; a pending transaction is created (transactionId NPAY-<gatewayTransactionId> or IPOS-<gatewayTransactionId>). The provider webhook later sets the final status.
  • 200 OK — the webhook already created the transaction; only the fee breakdown (and a missing invoiceId) 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) and POST /transactions/auth
  • POST /transactions/token-sale
  • POST /transactions/ach-sale
  • POST /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 uses
type: "product" plus its productId whether GET /device/products returned catalog type
product 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 send
type: "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. Send baseAmount explicitly, or the server derives it as amount - 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: false rejects insufficient Available before payment. With
    allowZeroStockSale: 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.
bash
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:

json
"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:

json
{
  "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#

bash
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.



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 inputFields by the field catalog: Billing Address drives the avs_* fields (it used to drive shipping_*), Customer Name drives customer_name, and Shipping Address no longer touches shipping.enabled or the shipping_* fields. Values change on a device the next time the merchant saves Field Config, and the bumped version makes 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/parameters returns the cart switch as posLite.enabled. productCart.enabled still returns the same value but is deprecated; read posLite.

v2.5.0 — 2026-10-14 — POS product cart (TNEW-350)#

Type: New optional fields — no breaking change.

  • GET /device/parameters returns productCart.enabled (default true): show or hide the cart.
  • Sales accept an optional lineItems cart (product and misc lines, decimal quantities) — see "Itemised sales (POS cart)" above. It is validated against baseAmount, stored on the transaction, shown on the portal and printed on email / print / PDF receipts.
  • GET /products adds isFavorite, soldQuantity, unit / unitLabel, the favorite filter, sort, and view=most_sold for 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#

  1. Store merchantId → deviceToken, not a single token. See Multi-merchant device storage.
  2. Do not call POST /device/logout on user sign-out. That endpoint unregisters the device and sets it INACTIVE, which breaks the "sign out then sign back in without a TPN prompt" behaviour. Keep the stored map across sign-out.
  3. After switching merchant, call GET /device/session before deciding whether to show Device Registration.
  4. Replace the startup GET /device/me probe with GET /device/session — /device/me answers 403 for 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#

json
{ "status": "ACTIVE", "gatewayType": "TsysSierra", "configStatus": "VALID", "processorStatus": "CONNECTED" }
  • gatewayType (new), configStatus: "VALID" (new). processorStatus retained 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#

text
☐ 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_UNREACHABLE

NearPay 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:

text
// Remove these from Device model/struct:
paymentMethod  ← absent from response
routingType    ← absent from response
processorId    ← absent from response
bin            ← absent from response

New: paymentAdjustmentType, paymentAdjustmentValue, paymentAdjustmentCardValue and paymentAdjustmentAchValue#

json
{
  "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:#
  • flat pricing model (or no pricing model configured): Use paymentAdjustmentValue for single adjustment display.
  • cashdiscount pricing model: Use paymentAdjustmentCardValue for credit card calculations and paymentAdjustmentAchValue for ACH payment calculations.

Display only — do not apply to the transaction amount. The server computes the final adjusted amount.


New: autoSettleEnabled and settleAt#

json
{ "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#

text
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 input

v2.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:

text
POST /device/register  →  device.status = "ACTIVE"  →  app goes to dashboard

After:

text
POST /device/register  →  device.status = "REGISTERED"  →  app must call test-connection
POST /device/test-connection (pass)  →  device.status = "ACTIVE"  →  app goes to dashboard

Required app changes:

  1. After /register succeeds, check data.testConnectionRequired === true
  2. Navigate to a new Test Connection screen instead of the dashboard
  3. Call POST /device/test-connection — show loading state
  4. On success (status = "ACTIVE") → navigate to dashboard
  5. 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:

New: REGISTERED device statustext
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:

text
403 DEVICE_NOT_ACTIVE  →  navigate to Test Connection screen

Register response — new fields#

POST /device/register response now includes additional fields to guide the app:

json
{
  "success": true,
  "data": {
    "device": { "status": "REGISTERED", ... },
    "deviceToken": "...",
    "nextStep": "test-connection",
    "testConnectionRequired": true
  }
}

Migration checklist for mobile developers#

  • Handle status = "REGISTERED" in /device/me response (startup check)
  • Add Test Connection screen (UI + API call)
  • Call POST /device/test-connection after each POST /device/register
  • Handle testConnectionRequired: true in register response
  • Handle 403 DEVICE_NOT_ACTIVE on /device/me, heartbeat and parameters (payment APIs do not re-check ACTIVE)
  • Handle new error codes: DEVICE_NOT_REGISTERED, DEVICE_ALREADY_ACTIVE, PROCESSOR_UNREACHABLE, RATE_LIMITED
  • Show Retry button for retryable: true errors on test-connection
  • Display requestId on 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").