# Device Management

Manage POS devices and payment terminals associated with a merchant account. This covers the full device lifecycle — provisioning, configuration, templates, and pairing.

---

## Overview

The device system has three components:

| Component      | Base Path                                                    | Auth     | Purpose                    |
| -------------- | ------------------------------------------------------------ | -------- | -------------------------- |
| **Devices**    | `/api/v1/merchants/:merchantId/devices`                      | User JWT | CRUD for device records    |
| **Parameters** | `/api/v1/merchants/:merchantId/devices/:deviceId/parameters` | User JWT | Per-device configuration   |
| **Templates**  | `/api/v1/merchants/:merchantId/device-templates`             | User JWT | Reusable parameter presets |

All portal endpoints require an `Authorization: Bearer <jwt>` header. The tenant is taken from the token (it is forwarded to the merchant service as `X-Tenant-ID`).

The `branches.manage` permission (Locations & Devices) controls serial-number visibility: callers without it still see devices, but `sunmiSN` is returned in full on device reads, and Reveal Serial Number returns `403`. Reads (`GET`) by merchant users are limited to their own merchant; tenant admins need `merchants.view`.

> For mobile app / POS integration, see the [Mobile Device Integration guide](../mobile/device-integration.md).

---

## Devices

### List Devices

```http
GET /api/v1/merchants/:merchantId/devices
```

**Query Parameters:**

| Parameter | Type   | Default | Description                                                                              |
| --------- | ------ | ------- | ---------------------------------------------------------------------------------------- |
| `status`  | enum   | —       | Filter by status: `PENDING`, `REGISTERED`, `ACTIVE`, `INACTIVE`, `SUSPENDED`, `ARCHIVED` |
| `limit`   | number | 50      | Results per page                                                                         |
| `offset`  | number | 0       | Pagination offset                                                                        |

> **Archived devices:** Omitting `status`, or filtering by any status other than `ARCHIVED`, always excludes archived (soft-deleted) devices. Pass `status=ARCHIVED` explicitly to list archived devices — this is the only way to see them.

**Example:**

```bash
curl "https://uat.auxcore.net/api/v1/merchants/8f3c2a1e-5b7d-4e9a-9c1f-2d6e8a4b7c30/devices?status=ACTIVE&limit=20" \
  -H "Authorization: Bearer YOUR_JWT"
```

**Response:**

```json
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "tenantId": "your-tenant-id",
      "merchantId": "8f3c2a1e-5b7d-4e9a-9c1f-2d6e8a4b7c30",
      "branchId": null,
      "deviceLabel": "Front Counter POS",
      "deviceNumber": 1001,
      "tpn": "483920174651",
      "manufacturer": "ANDROID_PHONE",
      "deviceType": "TAP_ON_PHONE",
      "paymentMode": "AUXVAULT",
      "merchantGatewayId": "2b9e7d1c-4f3a-4e8b-a5d6-7c0f1e2b3a48",
      "gateway": "NMI",
      "paymentAdjustmentType": "NONE",
      "paymentAdjustmentValue": null,
      "paymentAdjustmentCardValue": null,
      "paymentAdjustmentAchValue": null,
      "autoSettleEnabled": false,
      "settleAt": null,
      "templateId": "550e8400-e29b-41d4-a716-446655440099",
      "mid": "123456789012",
      "terminalId": "TID001",
      "iposTpn": null,
      "iposTpnCode": null,
      "nearpayTid": null,
      "sunmiSN": null,
      "serialMasked": null,
      "hasSerial": false,
      "hasApiKey": true,
      "skipTestConnection": false,
      "status": "ACTIVE",
      "pairingCode": "482910",
      "appVersion": "2.1.0",
      "osVersion": "14.0",
      "osPlatform": "ANDROID",
      "registeredAt": "2026-03-15T09:00:00.000Z",
      "registeredByUserId": "c2b7e4a9-1d3f-4e8b-9a6c-5f0d2e7b8a13",
      "assignedUserId": "c2b7e4a9-1d3f-4e8b-9a6c-5f0d2e7b8a13",
      "isShared": false,
      "lastActiveAt": "2026-04-14T08:32:00.000Z",
      "txCount": 247,
      "txVolumeUsd": "14820.50",
      "todayVolume": 312.4,
      "todayTransactionCount": 6,
      "createdBy": "c2b7e4a9-1d3f-4e8b-9a6c-5f0d2e7b8a13",
      "updatedBy": "c2b7e4a9-1d3f-4e8b-9a6c-5f0d2e7b8a13",
      "createdAt": "2026-03-10T00:00:00.000Z",
      "updatedAt": "2026-04-14T08:32:00.000Z",
      "deletedAt": null
    }
  ],
  "meta": {
    "total": 1,
    "limit": 20,
    "offset": 0
  }
}
```

> **Note:** The encrypted gateway API key is never returned; `hasApiKey` tells you whether one is set. `txCount` / `txVolumeUsd` are lifetime totals; `todayVolume` / `todayTransactionCount` cover today and are `null` when the figure could not be loaded.

---

### Create Device

```http
POST /api/v1/merchants/:merchantId/devices
```

**Request Body:**

| Field                                                                               | Type           | Required | Description                                                                                                            |
| ----------------------------------------------------------------------------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `deviceLabel`                                                                       | string         | Yes      | Human-readable name                                                                                                    |
| `manufacturer`                                                                      | enum           | Yes      | `ANDROID_PHONE` \| `APPLE_PHONE` \| `SUNMI` (Dejavoo terminals are also accepted and must use `IPOS`)                  |
| `deviceType`                                                                        | enum           | Yes      | `TAP_ON_PHONE` \| `CARD_READER` \| `POS_TERMINAL`                                                                      |
| `paymentMode`                                                                       | enum           | Yes      | `AUXVAULT` \| `IPOS` \| `NEARPAY`                                                                                      |
| `merchantGatewayId`                                                                 | uuid           | No       | Merchant gateway the device uses. Must belong to this merchant and be `ACTIVE`. The `gateway` code is derived from it. |
| `branchId`                                                                          | uuid           | No       | Location (child merchant) the device belongs to. Must belong to this merchant.                                         |
| `assignedUserId`                                                                    | uuid           | No       | Team member the device belongs to. Omit for a shared device (`isShared` is derived).                                   |
| `templateId`                                                                        | uuid           | No       | Applies template parameters at creation time                                                                           |
| `mid`                                                                               | string         | No       | Merchant ID at gateway                                                                                                 |
| `terminalId`                                                                        | string         | No       | Terminal ID at gateway                                                                                                 |
| `apiKey`                                                                            | string         | No       | Gateway API key (encrypted at rest with AES-256-GCM)                                                                   |
| `iposTpn`                                                                           | string         | No       | iPOSpays Terminal Processor Number (for `IPOS`). Must be unique across devices.                                        |
| `iposTpnCode`                                                                       | string         | No       | iPOSpays TPN code                                                                                                      |
| `nearpayTid`                                                                        | string         | No       | NearPay terminal ID (for `NEARPAY`)                                                                                    |
| `sunmiSN`                                                                           | string         | No       | Serial Number — for Sunmi hardware devices                                                                             |
| `skipTestConnection`                                                                | boolean        | No       | Device becomes `ACTIVE` on registration without a test-connection step                                                 |
| `paymentAdjustmentType`                                                             | enum           | No       | `NONE` (default) \| `FLAT` \| `PERCENTAGE`                                                                             |
| `paymentAdjustmentValue`, `paymentAdjustmentCardValue`, `paymentAdjustmentAchValue` | decimal string | No       | Adjustment amounts                                                                                                     |
| `autoSettleEnabled`                                                                 | boolean        | No       | Default `false`                                                                                                        |
| `settleAt`                                                                          | string         | No       | Auto-settle time                                                                                                       |

**Example:**

```bash
curl -X POST "https://uat.auxcore.net/api/v1/merchants/8f3c2a1e-5b7d-4e9a-9c1f-2d6e8a4b7c30/devices" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "deviceLabel": "Drive-Thru Terminal",
    "manufacturer": "ANDROID_PHONE",
    "deviceType": "TAP_ON_PHONE",
    "paymentMode": "AUXVAULT",
    "merchantGatewayId": "2b9e7d1c-4f3a-4e8b-a5d6-7c0f1e2b3a48",
    "mid": "123456789012",
    "terminalId": "TID002",
    "templateId": "550e8400-e29b-41d4-a716-446655440099"
  }'
```

**Response (201 Created):**

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440002",
    "deviceNumber": 1002,
    "tpn": "739201847362",
    "pairingCode": "619274",
    "status": "PENDING",
    "createdAt": "2026-04-14T10:00:00.000Z",
    ...
  }
}
```

**Auto-generated fields:**

| Field          | Rule                                                            |
| -------------- | --------------------------------------------------------------- |
| `tpn`          | 12-digit numeric, globally unique. Retry up to 10x on collision |
| `pairingCode`  | 6-digit numeric, unique per tenant. Used to pair the mobile app |
| `deviceNumber` | Sequential per merchant starting at `1001`; never reused        |

**Errors:** `400` `Missing required fields`; `400 VALIDATION_ERROR` for a gateway that does not belong to the merchant or is not active, a branch that does not belong to the merchant, a duplicate `iposTpn`, or a Dejavoo device not using `IPOS`.

> **If `templateId` is provided:** Parameters are automatically created from the template. The mobile app will receive the template-configured values on its first `GET /device/parameters` call.

---

### Get Device

```http
GET /api/v1/merchants/:merchantId/devices/:deviceId
```

Returns a single device object, including archived (soft-deleted) devices — check `status === "ARCHIVED"` in the response if you need to distinguish them. Returns `404` only if the device does not exist at all.

---

### Update Device

```http
PATCH /api/v1/merchants/:merchantId/devices/:deviceId
```

All create fields are optional here. Only supplied fields are updated.

**Request Body:**

```json
{
  "deviceLabel": "Updated Label",
  "branchId": "3d1f8e2a-7c4b-4a9e-8f2d-1b6c9e0a5d47",
  "mid": "999999999999",
  "terminalId": "TID003",
  "apiKey": "new-plaintext-gateway-key",
  "templateId": "550e8400-e29b-41d4-a716-446655440099"
}
```

- `merchantGatewayId: null` clears the gateway. Switching to a different gateway requires it to be `ACTIVE`.
- `branchId: null` (or `""`) removes the location; changing it records a reassignment in the device and location activity.
- A masked `sunmiSN` value sent back is ignored.

> **Note:** `tpn` and `pairingCode` are immutable after creation and cannot be updated via this endpoint. Use Refresh Pairing Code to issue a new pairing code.

**Response (200 OK):** Updated device object.

---

### Update Device Status

```http
PATCH /api/v1/merchants/:merchantId/devices/:deviceId/status
```

**Request Body:**

```json
{
  "status": "ACTIVE"
}
```

**Status Transition Rules:**

`PENDING`, `REGISTERED`, `ACTIVE`, `INACTIVE`, and `SUSPENDED` are freely inter-transitionable — any of these five statuses can be set from any other via this endpoint (e.g. `SUSPENDED → ACTIVE`, `SUSPENDED → PENDING`, `ACTIVE → PENDING` are all allowed). Setting the status the device already has returns `422`.

An unknown status returns `400 Invalid status value`. `ARCHIVED` is **not** a valid target for this endpoint — requesting `"status": "ARCHIVED"` always returns `422 Unprocessable Entity`. Use Archive Device instead. Likewise, an archived device cannot be reached by this endpoint at all (it's excluded from the underlying lookup, `404`) — use Unarchive Device to restore it first.

**Side effects when moving out of `ACTIVE`:** All active device sessions are immediately revoked — for **any** target status, not only `SUSPENDED`. The mobile app receives `401 Unauthorized` on its next request (or `reusable: false, reason: "NOT_ACTIVE"` from `GET /api/v1/device/session`) and must register again.

This is what releases a device: a device that is no longer `ACTIVE` can no longer be reused by the app that holds its token, and the merchant user is asked for TPN + Pairing Code again. Transitioning **to** `ACTIVE` leaves existing sessions untouched.

---

### Delete Device

```http
DELETE /api/v1/merchants/:merchantId/devices/:deviceId
```

Soft-deletes the device — sets `status` to `ARCHIVED` and `deletedAt` to now. Functionally identical to Archive Device; kept for backward compatibility. New integrations should prefer `POST .../archive`.

**Side effects:** All active sessions are revoked with reason `ARCHIVED`.

**Response:**

```json
{
  "success": true,
  "message": "Device deleted"
}
```

---

### Archive Device

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/archive
```

Soft-deletes the device: sets `status` to `ARCHIVED` and `deletedAt` to now. The device is immediately excluded from the default device list and from any status-filtered list other than `status=ARCHIVED`.

**Side effects:** All active sessions are revoked with reason `ARCHIVED`. The mobile app will receive `401 Unauthorized` on its next request.

**Request Body:** empty

**Response:**

```json
{
  "success": true,
  "message": "Device archived"
}
```

---

### Unarchive Device

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/unarchive
```

Restores a previously archived device: clears `deletedAt` and sets `status` to `INACTIVE`. The device reappears in the default device list. The admin must explicitly reactivate it afterward (e.g. via Update Device Status) — it is never restored directly to `ACTIVE`.

**Request Body:** empty

**Response (200 OK):** Updated device object (`status: "INACTIVE"`).

**Errors:**

- `404 Not Found` — device does not exist.
- `422 Unprocessable Entity` — device is not currently archived (`"Device is not archived"`).

---

### Refresh Pairing Code

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/refresh-pairing-code
```

Generates a new pairing code and resets the device to `PENDING` status. Use this when:

- A device needs to be re-paired to a different handset
- The original pairing code was compromised

**Request Body:** empty

**Response:**

```json
{
  "success": true,
  "data": {
    "status": "PENDING",
    "pairingCode": "827461",
    "appVersion": null,
    "osVersion": null,
    "osPlatform": null,
    "registeredAt": null,
    ...
  }
}
```

**Side effects:**

- Device status reset to `PENDING`
- All active sessions revoked with reason `PAIRING_REFRESHED`
- Hardware binding (`appVersion`, `osVersion`, `osPlatform`, `registeredAt`) cleared
- The old pairing code becomes invalid immediately

---

### Register Device (Admin)

```http
POST /api/v1/merchants/:merchantId/devices/register
```

Registers a device on behalf of the merchant from the portal, with the same body as the mobile self-registration. The calling admin is recorded as `registeredByUserId`.

**Request Body:**

| Field                     | Type   | Required | Description          |
| ------------------------- | ------ | -------- | -------------------- |
| `tpn`                     | string | Yes      | 12-digit TPN         |
| `pairingCode`             | string | Yes      | Current pairing code |
| `appVersion`, `osVersion` | string | No       |                      |
| `osPlatform`              | enum   | No       | `ANDROID` \| `IOS`   |

**Response (201 Created):**

```json
{
  "success": true,
  "data": {
    "device": { "id": "550e8400-e29b-41d4-a716-446655440002", "status": "REGISTERED", ... },
    "deviceToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "jwtNearpayDevice": null
  }
}
```

Unlike the other device endpoints, the `device` object here is the raw record (`sunmiSN` is omitted when empty, and `serialMasked` / `hasSerial` / `hasApiKey` are not added).

**Errors (400):** `tpn and pairingCode are required`, `tpn must be a 12-digit numeric code`, `Invalid osPlatform value`, `Invalid TPN`, `Invalid pairing code`, `Device is not available for registration`, `Device already registered. Ask admin to deactivate or refresh the pairing code.`

---

### Test Connection

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/test-connection
```

Validates the device's gateway configuration without changing its status (the device-initiated `POST /api/v1/device/test-connection` is the one that moves `REGISTERED → ACTIVE`). No call is made to the device or the processor.

**Response:**

```json
{
  "success": true,
  "data": {
    "deviceId": "550e8400-e29b-41d4-a716-446655440001",
    "status": "ACTIVE",
    "gatewayType": "NMI",
    "configStatus": "VALID",
    "processorStatus": "CONNECTED",
    "validatedAt": "2026-04-14T10:05:00.000Z",
    "testDurationMs": 42
  }
}
```

`testDurationMs` is how long the validation took, not device latency. A configuration problem returns `400` with an `error.code` describing it (and `error.gatewayType` when known); an unknown device returns `404`.

---

### Device Activity

```http
GET /api/v1/merchants/:merchantId/devices/:deviceId/events
```

Recent lifecycle events for the device, newest first. Heartbeats are not included.

| Query   | Default | Description |
| ------- | ------- | ----------- |
| `limit` | 20      | Max 50      |

**Response:**

```json
{
  "success": true,
  "data": [
    {
      "id": "created:550e8400-e29b-41d4-a716-446655440001",
      "type": "CREATED",
      "title": "Device added",
      "detail": null,
      "outcome": null,
      "occurredAt": "2026-03-10T00:00:00.000Z"
    }
  ]
}
```

`type` is one of `CREATED`, `PAIRED`, `CONNECTION_TEST`, `SESSION_STARTED`, `SESSION_REVOKED`, `ADMIN_ACTION`; `outcome` is `PASS`, `FAIL` or `null`.

---

### Reveal Serial Number

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/reveal-serial
```

Requires `branches.manage`. Returns the full serial and records the reveal in the audit trail.

**Response:** `{ "success": true, "data": { "serialNumber": "V2S1234567890" } }`

---

## Device Parameters

Parameters control how a device behaves during payment flows — which transaction types are enabled, tip settings, receipt options, limits, and more.

> Parameters of Dejavoo devices are managed by the provider: Update, Apply Template and Reset return `403` with code `PROVIDER_PARAMETERS_NOT_EDITABLE`.

### Get Parameters

```http
GET /api/v1/merchants/:merchantId/devices/:deviceId/parameters
```

**Example:**

```bash
curl "https://uat.auxcore.net/api/v1/merchants/8f3c2a1e-5b7d-4e9a-9c1f-2d6e8a4b7c30/devices/550e8400-e29b-41d4-a716-446655440001/parameters" \
  -H "Authorization: Bearer YOUR_JWT"
```

**Response:** the stored parameter record, using the flat field names listed under Parameter Field Names (the grouped shape is only returned to the mobile app by `GET /api/v1/device/parameters`).

```json
{
  "success": true,
  "data": {
    "id": "7d4c2b1a-9e8f-4a6b-b3c2-1d0e9f8a7b65",
    "deviceId": "550e8400-e29b-41d4-a716-446655440001",
    "tenantId": "your-tenant-id",
    "version": 5,
    "txnSaleEnabled": true,
    "txnRefundEnabled": true,
    "txnVoidEnabled": true,
    "txnPreauthEnabled": false,
    "pinVoidEnabled": false,
    "pinRefundEnabled": false,
    "payTypeTapToPayEnabled": true,
    "payTypeKeyedInEnabled": true,
    "payTypeCashEnabled": false,
    "limitPerTxn": "500.00",
    "limitDaily": "5000.00",
    "isViewAllMerchantTransaction": false,
    "signatureThreshold": "0.00",
    "allowTips": true,
    "tipPresets": [10, 15, 20, 25],
    "taxEnabled": true,
    "tax1Label": "Sales Tax",
    "tax1Value": "8.25",
    "inputFieldsConfig": [
      {
        "key": "customer_name",
        "label": "Name",
        "group": "general",
        "type": "text",
        "display": true,
        "required": false,
        "locked": false,
        "order": 1
      }
    ],
    "createdAt": "2026-03-10T00:00:00.000Z",
    "updatedAt": "2026-04-14T08:32:00.000Z"
  }
}
```

> **`version`** increments on every update. Mobile apps use this field in the heartbeat response to detect when a full parameter refresh is needed.

---

### Update Parameters

```http
PUT /api/v1/merchants/:merchantId/devices/:deviceId/parameters
```

Supply any subset of fields. Omitted fields retain their current values. `id`, `deviceId`, `tenantId`, `version`, `createdAt` and `updatedAt` in the body are ignored.

**Request Body (flat, uses raw entity field names):**

```json
{
  "txnSaleEnabled": true,
  "txnRefundEnabled": true,
  "txnVoidEnabled": false,
  "txnPreauthEnabled": false,
  "pinVoidEnabled": true,
  "pinRefundEnabled": true,
  "payTypeTapToPayEnabled": true,
  "payTypeKeyedInEnabled": true,
  "payTypeQrCodeEnabled": false,
  "payTypePaymentLinkEnabled": false,
  "payTypeAchTransferEnabled": false,
  "payTypeCashEnabled": true,
  "limitPerTxn": "500.00",
  "limitDaily": "5000.00",
  "limitWeekly": null,
  "limitMonthly": null,
  "isViewAllMerchantTransaction": false,
  "signatureThreshold": "25.00",
  "receiptHeaderMsg": "Welcome!",
  "receiptFooterMsg": "Thank you!",
  "receiptDisclaimer": null,
  "receiptAutoPrintEnabled": true,
  "receiptPrintMerchantCopy": true,
  "receiptSmsEnabled": true,
  "receiptEmailEnabled": true,
  "receiptSignatureCapture": false,
  "posLiteEnabled": true,
  "allowTips": true,
  "tipPresets": [10, 15, 20, 25],
  "tipMaxAmount": "100.00",
  "tipMaxPercentage": 30,
  "taxEnabled": true,
  "taxEnableRemove": false,
  "taxRemovePassProtect": false,
  "tax1Label": "Sales Tax",
  "tax1Value": "8.25",
  "tax2Label": null,
  "tax2Value": null,
  "shippingFieldsEnabled": false,
  "shippingSameAsBillingVisible": false,
  "inputFieldsConfig": [
    {
      "key": "customer_name",
      "label": "Name",
      "group": "general",
      "type": "text",
      "display": true,
      "required": true,
      "locked": false,
      "order": 1
    }
  ]
}
```

**Response (200 OK):** Updated parameter record (same flat format as GET).

> **Side effect:** `version` always increments on every PUT call, even when values are unchanged.

---

### Apply Template to Device

Overrides the device's current parameters with values from a template, without replacing unspecified fields.

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/parameters/apply-template
```

**Request Body:**

```json
{
  "templateId": "550e8400-e29b-41d4-a716-446655440099"
}
```

**Response (200 OK):** Updated parameter record. Missing `templateId` returns `400`; an unknown template returns `404 Template not found`.

> Only fields present in the template's `parameters` payload are applied. Other device-specific settings are preserved.

---

### Reset Parameters

Resets all parameters to factory defaults.

```http
POST /api/v1/merchants/:merchantId/devices/:deviceId/parameters/reset
```

**Request Body:** empty

**Response (200 OK):** Parameters with factory default values.

---

## Device Templates

Templates are named snapshots of device parameters that can be applied to multiple devices.

### List Templates

```http
GET /api/v1/merchants/:merchantId/device-templates
```

| Query          | Default | Description                                                         |
| -------------- | ------- | ------------------------------------------------------------------- |
| `limit`        | 50      |                                                                     |
| `offset`       | 0       |                                                                     |
| `manufacturer` | —       | Templates for this manufacturer plus templates with no manufacturer |
| `deviceType`   | —       | Templates for this device type plus templates with no device type   |

Returns templates sorted: default template first, then by creation date descending.

**Response:**

```json
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440099",
      "tenantId": "your-tenant-id",
      "merchantId": "8f3c2a1e-5b7d-4e9a-9c1f-2d6e8a4b7c30",
      "templateName": "Standard POS Config",
      "parameters": {
        "txnSaleEnabled": true,
        "txnRefundEnabled": true,
        "allowTips": true,
        "tipPresets": [10, 15, 20],
        "taxEnabled": true,
        "tax1Label": "Sales Tax",
        "tax1Value": "8.25"
      },
      "isDefault": true,
      "manufacturer": null,
      "deviceType": null,
      "createdBy": "c2b7e4a9-1d3f-4e8b-9a6c-5f0d2e7b8a13",
      "updatedBy": "c2b7e4a9-1d3f-4e8b-9a6c-5f0d2e7b8a13",
      "createdAt": "2026-03-01T00:00:00.000Z",
      "updatedAt": "2026-03-01T00:00:00.000Z",
      "deletedAt": null
    }
  ],
  "meta": {
    "total": 1,
    "limit": 50,
    "offset": 0
  }
}
```

---

### Create Template

```http
POST /api/v1/merchants/:merchantId/device-templates
```

**Request Body:**

| Field          | Type    | Required | Description                                             |
| -------------- | ------- | -------- | ------------------------------------------------------- |
| `templateName` | string  | Yes      | Display name for this template                          |
| `parameters`   | object  | Yes      | Any subset of device parameter fields (flat, raw names) |
| `isDefault`    | boolean | No       | If `true`, replaces any existing default template       |
| `manufacturer` | string  | No       | Limit the template to one manufacturer                  |
| `deviceType`   | string  | No       | Limit the template to one device type                   |

Missing `templateName` or `parameters` returns `400 templateName and parameters are required`.

**Example:**

```bash
curl -X POST "https://uat.auxcore.net/api/v1/merchants/8f3c2a1e-5b7d-4e9a-9c1f-2d6e8a4b7c30/device-templates" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "templateName": "Restaurant Config",
    "isDefault": true,
    "parameters": {
      "txnSaleEnabled": true,
      "txnRefundEnabled": true,
      "txnVoidEnabled": true,
      "txnPreauthEnabled": true,
      "allowTips": true,
      "tipPresets": [15, 18, 20, 25],
      "tipMaxPercentage": 40,
      "taxEnabled": true,
      "tax1Label": "Sales Tax",
      "tax1Value": "8.25",
      "receiptSignatureCapture": false
    }
  }'
```

**Response (201 Created):** Created template object.

> **Side effect:** If `isDefault: true`, any previously default template for this merchant is automatically cleared.

---

### Get Template

```http
GET /api/v1/merchants/:merchantId/device-templates/:templateId
```

---

### Update Template

```http
PATCH /api/v1/merchants/:merchantId/device-templates/:templateId
```

**Request Body (all optional):** `templateName`, `parameters`, `isDefault`, `manufacturer`, `deviceType`.

```json
{
  "templateName": "Updated Restaurant Config",
  "isDefault": false,
  "parameters": {
    "tipPresets": [15, 20, 25]
  }
}
```

---

### Set Template as Default

```http
POST /api/v1/merchants/:merchantId/device-templates/:templateId/set-default
```

**Request Body:** empty

**Response:**

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440099",
    "isDefault": true,
    ...
  }
}
```

> **Side effect:** Clears `isDefault` on the previously default template first, then sets this one. Only one template per merchant can be default at a time.

---

### Delete Template

```http
DELETE /api/v1/merchants/:merchantId/device-templates/:templateId
```

Soft-deletes the template and returns `{ "success": true, "message": "Template deleted" }`. If the deleted template was the default, no template will be marked as default until one is explicitly set.

---

## Reference

### Enums

**DeviceManufacturer:** `ANDROID_PHONE` | `APPLE_PHONE` | `SUNMI`

**DeviceType:** `TAP_ON_PHONE` | `CARD_READER` | `POS_TERMINAL`

**DevicePaymentMode:** `AUXVAULT` | `IPOS` | `NEARPAY`

**PaymentAdjustmentType:** `NONE` | `FLAT` | `PERCENTAGE`

**DeviceStatus:** `PENDING` | `REGISTERED` | `ACTIVE` | `INACTIVE` | `SUSPENDED` | `ARCHIVED`

`ARCHIVED` represents a soft-deleted device (`deletedAt` set). It's reachable only via Archive Device (or Delete Device) and leaveable only via Unarchive Device — never through Update Device Status. The other five statuses are freely inter-transitionable via Update Device Status.

There is no `UNREGISTERED` status — a device released by the app (`POST /api/v1/device/logout`) becomes `INACTIVE`.

**DeviceOsPlatform:** `ANDROID` | `IOS`

`gateway` is not an input. It is the gateway type of the selected `merchantGatewayId` (for example `NMI` or `TsysSierra`) and is `null` when no gateway is set.

### Device Ownership (`registeredByUserId`)

`registeredByUserId` records the merchant user whose JWT was used at `POST /api/v1/device/register`. It is distinct from `createdBy` / `updatedBy`, which record the portal admin who provisioned or edited the device record, and from `assignedUserId`, the team member the device is assigned to.

- While the device is `ACTIVE`, only that user may register it again (recovering after the app lost its stored device token). The pairing code is still required.
- Another user attempting to register an `ACTIVE` device gets `400 Device already registered. Ask admin to deactivate or refresh the pairing code.` — deactivate it or refresh the pairing code to hand it over.
- `null` on devices registered before this field existed; those reject every re-registration while `ACTIVE`.
- It also drives `deviceRegistration.hasActiveDevice` in [`GET /api/v1/auth/my-merchants`](../getting-started/authentication.md#multi-merchant-sessions).

### Parameter Field Names

These flat field names are used in the GET/PUT parameter record and in the template `parameters` object:

| Field Name                     | Section       | Type                                                                                                                                                                  |
| ------------------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `txnSaleEnabled`               | Transactions  | boolean                                                                                                                                                               |
| `txnRefundEnabled`             | Transactions  | boolean                                                                                                                                                               |
| `txnVoidEnabled`               | Transactions  | boolean                                                                                                                                                               |
| `txnPreauthEnabled`            | Transactions  | boolean                                                                                                                                                               |
| `pinVoidEnabled`               | Require PIN   | boolean                                                                                                                                                               |
| `pinRefundEnabled`             | Require PIN   | boolean                                                                                                                                                               |
| `payTypeTapToPayEnabled`       | Payment Types | boolean                                                                                                                                                               |
| `payTypeKeyedInEnabled`        | Payment Types | boolean                                                                                                                                                               |
| `payTypeQrCodeEnabled`         | Payment Types | boolean                                                                                                                                                               |
| `payTypePaymentLinkEnabled`    | Payment Types | boolean                                                                                                                                                               |
| `payTypeAchTransferEnabled`    | Payment Types | boolean                                                                                                                                                               |
| `payTypeCashEnabled`           | Payment Types | boolean                                                                                                                                                               |
| `limitPerTxn`                  | Limits        | decimal string \| null                                                                                                                                                |
| `limitDaily`                   | Limits        | decimal string \| null                                                                                                                                                |
| `limitWeekly`                  | Limits        | decimal string \| null                                                                                                                                                |
| `limitMonthly`                 | Limits        | decimal string \| null                                                                                                                                                |
| `isViewAllMerchantTransaction` | General       | boolean                                                                                                                                                               |
| `signatureThreshold`           | General       | decimal string (negative values become `0.00`)                                                                                                                        |
| `shippingFieldsEnabled`        | Shipping      | boolean                                                                                                                                                               |
| `shippingSameAsBillingVisible` | Shipping      | boolean                                                                                                                                                               |
| `receiptHeaderMsg`             | Receipt       | string \| null                                                                                                                                                        |
| `receiptFooterMsg`             | Receipt       | string \| null                                                                                                                                                        |
| `receiptDisclaimer`            | Receipt       | string \| null                                                                                                                                                        |
| `receiptAutoPrintEnabled`      | Receipt       | boolean                                                                                                                                                               |
| `receiptPrintMerchantCopy`     | Receipt       | boolean                                                                                                                                                               |
| `receiptSmsEnabled`            | Receipt       | boolean                                                                                                                                                               |
| `receiptEmailEnabled`          | Receipt       | boolean                                                                                                                                                               |
| `receiptSignatureCapture`      | Receipt       | boolean                                                                                                                                                               |
| `posLiteEnabled`               | POS           | boolean — **POS Lite**: show the product cart on the POS app (default `true`); returned to the app as `posLite.enabled`. Named `productCartEnabled` before 2026-10-14 |
| `allowTips`                    | Tips          | boolean                                                                                                                                                               |
| `tipPresets`                   | Tips          | number[] \| null                                                                                                                                                      |
| `tipMaxAmount`                 | Tips          | decimal string                                                                                                                                                        |
| `tipMaxPercentage`             | Tips          | number                                                                                                                                                                |
| `taxEnabled`                   | Tax           | boolean                                                                                                                                                               |
| `taxEnableRemove`              | Tax           | boolean                                                                                                                                                               |
| `taxRemovePassProtect`         | Tax           | boolean                                                                                                                                                               |
| `tax1Label`                    | Tax           | string \| null                                                                                                                                                        |
| `tax1Value`                    | Tax           | decimal string \| null                                                                                                                                                |
| `tax2Label`                    | Tax           | string \| null                                                                                                                                                        |
| `tax2Value`                    | Tax           | decimal string \| null                                                                                                                                                |
| `inputFieldsConfig`            | Input Fields  | PaymentFormField[] \| null                                                                                                                                            |

`PaymentFormField` is `{ key, label, group, type, display, required, order, locked? }` with `group` one of `general`, `avs`, `transaction`, `security`, `shipping` and `type` one of `text`, `email`, `phone`, `textarea`, `checkbox`. `locked: true` means the app must not let the cashier edit the field.

### Business Rules

1. **TPN uniqueness:** 12-digit numeric, globally unique. Auto-generated on create.
2. **Pairing code:** 6-digit numeric, unique per tenant. Auto-generated; refreshed via dedicated endpoint.
3. **Default template:** Only one template per merchant can have `isDefault: true`.
4. **Parameter versioning:** `version` increments on every PUT, even when values are unchanged.
5. **Soft deletes:** All deletes set `deletedAt`; records are retained. For devices specifically, this also sets `status` to `ARCHIVED` and is reversible via Unarchive Device.
6. **API key encryption:** AES-256-GCM; never returned in responses; decrypted only at payment time.
7. **Merchant settings sync:** Saving the merchant's Field Config or payment types (VT settings) pushes those rules down to every device's parameters.

---

## Error Responses

```json
{ "success": false, "message": "Missing required fields" }
{ "success": false, "message": "Device not found" }
{ "success": false, "message": "ARCHIVED is not a valid transition target — use the archive endpoint" }
{ "success": false, "message": "Device is not archived" }
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Selected gateway is not active" } }
```

| Status Code | Meaning                                            |
| ----------- | -------------------------------------------------- |
| `200`       | Success                                            |
| `201`       | Created                                            |
| `400`       | Missing or invalid fields                          |
| `403`       | Missing permission, or provider-managed parameters |
| `404`       | Resource not found                                 |
| `422`       | Invalid state transition                           |
| `500`       | Server error                                       |

---

## Next Steps

- [Mobile Device Integration →](../mobile/device-integration.md)
- [Authentication →](../getting-started/authentication.md)
- [Error Codes →](../reference/error-codes.md)

---

**Need help?** Contact support@auxcore.net
