# Terminal management

> Activate, assign, configure, and remotely manage your VINR terminal fleet.

Provision and maintain any number of terminals from the VINR Dashboard or the Management API. Whether you operate a single countertop device or hundreds of handhelds across multiple sites, every terminal in your fleet shares the same lifecycle: activate once, assign to a location, push configuration, and monitor health in real time. No on-site IT visit is required after the initial power-on.

## Activate a terminal

Every VINR terminal arrives factory-reset and ready to pair. Activation registers the device under your account and downloads your location configuration.

**Power on the terminal**

Press and hold the power button until the VINR setup screen appears. All five models — Nexgo N92, Nexgo N86Pro, Nexgo CT20, Nexgo CT20P, and Ciontek CM30 — display an activation QR code and a six-character alphanumeric activation code on first boot.

**Record the activation code**

You can activate by scanning the QR code with the VINR mobile app (Dashboard → Hardware → Scan) or by copying the activation code for use with the API.

**Add the terminal in the Dashboard or via SDK**

In the Dashboard go to **Hardware → Terminals → Add terminal** and enter the activation code. Alternatively, activate programmatically:

##### SDK

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

const terminal = await vinr.terminal.terminals.activate({
  activationCode: 'ABC123',
  label: 'Counter 1 — Main Street',
  locationId: 'loc_01HZ9RSTORE',
});

console.log(terminal.id);
console.log(terminal.status);
```

##### REST

```bash
curl -X POST https://api.vinr.com/v1/terminal/terminals/activate \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "activationCode": "ABC123",
    "label": "Counter 1 — Main Street",
    "locationId": "loc_01HZ9RSTORE"
  }'
```

**Terminal downloads its configuration**

Once activated the device connects to VINR, downloads its location profile (currency, receipt template, feature flags, idle screen), and moves to `online` status. This takes under thirty seconds on a typical connection.

> An activation code is single-use and expires after 24 hours. If the code expires before you complete activation, reboot the terminal to generate a fresh code.

## Assign to a location

A location is the unit of configuration in VINR. Assigning a terminal to a location automatically inherits that location's currency, tax rules, receipt settings, and feature profiles. Move a terminal to a different location at any time — the device re-downloads configuration within seconds.

##### SDK

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

const updated = await vinr.terminal.terminals.update({
  id: 'term_01HZ5QXYZ',
  locationId: 'loc_01HZ9RNORTH',
});

console.log(updated.locationId);
```

##### REST

```bash
curl -X PATCH https://api.vinr.com/v1/terminal/terminals/term_01HZ5QXYZ \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "locationId": "loc_01HZ9RNORTH" }'
```

> Reassigning a terminal clears any per-terminal configuration overrides that differ from the new location profile. Review the destination location's settings before moving a device.

## Configure a terminal

Configuration can be set at the location level (inherited by all terminals in that location) or overridden per terminal. Per-terminal overrides take precedence. Configurable properties include:

- **Feature flags** — tipping, cashback, DCC, offline authorization, contactless limit
- **Receipt settings** — header logo, footer text, email/SMS receipt prompt
- **Idle screen** — custom image or promotional message displayed between transactions
- **Tipping** — preset percentages, fixed amounts, or free-entry; prompt placement (pre- or post-authorization)

##### SDK

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

await vinr.terminal.terminals.configure({
  id: 'term_01HZ5QXYZ',
  tipping: {
    enabled: true,
    presets: [10, 15, 20],
    allowCustom: true,
  },
  receipt: {
    footerText: 'Thank you for visiting VINR Demo Store!',
    promptDigitalReceipt: true,
  },
  idleScreen: {
    imageUrl: 'https://assets.example.com/idle-banner.png',
  },
});
```

##### REST

```bash
curl -X POST https://api.vinr.com/v1/terminal/terminals/term_01HZ5QXYZ/configure \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tipping": { "enabled": true, "presets": [10, 15, 20], "allowCustom": true },
    "receipt": { "footerText": "Thank you!", "promptDigitalReceipt": true },
    "idleScreen": { "imageUrl": "https://assets.example.com/idle-banner.png" }
  }'
```

## Remote actions

Trigger administrative actions on any terminal without physical access. Actions are queued and delivered when the device is next online.

| Action   | Effect                                                                                                      |
| -------- | ----------------------------------------------------------------------------------------------------------- |
| `reboot` | Gracefully restarts the terminal OS. Pending transactions are not affected.                                 |
| `update` | Downloads and installs the latest approved software version for the device model.                           |
| `lock`   | Prevents new transactions until the terminal is explicitly unlocked. Use when a device is reported missing. |
| `unlock` | Re-enables transaction processing after a lock.                                                             |

##### SDK

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

await vinr.terminal.terminals.action({
  id: 'term_01HZ5QXYZ',
  action: 'reboot',
});

await vinr.terminal.terminals.action({
  id: 'term_01HZ5QXYZ',
  action: 'lock',
});
```

##### REST

```bash
curl -X POST https://api.vinr.com/v1/terminal/terminals/term_01HZ5QXYZ/actions \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "lock" }'
```

> Locking a terminal immediately blocks new payment sessions. Any session already in progress on the device will complete normally. Notify affected locations before locking devices during business hours.

## Monitor terminal health

Every terminal reports its status continuously. Use the Dashboard **Hardware → Health** tab for a visual fleet overview, or query the API to integrate terminal health into your own operations tooling.

**Terminal statuses**

| Status            | Meaning                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------- |
| `online`          | Device is reachable and ready to accept payments.                                           |
| `offline`         | Device has not checked in within the expected heartbeat window.                             |
| `needs_attention` | Device requires action — low battery, pending required update, or connectivity degradation. |
| `locked`          | Device is administratively locked; no transactions accepted.                                |
| `deactivated`     | Device has been permanently removed from the fleet.                                         |

##### SDK

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

const { data: terminals } = await vinr.terminal.terminals.list({
  locationId: 'loc_01HZ9RSTORE',
  status: 'needs_attention',
});

for (const terminal of terminals) {
  console.log(terminal.id, terminal.model, terminal.status, terminal.lastSeen);
}
```

##### REST

```bash
curl "https://api.vinr.com/v1/terminal/terminals?locationId=loc_01HZ9RSTORE&status=needs_attention" \
  -H "X-Api-Key: $VINR_SECRET_KEY"
```

## Bulk operations

For fleets with many terminals, use bulk operations to import devices and push configuration changes in a single call.

**CSV import**

Download the bulk activation template from **Hardware → Terminals → Import**. Each row specifies an activation code, a label, and an optional location ID. Upload the completed CSV to register up to 500 terminals at once.

**Bulk configuration push**

Push a shared configuration to every terminal in a location without iterating over individual devices:

##### SDK

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

await vinr.terminal.locations.bulkConfigure({
  locationId: 'loc_01HZ9RSTORE',
  tipping: {
    enabled: true,
    presets: [15, 18, 20],
  },
  receipt: {
    footerText: 'Come back soon!',
  },
});
```

##### REST

```bash
curl -X POST https://api.vinr.com/v1/terminal/locations/loc_01HZ9RSTORE/bulk-configure \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tipping": { "enabled": true, "presets": [15, 18, 20] },
    "receipt": { "footerText": "Come back soon!" }
  }'
```

> Bulk configuration pushes are applied as location-level defaults. Any existing per-terminal overrides remain in place unless you also call `vinr.terminal.terminals.configure` with `clearOverrides: true` on the individual devices.

## Automate terminal management

For large fleets, use the Management API to automate provisioning, configuration, and monitoring. The patterns below are common starting points.

### Provision terminals from a CI/CD pipeline

If terminals arrive in batches — for example, before a seasonal store opening — automate activation from your deployment pipeline rather than the Dashboard:

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

async function provisionBatch(
  activationCodes: string[],
  locationId: string,
) {
  const results = await Promise.allSettled(
    activationCodes.map((code, i) =>
      vinr.terminal.terminals.activate({
        activationCode: code,
        locationId,
        label: `Counter ${i + 1}`,
      })
    )
  );

  const failed = results.filter(r => r.status === 'rejected');
  if (failed.length > 0) {
    console.error(`${failed.length} terminals failed to activate`);
  }
}
```

### Monitor fleet health

Subscribe to `terminal.offline` webhooks or poll the list API to keep an operations dashboard up to date. Use `lastSeen` as the primary staleness signal:

```typescript
const OFFLINE_THRESHOLD_MINUTES = 10;

const { data: terminals } = await vinr.terminal.terminals.list({ limit: 200 });

const offline = terminals.filter(t => {
  const minutesSinceHeartbeat =
    (Date.now() - new Date(t.lastSeen).getTime()) / 60_000;
  return minutesSinceHeartbeat > OFFLINE_THRESHOLD_MINUTES;
});

if (offline.length > 0) {
  await alertOpsChannel(
    `${offline.length} terminals offline: ${offline.map(t => t.id).join(', ')}`
  );
}
```

### Push config changes across all locations

```typescript
const { data: locations } = await vinr.terminal.locations.list();

await Promise.all(
  locations.map(loc =>
    vinr.terminal.locations.bulkConfigure({
      locationId: loc.id,
      tipping: { enabled: true, presets: [15, 18, 20] },
      receipt: { footerText: 'Thank you!' },
    })
  )
);
```

### Deactivate and reassign returned devices

When a field agent returns a device, deactivate it from the old location and prepare it for reuse:

```typescript
await vinr.terminal.terminals.action({
  id: 'term_01HZ5QXYZ',
  action: 'lock',
  reason: 'Returned from field agent — pending reassignment',
});

await vinr.terminal.terminals.action({
  id: 'term_01HZ5QXYZ',
  action: 'rotate_keys',
  reason: 'Pre-reassignment security rotation',
});

await vinr.terminal.terminals.update({
  id: 'term_01HZ5QXYZ',
  locationId: 'loc_warehouse',
  label: 'Spare — unassigned',
});
```

## Terminal object fields

| Field             | Type                | Description                                                                                             | Default |
| ----------------- | ------------------- | ------------------------------------------------------------------------------------------------------- | ------- |
| `id`              | `string`            | Unique terminal identifier. Prefix: term\_.                                                             | `—`     |
| `model`           | ``                  | Hardware model. Determines available connectivity options and whether a built-in printer is present.    | `—`     |
| `serialNumber`    | `string`            | Manufacturer serial number printed on the device label. Immutable.                                      | `—`     |
| `status`          | ``                  | Current operational status of the terminal.                                                             | `—`     |
| `locationId`      | `string`            | The location this terminal is assigned to. Determines currency, receipt config, and feature profiles.   | `—`     |
| `softwareVersion` | `string`            | Currently installed VINR software version on the device, e.g. 4.2.1.                                    | `—`     |
| `lastSeen`        | `string (ISO 8601)` | Timestamp of the most recent successful heartbeat from the device.                                      | `—`     |
| `batteryLevel`    | `number \| null`    | Battery percentage for handheld and mPOS devices (N92, N86Pro, CM30). null for wired countertop models. | `—`     |

#### Advanced — Terminal webhooks, ops monitoring integration, and key rotation

**Terminal lifecycle webhooks**

Subscribe to terminal connectivity events to drive alerting or automated workflows. Register a webhook endpoint in the Dashboard under **Developers → Webhooks** and enable the `terminal.*` event category.

| Event                      | Fired when                                                                |
| -------------------------- | ------------------------------------------------------------------------- |
| `terminal.online`          | A terminal successfully checks in after being offline.                    |
| `terminal.offline`         | A terminal misses two consecutive heartbeats (\~4 minutes of no contact). |
| `terminal.needs_attention` | A terminal transitions to `needs_attention` status.                       |
| `terminal.locked`          | A lock action is applied to a terminal.                                   |

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

app.post('/webhooks/vinr', (req, res) => {
  const event = vinr.webhooks.constructEvent(
    req.body,
    req.headers['vinr-signature'] as string,
    process.env.VINR_WEBHOOK_SECRET!,
  );

  if (event.type === 'terminal.offline') {
    const terminal = event.data;
    alertOpsChannel(`Terminal ${terminal.id} (${terminal.model}) at location ${terminal.locationId} is offline.`);
  }

  res.sendStatus(200);
});
```

**Integrating into your operations monitoring**

`vinr.terminal.terminals.list` returns paginated results with full status fields. Poll it on a schedule (or consume `terminal.offline` webhooks) to feed your existing monitoring stack — Datadog, PagerDuty, OpsGenie, or similar. The `lastSeen` and `batteryLevel` fields are the most useful signals for automated alerting.

For high-volume fleets, prefer webhooks over polling: a `terminal.offline` webhook fires within five minutes of the device going dark, whereas polling on a short interval may hit rate limits on accounts with hundreds of devices.

**Key rotation via Management API**

VINR manages TDES/DUKPT encryption keys automatically. Under normal circumstances you never handle keys directly. If your compliance team requires documented key rotation for audit purposes — for example, after a device is returned from a field agent — initiate a forced key rotation:

```typescript
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

await vinr.terminal.terminals.action({
  id: 'term_01HZ5QXYZ',
  action: 'rotate_keys',
  reason: 'Device returned from field — pre-reassignment rotation',
});
```

The device must be online for key rotation to complete. The terminal will briefly display a "Updating security keys" message and become unavailable for transactions for approximately 30 seconds. Key rotation events are logged in **Hardware → Terminals → \[device] → Audit log** and are available via the Management API audit endpoint for PCI evidence packages.

## Next steps

[Accept a payment](/docs/payments/in-person/accept-a-payment) — Create a terminal payment session, present it to the terminal, and verify the result.

[Terminal hardware](/docs/payments/in-person/terminals) — Device specifications, supported markets, and how to order and provision VINR terminals.

[In-person go-live checklist](/docs/payments/in-person/go-live) — Steps to complete before enabling live card-present acceptance on your fleet.
