PaymentsIn-Person PaymentsLocal API

Local API

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

View as MarkdownInstall skills

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. To connect an existing ECR or third-party POS system without writing custom code, see ECR Integration.

How it worksAsk

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.

PrerequisitesAsk

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

Terminal discoveryAsk

mDNS (automatic)

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

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:

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.

SecurityAsk

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:

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 paymentAsk

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.

Prop

Type

Without the SDK (direct HTTP)Ask

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
{
  "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 recoveryAsk

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:

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 paymentsAsk

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

DeviceOffline queuingMax per-transactionMax queued
Nexgo CT20Yes150 USD10
Nexgo CT20PYes150 USD10
Nexgo N92Yes100 USD5
Nexgo N86ProYes100 USD5
Ciontek CM30No

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 modeAsk

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.

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 routingAsk

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.

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

TestingAsk

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

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 stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page