RevKeen Docs

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_scheme and masked_pan fields 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 mock

This 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

ScenarioAction
409 device_busyWait or try different device
422 device_offlineCheck connector status, alert ops
422 device_not_pairedDirect merchant to pairing flow
status: timed_outSafe to retry with new idempotency key
status: declinedShow decline reason, let user retry with different card
status: errorCheck failure_reason, retry if communication_error

On this page