Best Practices
Recommendations for building reliable terminal payment integrations
Follow these best practices to build reliable, production-ready terminal payment integrations.
Always Query Devices First
Before initiating a payment, verify the target device is online:
const devices = await revkeen.terminalDevices.list();
const onlineDevice = devices.data.find(
d => d.status === 'online' && d.terminal_paired
);
if (!onlineDevice) {
throw new Error('No online terminal available');
}
const payment = await revkeen.terminalPayments.create({
device_id: onlineDevice.id,
amount_minor: 5000,
currency: 'GBP',
});Check last_heartbeat_at to detect terminals that may have recently gone offline -- a stale heartbeat (older than 5 minutes) indicates the device is unreachable.
Use Webhooks for Results
Terminal payments are asynchronous. The POST /v2/terminal-payments endpoint returns immediately with status: "requested", and the actual result arrives later.
Recommended: Subscribe to billing.terminal_payment.succeeded and billing.terminal_payment.declined webhooks for reliable result delivery.
Alternative: Poll GET /v2/terminal-payments/{id} at intervals, but this adds latency and request overhead.
// ✅ Recommended: Use webhooks
app.post('/webhooks/revkeen', async (req, res) => {
if (req.body.type === 'billing.terminal_payment.succeeded') {
await markOrderPaid(req.body.data.invoice_id);
}
res.json({ received: true });
});
// ⚠️ Alternative: Poll (less efficient)
let payment;
do {
await new Promise(r => setTimeout(r, 2000));
payment = await revkeen.terminalPayments.retrieve(paymentId);
} while (payment.data.status === 'requested' || payment.data.status === 'in_progress');Include Idempotency Keys
Terminal operations involve physical devices and network hops. Network retries without idempotency can cause duplicate charges.
const payment = await revkeen.terminalPayments.create(
{
device_id: deviceId,
amount_minor: 5000,
currency: 'GBP',
},
{
headers: {
'Idempotency-Key': `order-${orderId}-terminal-payment`,
},
}
);Idempotency keys are valid for 24 hours. Use a deterministic key derived from your order/invoice ID to prevent duplicates across retries.
Handle device_busy Gracefully
RevKeen enforces a per-device concurrency lock. If a device is already processing a payment, you'll receive a 409 Conflict:
try {
await revkeen.terminalPayments.create({ ... });
} catch (error) {
if (error.status === 409 && error.body?.error === 'device_busy') {
// Wait for current payment to complete, or try a different device
console.log('Device busy — waiting for current payment to complete');
}
}Do not retry immediately on 409. Either wait for the current payment to complete or route to a different terminal.
Handle Timeouts
The terminal has a 3-minute timeout window. If no card is presented within 3 minutes, the payment automatically transitions to timed_out.
Design your UI and logic to handle this gracefully:
// Check if a payment timed out
const payment = await revkeen.terminalPayments.retrieve(paymentId);
if (payment.data.status === 'timed_out') {
// Safe to retry — the original charge was never processed
const retry = await revkeen.terminalPayments.create({
device_id: payment.data.device_id,
amount_minor: payment.data.amount_minor,
currency: payment.data.currency,
invoice_id: payment.data.invoice_id,
}, {
headers: {
'Idempotency-Key': `retry-${paymentId}`,
},
});
}Multi-Terminal Routing
When a merchant has multiple terminals, implement device selection logic:
async function findBestDevice(): Promise<string> {
const devices = await revkeen.terminalDevices.list();
const online = devices.data.filter(
d => d.status === 'online' && d.terminal_paired
);
if (online.length === 0) {
throw new Error('No online terminals available');
}
// Simple: return first online device
// Advanced: implement round-robin, location-based, or load-balanced routing
return online[0].id;
}PCI Compliance
RevKeen Terminal is designed with PCI compliance in mind:
- No full card data is ever transmitted to or stored by RevKeen. The PAX terminal handles all card data and communicates directly with the payment processor.
- RevKeen stores only masked PANs (last 4 digits), card scheme, and entry mode metadata.
- The
card_schemeandmasked_panfields are safe to log and display in your application. - Never attempt to capture or log full card numbers from any terminal response.
Testing with the Mock Server
Use the OpenAPI mock server for development and testing:
cd packages/openapi
pnpm mockThis starts a mock server on localhost:4010 that returns realistic responses based on the OpenAPI spec examples:
# List devices
curl http://localhost:4010/v2/terminal-devices \
-H "x-api-key: rk_sandbox_test"
# Initiate a payment
curl -X POST http://localhost:4010/v2/terminal-payments \
-H "x-api-key: rk_sandbox_test" \
-H "Content-Type: application/json" \
-d '{"device_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890", "amount_minor": 5000, "currency": "GBP"}'Error Handling Summary
| Scenario | Action |
|---|---|
409 device_busy | Wait or try different device |
422 device_offline | Check connector status, alert ops |
422 device_not_paired | Direct merchant to pairing flow |
status: timed_out | Safe to retry with new idempotency key |
status: declined | Show decline reason, let user retry with different card |
status: error | Check failure_reason, retry if communication_error |
Related
- Payments API -- Full endpoint documentation
- Webhook Events -- Real-time payment event delivery
- Terminal Overview -- How terminal payments work end-to-end