# Local API

> Connect your POS directly to the terminal over your LAN — sub-50 ms activation, offline resilience.

In Local API mode your POS application communicates directly with the terminal over your local network. VINR's cloud handles card authorization but is not in the command path — the terminal screen activates in under 50 ms. Supported devices can also queue and defer authorization when internet connectivity drops.

**Choose Local API when:**

- You need the fastest possible screen activation — fixed-lane retail, high-throughput counters, kiosks
- Your environment has unreliable internet and you need offline payment queuing
- You are building a custom POS application on hardware that shares a LAN with the terminal

For server-side or cloud-hosted POS systems, see [Cloud API](/docs/payments/in-person/cloud-api). To connect an existing ECR or third-party POS system without writing custom code, see [ECR Integration](/docs/payments/in-person/ecr-integration).

## How it works

```
Your POS app ──HTTPS──▶ Terminal (LAN, port 8443)
                               │
                         Card interaction
                               │
                         ──▶ Card network (authorization via internet)
                               │
Your POS app ◀─────────── Terminal (synchronous response)
                               │
                        VINR cloud ──▶ webhook → Your server
```

1. Your POS discovers the terminal on the local network and authenticates with a per-device client certificate.
2. Your POS sends the payment command directly to the terminal's HTTPS endpoint on port 8443. VINR's cloud is not involved at this stage.
3. The terminal handles the card interaction and routes authorization to the card network. If internet is unavailable, it can queue the transaction for deferred authorization (device-dependent).
4. The terminal returns the outcome synchronously. VINR's cloud also fires a webhook to your server after the authorization settles.

## Prerequisites

- Your POS device and the terminal must be on the same LAN or VLAN, or connected via a static route.
- Port **8443** must be open inbound on the terminal's IP address from your POS device.
- Local mode enabled on the terminal: **Dashboard → Hardware → Terminals → \[device] → Connectivity → Enable Local mode**.
- A per-device client certificate provisioned from the Dashboard (see [Security](#security) below).

## Terminal discovery

### mDNS (automatic)

Each VINR terminal advertises `_vinr-terminal._tcp.local` via mDNS. The SDK resolves this automatically on flat networks.

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

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

const readers = await vinr.terminal.readers.discover({ network: 'local' });

console.log(readers);
// [{ id: 'term_CT20_0042', ip: '192.168.1.51', model: 'nexgo_ct20', status: 'online' }]
```

### Static IP (enterprise networks)

Enterprise networks commonly block mDNS across VLANs. Use DHCP reservations at your router and pass static host addresses to bypass discovery:

```typescript
const readers = await vinr.terminal.readers.discover({
  network: 'local',
  staticHosts: ['192.168.10.51', '192.168.10.52', '192.168.10.53'],
});
```

Static hosts connect directly over port 8443 without mDNS. Combine with DHCP reservations so IP addresses remain stable across device reboots.

## Security

All Local API traffic uses **mutual TLS (mTLS)**:

- The terminal presents a VINR-issued per-device certificate. Your POS validates it against the VINR terminal CA bundle, which is bundled with the SDK.
- Your POS presents a client certificate provisioned from the Dashboard. The terminal rejects any request without a valid, matching client certificate.
- **Your API secret key never reaches the terminal.** Terminals carry a separate device credential. Revoking a lost device from the Dashboard does not require rotating your API key.

### Provision a client certificate

Go to **Dashboard → Hardware → Terminals → \[device] → Local API**.

Click **Generate client certificate**. Download the `.p12` bundle and copy the passphrase shown — it will not be displayed again.

Pass the certificate to the SDK:

```typescript
const vinr = new Vinr({
  secretKey: process.env.VINR_SECRET_KEY,
  terminal: {
    localCertPath: '/etc/vinr/client.p12',
    localCertPassphrase: process.env.VINR_LOCAL_CERT_PASSPHRASE,
  },
});
```

The SDK pins the VINR terminal CA and validates the terminal's device certificate on every connection. A certificate mismatch throws before any payment data is exchanged.

## Create a payment

```typescript
const payment = await vinr.terminal.payments.createLocal({
  readerId: 'term_CT20_0042',
  amount: 2500,
  currency: 'USD',
  reference: 'order_8821',
});

// Response is synchronous — the SDK holds the connection until card interaction completes
console.log(payment.status); // 'completed' or 'failed'
console.log(payment.last4, payment.brand);
```

The response object is identical in shape to a Cloud API payment. Both `completed` and `failed` results are returned synchronously — no need to poll or wait for a webhook for the initial outcome.

| Field           | Type                      | Description                                                                                                  | Default       |
| --------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------- |
| `readerId`      | `string`                  | Terminal ID as returned by discover(). The SDK resolves this to the terminal's local IP automatically.       | `—`           |
| `amount`        | `integer`                 | Amount in the smallest currency unit.                                                                        | `—`           |
| `currency`      | `string`                  | ISO 4217 three-letter currency code.                                                                         | `—`           |
| `reference`     | `string`                  | Your order reference. Returned on the webhook event. Used for timeout recovery — must be unique per payment. | `—`           |
| `captureMethod` | `'automatic' \| 'manual'` |                                                                                                              | `'automatic'` |
| `timeoutMs`     | `integer`                 | Client-side connection timeout in milliseconds. Must exceed 120000 (2 min). Defaults to 130000.              | `130000`      |

## Without the SDK (direct HTTP)

If you cannot use the VINR SDK, send a POST request directly to the terminal's HTTPS endpoint. You are responsible for mTLS and certificate pinning.

```
POST https://{terminal-ip}:8443/v1/pay
Content-Type: application/json
```

```json
{
  "terminalId": "term_CT20_0042",
  "amount": 2500,
  "currency": "USD",
  "reference": "order_8821",
  "captureMethod": "automatic"
}
```

Download the VINR terminal CA bundle from **Dashboard → Developers → Certificates** and configure it as the trusted root in your HTTP client. Your `.p12` client certificate must be loaded as the client identity on every request.

> Your HTTP client must hold the connection open for **at least 120 seconds**. Card interactions can take up to 90 seconds including PIN entry and network round-trip time. A shorter timeout leaves the terminal in an indeterminate state.

## Timeout recovery

If your connection times out before receiving a response, **do not immediately create a new payment**. The terminal may have completed the transaction even though your client closed the connection.

Your HTTP call or SDK call times out.

Immediately retrieve the payment by your `reference`:

```typescript
const payment = await vinr.terminal.payments.retrieveByReference('order_8821');
```

If `status` is `completed` or `failed`, use that result. Do **not** retry.

If `status` is `pending`, the terminal is still waiting for card presentation. Cancel the session and create a new one.

> Retrying without checking status first is the most common cause of duplicate charges in Local API integrations. Always retrieve by reference before creating a new payment session.

## Offline payments

When the terminal temporarily loses internet connectivity, supported devices queue transactions and submit them for authorization when connectivity resumes.

| Device       | Offline queuing | Max per-transaction | Max queued |
| ------------ | --------------- | ------------------- | ---------- |
| Nexgo CT20   | Yes             | 150 USD             | 10         |
| Nexgo CT20P  | Yes             | 150 USD             | 10         |
| Nexgo N92    | Yes             | 100 USD             | 5          |
| Nexgo N86Pro | Yes             | 100 USD             | 5          |
| Ciontek CM30 | No              | —                   | —          |

Offline transactions are stored encrypted on the device. On reconnection the terminal submits the queued authorizations and VINR fires `terminal_payment.completed` webhooks for each. If an offline authorization is declined post-reconnect, VINR fires `terminal_payment.failed` with `declineCode: 'offline_auth_declined'`.

> Offline transactions carry elevated fraud risk because issuers cannot decline in real time. Contact your VINR account manager to configure per-device offline limits appropriate for your business.

## Hybrid mode

Hybrid mode uses Local API as the primary path and falls back to Cloud API automatically when the LAN path is unavailable. Recommended when terminals and POS devices share a reliable LAN but cloud fallback is needed for resilience.

```typescript
const payment = await vinr.terminal.payments.create({
  terminalId: 'term_CT20_0042',
  amount: 2500,
  currency: 'USD',
  reference: 'order_8821',
  connectivity: {
    mode: 'hybrid',
    localTimeoutMs: 2000,
  },
});

// which path was used
console.log(payment.metadata.connectionMode); // 'local' or 'cloud'
```

If the local call does not respond within `localTimeoutMs`, the SDK re-issues the command via VINR cloud. The `payment.metadata.connectionMode` field records which path was used — monitor this in your logs to detect degraded LAN conditions before they become customer-facing issues.

## Multi-terminal routing

For high-throughput counters with multiple terminals, pass a group of terminal IDs. VINR selects the first idle terminal; if all are busy, it queues and resolves to whichever becomes free first.

```typescript
const payment = await vinr.terminal.payments.create({
  amount: 1500,
  currency: 'USD',
  reference: 'order_8822',
  terminalGroup: ['term_CT20_0042', 'term_CT20_0043', 'term_CT20_0044'],
});

console.log(payment.terminalId); // ID of the terminal that handled it
```

## Testing

The VINR SDK includes a local simulator that runs on your development machine, behaving identically to a physical terminal on the LAN.

```typescript
const vinr = new Vinr({
  secretKey: 'sk_test_...',
  terminal: { localSimulator: true },
});

const readers = await vinr.terminal.readers.discover({ network: 'local' });
// [{ id: 'term_local_simulator', ip: '127.0.0.1', model: 'simulator' }]

const payment = await vinr.terminal.payments.createLocal({
  readerId: 'term_local_simulator',
  amount: 1000,
  currency: 'USD',
  reference: 'local_test_001',
});
```

The simulator supports all test card numbers from the Cloud API. Pass `simulateOffline: true` in the options to test offline queuing behaviour without disconnecting any hardware.

## Next steps

[ECR Integration](/docs/payments/in-person/ecr-integration) — Connect an existing ECR or POS system to a VINR terminal using the VINR Cashless API.

[Terminal management](/docs/payments/in-person/terminal-management) — Activate, assign, and monitor your terminal fleet.

[Offline payments](/docs/payments/in-person/offline-payments) — Configure offline queuing limits and deferred authorization behaviour.
