Collect an in-person payment
Set up a VINR terminal and take your first card-present payment in under 10 minutes.
This guide covers everything needed to accept a card-present payment through a VINR terminal: activating the device, creating a payment from your server, handling the result via webhook, and testing before going live. By the end you'll have a working end-to-end in-person payment flow running against the sandbox.
OverviewAsk
your POS server VINR API terminal customer
│ create payment │ │ │
│───────────────────►│ │ │
│ terminalPayment │ present amount │ │
│◄───────────────────│───────────────────►│ │
│ │ │ tap/dip/swipe │
│ │ │◄────────────────│
│ terminal_payment.completed (webhook) │ │
│◄───────────────────│◄───────────────────│ │
│ fulfil order │ │ │PrerequisitesAsk
Before writing any code, confirm you have:
Enable in-person processing
Log in to the VINR Dashboard, go to Settings → Payment methods → In-person, and enable card-present processing for your account.
Activate a supported terminal
VINR supports the Nexgo N92, N86Pro, CT20, CT20P, and the Ciontek CM30. Activate your device and assign it to a location in the Dashboard. The full activation walk-through is at In-person terminal setup.
Obtain API keys
Copy your secret key from Dashboard → Developers → API keys. Use a sandbox key while following this guide.
Activate and assign your terminalAsk
If you haven't yet activated your terminal or assigned it to a location, follow the step-by-step instructions at In-person terminal setup before continuing. The steps below assume the device is already online.
Confirm the terminal is reachable and online before creating your first payment.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const { data: terminals } = await vinr.terminal.terminals.list({ status: 'online' });
console.log(terminals);A healthy terminal returns status: "online" and the locationId you set in the Dashboard. If the device appears offline, check that it is powered on, connected to the network, and has completed its initial software update.
Create a terminal paymentAsk
Once the terminal is online, create a payment from your server. VINR routes it to the terminal and presents the amount to the customer automatically — you do not need a second API call to start the transaction.
const terminalPayment = await vinr.terminal.payments.create({
terminalId: 'term_live_abc123',
amount: 4999,
currency: 'EUR',
reference: 'order-1042',
});
console.log(terminalPayment.status);The response looks like this:
{
id: 'tpay_01j9xkm7qefg8h3nv2wp4c5rd6',
terminalId: 'term_live_abc123',
amount: 4999,
currency: 'EUR',
reference: 'order-1042',
status: 'pending',
createdAt: '2026-06-01T09:14:22Z',
}Once status is pending, the terminal displays the amount and is waiting for the customer to tap, dip, or swipe. No further API call is needed — the transaction completes on the device.
Handle the resultAsk
Listen for the terminal_payment.completed or terminal_payment.failed webhook events to fulfil or handle failure. Fulfilling from the webhook ensures it happens exactly once, even if your server never receives a redirect.
export async function POST(req: Request) {
const event = vinr.webhooks.verify(
await req.text(),
req.headers.get('x-vinr-signature'),
);
switch (event.type) {
case 'terminal_payment.completed': {
const { id, amountCaptured, reference, paymentMethod } = event.data;
console.log(
`Payment ${id} captured ${amountCaptured} — card ending ${paymentMethod.card.last4}`,
);
await fulfillOrder(reference);
break;
}
case 'terminal_payment.failed': {
const { id, reference, failureReason } = event.data;
console.warn(`Payment ${id} failed for order ${reference}: ${failureReason}`);
await notifyStaff(reference);
break;
}
}
return new Response('OK', { status: 200 });
}The terminal_payment.completed payload includes:
| Field | Description |
|---|---|
id | Unique ID of the terminal payment |
status | completed |
amountCaptured | Amount captured in minor units |
reference | The reference you supplied at creation |
paymentMethod.card.last4 | Last four digits of the card used |
Test before going liveAsk
Use the sandbox terminal simulator to exercise both outcomes without a physical device.
const testPayment = await vinr.terminal.payments.create({
terminalId: 'term_test_simulator',
amount: 4999,
currency: 'EUR',
reference: 'order-test-001',
});Then trigger a result in the simulator using these test card numbers:
| Card | Result |
|---|---|
4242 4242 4242 4242 (tap) | terminal_payment.completed |
4000 0000 0000 0002 (tap) | terminal_payment.failed — card declined |
term_test_simulator only works with sandbox API keys. It will be rejected in the live environment.
See Testing your integration for the full list of simulator cards and error scenarios.
Go liveAsk
Switch to live API keys
Replace VINR_SECRET_KEY with your live secret key from Dashboard → Developers → API keys. Update your webhook signing secret at the same time.
Run a real transaction on each terminal model
Process one genuine payment on every terminal model you've deployed (Nexgo N92, N86Pro, CT20, CT20P, Ciontek CM30) before opening to customers. This confirms firmware, network routing, and receipt printing all work as expected.
Complete the go-live checklist
Review error handling, webhook reliability, and reconciliation with the In-person go-live checklist.
Next stepsAsk
In-person payment features
Refunds, tips, receipts, and partial captures on terminal payments.
Terminal management
Assign terminals to locations, update firmware, and monitor status.
Go-live checklist
Everything to verify before accepting real customer payments.
Last updated on