Kiosk PMI Integration Guide

Complete integration reference for self-service kiosk terminals using the NCR PMI REST API — covering engine startup at mount, out-of-order handling, contactless payment, and the full unattended payment lifecycle.

A self-service kiosk has no cashier and no sign-in screen. The moment the kiosk boots, it must independently verify the payment engine is ready, initialize the PIN pad, and open the lane — all before a customer even touches the screen. This guide covers the kiosk-specific startup sequence, how to gracefully handle failures, and the complete payment flow from order review to receipt.

Onboarding & prerequisites

Before your kiosk application can accept payments you need:

  • A valid NCR PMI merchant account with credentials (username, password, and lane ID)
  • CommonClientSDK installed and running on the kiosk machine (default: http://localhost:8600)
  • A supported PIN pad / card reader connected and powered (e.g. Ingenico, Verifone, or PAX terminal)
  • Your integration credentials stored securely — never hard-coded in application source
⚠️ Security note

Kiosk terminals are physically accessible to the public. Store PMI credentials in environment variables or a secrets vault. The CommonClientSDK runs locally on the device, so all PMI API calls are over localhost only and never traverse the public internet.

Kiosk supported features

The following kiosk-focused capabilities are derived from CommonClientSDK/Docs/comcli-CCL supported features-090626-180108.pdf. These settings are useful when running unattended, self-service lanes.

CapabilityDescriptionConfigurationKiosk guidance
Lane type (Unattended) Sets terminal capabilities based on lane context. HostProcessor → Option Name="LaneType" For kiosk lanes, set lane type to Unattended instead of the default Attended.
Async card entry Enables swipe-ahead behavior, including tap/insert where supported by the PIN pad. PinpadProcessor → Option Name="EnableAsyncCardEntry" Recommended for unattended checkout to reduce customer wait time.
Alt ID with card entry Allows alternate ID prompts (for loyalty and related flows) during card-entry workflows. PinpadProcessorGeneric → Option Name="GetCardEntryWithAltId" Use only when your kiosk journey includes loyalty/alternate ID capture.
Fee prompt support Controls service/surcharge confirmation prompt on the PIN pad (POS support required). CardProcessor → Option Name="FeeSupported" Keep disabled unless your compliance and UX requirements explicitly need it.
8-digit BIN support Document notes CCL BIN processing support by device family/version. CCL feature support note in the matrix section Validate lane device/firmware compatibility before enabling BIN-dependent rules.
Document source

The source matrix in the CCL PDF is broader than kiosk flows. This section lists the kiosk-relevant subset to keep integration guidance focused and implementation-ready.

Supported device matrix

Device and OS compatibility snapshot from the same CCL document (as-of 03/30/2026).

DeviceWindowsAndroidLinux (Ubuntu)Connection
Lane8000✓X✓USB, TCPIP
Lane7000✓X✓USB
Link2500✓✓✓USB, Bluetooth
Axium8000X✓XUSB
RP457X✓XUSB
MOBY5500-CL3X✓XUSB
Neo M425✓X✓USB, TCPIP
Neo P630✓X✓USB, TCPIP
VF Engage-P400✓X✓USB, TCPIP
VF Engage-M400✓X✓USB, TCPIP
LUXE6200✓X✓HTTP
LUXE8500✓X✓HTTP
LUXE8700✓X✓HTTP
Validation before production

Before rollout, verify your exact device model, connection transport, and CCL package level in your target environment because support can change by release and profile.

Flow diagrams

Startup decision flow

Flow
GetEngineStatus
  -> EngineStatusCode = 0  -> READY
  -> EngineStatusCode = 1  -> Start -> Initialize -> OpenLane -> READY
  -> EngineStatusCode = 2  -> Initialize -> OpenLane -> READY
  -> EngineStatusCode = 3  -> OpenLane -> READY

Purchase transaction flow

Flow
BeginPaymentSession
  -> StartNotifications
  -> GetCardDetails
  -> Purchase
  -> EndPaymentSession

Startup flow — runs on application load

Step 1
GetEngineStatus

Query the current engine state. Returns a status code that determines which startup steps are needed.

Step 2 (if needed)
Start

Only called when status = 1 (StartNeeded). Starts the PMI engine process.

Step 3 (if needed)
Initialize

Only called when status ≤ 2. Downloads configuration and authenticates with the payment host.

Step 4 (if needed)
OpenLane

Only called when status ≤ 3. Activates the PIN pad and opens the lane for card reads.

💡 Smart skip logic

Each step is conditional. If GetEngineStatus returns 0 (Ready), the lane is already open and all startup steps are skipped. If it returns 3 (OpenLaneNeeded), only OpenLane is called. This avoids unnecessary API calls on every application start.

Purchase flow — runs on "Pay Now"

Step 1
BeginPaymentSession

Opens a session with the order amount and tip. Returns a SessionTranID used in all subsequent calls.

Step 2
StartNotifications

Activates the PIN pad display so the customer sees "Tap / Insert / Swipe". The terminal is now waiting for a card.

Step 3
GetCardDetails

Reads the card after the customer presents it. Returns the masked PAN, card brand, and entry method.

Step 4
Purchase

Submits the authorization to the payment host. Returns approval or decline with an auth code.

Step 5
EndPaymentSession

Always called — on approval, decline, timeout, or error. Resets the PIN pad for the next customer.

Purchase scenario diagram

Kiosk App Customer UI PMI WebServer localhost:8600 PIN Pad Tap / Insert / Swipe Payment Host Gateway Customer taps PAY NOW 1) BeginPaymentSession <- SessionTranID 2) StartNotifications PMI activates card reader prompt 3) GetCardDetails <- PaymentTranID, CardBrand, LastFour 4) Purchase Authorization at gateway <- AuthCode, AmountApproved 5) EndPaymentSession Session closed and lane returns to ready for the next customer
Session rule

Every BeginPaymentSession must be followed by EndPaymentSession — even on error or cancellation. A session left open blocks all further payment operations on that lane until it times out.

Engine startup sequence

Run this sequence when the application starts. Each step is conditional based on the EngineStatusCode returned by GetEngineStatus. All four calls share the same ConfigOptions block.

Query engine status — GetEngineStatus

Always the first call. The EngineStatusCode in the response controls which steps follow.

curl -X POST https://localhost:8600/PMI/GetEngineStatus \
  -H "Content-Type: application/json" \
  -H "X-ClientId: KIOSK1" \
  --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1",
      "MessageType": "Request",
      "Operation": "GetEngineStatus",
      "RequestID": "k001",
      "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 }
    }
  }'

# Response
# { "Message": { "StatusCode": 0, "EngineStatusCode": 3, "StatusDescription": "OpenLaneNeeded" } }
# EngineStatusCode: 0=Ready, 1=StartNeeded, 2=InitializeNeeded, 3=OpenLaneNeeded
const response = await fetch('https://localhost:8600/PMI/GetEngineStatus', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({
    Message: {
      ClientID: 'KIOSK1', MessageType: 'Request',
      Operation: 'GetEngineStatus', RequestID: 'k001', Version: '4.7.0',
      ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
    }
  })
});
const { Message } = await response.json();
const engineStatusCode = Message.EngineStatusCode;
// 0 = Ready, 1 = StartNeeded, 2 = InitializeNeeded, 3 = OpenLaneNeeded
import requests, urllib3
urllib3.disable_warnings()

resp = requests.post(
    'https://localhost:8600/PMI/GetEngineStatus',
    headers={'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1'},
    json={
        'Message': {
            'ClientID': 'KIOSK1', 'MessageType': 'Request',
            'Operation': 'GetEngineStatus', 'RequestID': 'k001', 'Version': '4.7.0',
            'ConfigOptions': {'CompanyNumber': 185197, 'StoreNumber': 1, 'LaneNumber': 1}
        }
    }, verify=False
)
engine_status_code = resp.json()['Message']['EngineStatusCode']
# 0 = Ready, 1 = StartNeeded, 2 = InitializeNeeded, 3 = OpenLaneNeeded
<?php
$body = json_encode([
    'Message' => [
        'ClientID' => 'KIOSK1', 'MessageType' => 'Request',
        'Operation' => 'GetEngineStatus', 'RequestID' => 'k001', 'Version' => '4.7.0',
        'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1]
    ]
]);
$ch = curl_init('https://localhost:8600/PMI/GetEngineStatus');
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
$engineStatusCode = $msg['EngineStatusCode'];
// 0 = Ready, 1 = StartNeeded, 2 = InitializeNeeded, 3 = OpenLaneNeeded
using System.Net.Http.Json;
using System.Text.Json;

var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "KIOSK1");

var payload = new {
    Message = new {
        ClientID = "KIOSK1", MessageType = "Request",
        Operation = "GetEngineStatus", RequestID = "k001", Version = "4.7.0",
        ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 }
    }
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/GetEngineStatus", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
var engineStatusCode = root.GetProperty("Message").GetProperty("EngineStatusCode").GetInt32();
// 0 = Ready, 1 = StartNeeded, 2 = InitializeNeeded, 3 = OpenLaneNeeded
require 'net/http'; require 'json'; require 'openssl'

uri = URI('https://localhost:8600/PMI/GetEngineStatus')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json', 'X-ClientId' => 'KIOSK1')
req.body = { Message: {
  ClientID: 'KIOSK1', MessageType: 'Request',
  Operation: 'GetEngineStatus', RequestID: 'k001', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
}}.to_json

engine_status_code = JSON.parse(http.request(req).body)['Message']['EngineStatusCode']
# 0 = Ready, 1 = StartNeeded, 2 = InitializeNeeded, 3 = OpenLaneNeeded
import java.net.http.*; import java.net.URI;

// buildTrustAllContext() — see Retail PMI guide for helper
HttpClient client = HttpClient.newBuilder().sslContext(buildTrustAllContext()).build();

String body = """{"Message":{"ClientID":"KIOSK1","MessageType":"Request",
  "Operation":"GetEngineStatus","RequestID":"k001","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1}}}""";

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/GetEngineStatus"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();

var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Parse resp.body() → Message.EngineStatusCode
// 0=Ready, 1=StartNeeded, 2=InitializeNeeded, 3=OpenLaneNeeded
package main

import ( "bytes"; "crypto/tls"; "encoding/json"; "net/http" )

func getEngineStatus(client *http.Client) int {
    body, _ := json.Marshal(map[string]any{
        "Message": map[string]any{
            "ClientID": "KIOSK1", "MessageType": "Request",
            "Operation": "GetEngineStatus", "RequestID": "k001", "Version": "4.7.0",
            "ConfigOptions": map[string]any{"CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1},
        },
    })
    req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/GetEngineStatus", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-ClientId", "KIOSK1")
    resp, _ := client.Do(req)
    var result map[string]any
    json.NewDecoder(resp.Body).Decode(&result)
    // 0=Ready, 1=StartNeeded, 2=InitializeNeeded, 3=OpenLaneNeeded
    return int(result["Message"].(map[string]any)["EngineStatusCode"].(float64))
}

Start engine (only when EngineStatusCode = 1)

Starts the PMI engine process. Skip if the status code is 2 or 3 — the engine is already running.

curl -X POST https://localhost:8600/PMI/Start \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{"Message":{"ClientID":"KIOSK1","MessageType":"Request","Operation":"Start",
       "RequestID":"k002","Version":"4.7.0",
       "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1}}}'

# Response: { "Message": { "StatusCode": 0, "StatusDescription": "Engine started" } }
const resp = await fetch('https://localhost:8600/PMI/Start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({ Message: {
    ClientID: 'KIOSK1', MessageType: 'Request',
    Operation: 'Start', RequestID: 'k002', Version: '4.7.0',
    ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
  }})
});
const { Message } = await resp.json();
if (Message.StatusCode !== 0) throw new Error('Start failed: ' + Message.StatusDescription);
resp = requests.post('https://localhost:8600/PMI/Start',
    headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
    json={'Message':{'ClientID':'KIOSK1','MessageType':'Request','Operation':'Start',
          'RequestID':'k002','Version':'4.7.0',
          'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1}}},
    verify=False)
msg = resp.json()['Message']
if msg['StatusCode'] != 0: raise RuntimeError('Start failed: ' + msg['StatusDescription'])
<?php
$body = json_encode(['Message' => ['ClientID' => 'KIOSK1', 'MessageType' => 'Request',
    'Operation' => 'Start', 'RequestID' => 'k002', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1]]]);
$ch = curl_init('https://localhost:8600/PMI/Start');
curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,
    CURLOPT_SSL_VERIFYPEER=>false,
    CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS=>$body]);
$msg = json_decode(curl_exec($ch),true)['Message']; curl_close($ch);
if ($msg['StatusCode'] !== 0) throw new RuntimeException('Start failed');
var payload = new { Message = new {
    ClientID = "KIOSK1", MessageType = "Request", Operation = "Start",
    RequestID = "k002", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 }
}};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/Start", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (root.GetProperty("Message").GetProperty("StatusCode").GetInt32() != 0)
    throw new Exception("Start failed");
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json', 'X-ClientId' => 'KIOSK1')
req.body = { Message: { ClientID: 'KIOSK1', MessageType: 'Request', Operation: 'Start',
  RequestID: 'k002', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }}}.to_json
msg = JSON.parse(http.request(req).body)['Message']
raise "Start failed: #{msg['StatusDescription']}" unless msg['StatusCode'] == 0
String body = """{"Message":{"ClientID":"KIOSK1","MessageType":"Request","Operation":"Start",
  "RequestID":"k002","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1}}}""";
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/Start"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
client.send(req, HttpResponse.BodyHandlers.ofString()); // check StatusCode == 0
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID":"KIOSK1","MessageType":"Request","Operation":"Start",
    "RequestID":"k002","Version":"4.7.0",
    "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
}})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/Start", bytes.NewBuffer(body))
req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
client.Do(req) // check Message.StatusCode == 0

Initialize (only when EngineStatusCode ≤ 2)

Downloads the merchant configuration from the NCR payment host and discovers connected PIN pad devices. Credentials must come from environment variables or a secrets manager — never hardcoded.

curl -X POST https://localhost:8600/PMI/Initialize \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1", "MessageType": "Request",
      "Operation": "Initialize", "RequestID": "k003", "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 }
    }
  }'

# Response
# { "Message": { "StatusCode": 0, "StatusDescription": "Initialize successful",
#     "DevicesFound": ["Ingenico_iPP320_USB"] } }
const resp = await fetch('https://localhost:8600/PMI/Initialize', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({ Message: {
    ClientID: 'KIOSK1', MessageType: 'Request',
    Operation: 'Initialize', RequestID: 'k003', Version: '4.7.0',
    ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
  }})
});
const { Message } = await resp.json();
if (Message.StatusCode !== 0) throw new Error('Initialize failed: ' + Message.StatusDescription);
console.log('Devices found:', Message.DevicesFound);
resp = requests.post('https://localhost:8600/PMI/Initialize',
    headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
    json={'Message':{'ClientID':'KIOSK1','MessageType':'Request','Operation':'Initialize',
          'RequestID':'k003','Version':'4.7.0',
          'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1}}},
    verify=False)
msg = resp.json()['Message']
if msg['StatusCode'] != 0: raise RuntimeError('Initialize failed: ' + msg['StatusDescription'])
print('Devices found:', msg.get('DevicesFound', []))
<?php
$body = json_encode(['Message' => ['ClientID' => 'KIOSK1', 'MessageType' => 'Request',
    'Operation' => 'Initialize', 'RequestID' => 'k003', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1]]]);
$ch = curl_init('https://localhost:8600/PMI/Initialize');
curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,
    CURLOPT_SSL_VERIFYPEER=>false,
    CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS=>$body]);
$msg = json_decode(curl_exec($ch),true)['Message']; curl_close($ch);
if ($msg['StatusCode'] !== 0) throw new RuntimeException('Initialize failed');
var payload = new { Message = new {
    ClientID = "KIOSK1", MessageType = "Request", Operation = "Initialize",
    RequestID = "k003", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 }
}};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/Initialize", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (root.GetProperty("Message").GetProperty("StatusCode").GetInt32() != 0)
    throw new Exception("Initialize failed");
req.body = { Message: { ClientID: 'KIOSK1', MessageType: 'Request', Operation: 'Initialize',
  RequestID: 'k003', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }}}.to_json
msg = JSON.parse(http.request(req).body)['Message']
raise "Initialize failed: #{msg['StatusDescription']}" unless msg['StatusCode'] == 0
String body = """{"Message":{"ClientID":"KIOSK1","MessageType":"Request","Operation":"Initialize",
  "RequestID":"k003","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1}}}""";
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/Initialize"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
client.send(req, HttpResponse.BodyHandlers.ofString()); // check StatusCode == 0
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID":"KIOSK1","MessageType":"Request","Operation":"Initialize",
    "RequestID":"k003","Version":"4.7.0",
    "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
}})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/Initialize", bytes.NewBuffer(body))
req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
client.Do(req) // check Message.StatusCode == 0

OpenLane (only when EngineStatusCode ≤ 3)

Activates the PIN pad and opens the payment lane. After this call the terminal is ready to read cards. On a kiosk the lane stays open indefinitely — it is not closed between customer transactions.

curl -X POST https://localhost:8600/PMI/OpenLane \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1", "MessageType": "Request",
      "Operation": "OpenLane", "RequestID": "k004", "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 }
    }
  }'

# Response: { "Message": { "StatusCode": 0, "StatusDescription": "Lane opened successfully" } }
# Application is now ready for customer transactions
const resp = await fetch('https://localhost:8600/PMI/OpenLane', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({ Message: {
    ClientID: 'KIOSK1', MessageType: 'Request',
    Operation: 'OpenLane', RequestID: 'k004', Version: '4.7.0',
    ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
  }})
});
const { Message } = await resp.json();
if (Message.StatusCode !== 0) throw new Error('OpenLane failed: ' + Message.StatusDescription);
// Application is now ready for customer transactions
resp = requests.post('https://localhost:8600/PMI/OpenLane',
    headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
    json={'Message':{'ClientID':'KIOSK1','MessageType':'Request','Operation':'OpenLane',
          'RequestID':'k004','Version':'4.7.0',
          'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1}}},
    verify=False)
msg = resp.json()['Message']
if msg['StatusCode'] != 0: raise RuntimeError('OpenLane failed: ' + msg['StatusDescription'])
# Application is now ready for customer transactions
<?php
$body = json_encode(['Message' => ['ClientID' => 'KIOSK1', 'MessageType' => 'Request',
    'Operation' => 'OpenLane', 'RequestID' => 'k004', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1]]]);
$ch = curl_init('https://localhost:8600/PMI/OpenLane');
curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,
    CURLOPT_SSL_VERIFYPEER=>false,
    CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS=>$body]);
$msg = json_decode(curl_exec($ch),true)['Message']; curl_close($ch);
if ($msg['StatusCode'] !== 0) throw new RuntimeException('OpenLane failed');
// Application is now ready for customer transactions
var payload = new { Message = new {
    ClientID = "KIOSK1", MessageType = "Request", Operation = "OpenLane",
    RequestID = "k004", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 }
}};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/OpenLane", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (root.GetProperty("Message").GetProperty("StatusCode").GetInt32() != 0)
    throw new Exception("OpenLane failed");
// Application is now ready for customer transactions
req.body = { Message: { ClientID: 'KIOSK1', MessageType: 'Request', Operation: 'OpenLane',
  RequestID: 'k004', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }}}.to_json
msg = JSON.parse(http.request(req).body)['Message']
raise "OpenLane failed: #{msg['StatusDescription']}" unless msg['StatusCode'] == 0
# Application is now ready for customer transactions
String body = """{"Message":{"ClientID":"KIOSK1","MessageType":"Request","Operation":"OpenLane",
  "RequestID":"k004","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1}}}""";
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/OpenLane"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
client.send(req, HttpResponse.BodyHandlers.ofString());
// Check StatusCode == 0 — application is now ready for customer transactions
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID":"KIOSK1","MessageType":"Request","Operation":"OpenLane",
    "RequestID":"k004","Version":"4.7.0",
    "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
}})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/OpenLane", bytes.NewBuffer(body))
req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
client.Do(req)
// Check Message.StatusCode == 0 — application is now ready for customer transactions

Engine status codes

CodeNameMeaningNext step
0ReadyEngine fully initialized, lane openNone — proceed to payment
1StartNeededPMI engine process is not runningCall Start → Initialize → OpenLane
2InitializeNeededEngine running but not configuredCall Initialize → OpenLane
3OpenLaneNeededConfigured but lane not openCall OpenLane only

Out-of-order handling

If any step in the startup sequence fails — device not connected, wrong credentials, network error — the application must enter an out-of-order state. Because there is no operator, the kiosk cannot prompt for a retry.

  • Display a clear "Out of Order — Please see a staff member" message to the customer
  • Disable all payment-related UI (category tiles, "Pay Now" button)
  • Do not allow the customer to reach the payment screen
  • Log the specific error (device name, error code, timestamp) for the service team
⛔ Never silently bypass a startup failure

If OpenLane failed, the PIN pad is not ready. Allowing a customer to reach the payment screen and calling BeginPaymentSession will return an error that is confusing to the customer. Block at the UI level as soon as startup fails.

Standard kiosk purchase

🖥️ Scenario: Customer reviews order and taps "Pay Now"

The primary kiosk payment use case. The customer builds their order, optionally selects a tip on-screen, then taps Pay Now. The following five API calls complete the purchase end-to-end.

Open a payment session — BeginPaymentSession

Opens a session with the final order total. The returned SessionTranID is required in all subsequent calls.

curl -X POST https://localhost:8600/PMI/BeginPaymentSession \
  -H "Content-Type: application/json" \
  -H "X-ClientId: KIOSK1" \
  --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1",
      "MessageType": "Request",
      "Operation": "BeginPaymentSession",
      "RequestID": "k101",
      "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 }
    }
  }'

# Response
# { "Message": { "StatusCode": 0,
#     "PaymentDetails": { "SessionTranID": "ccl-session-k42" } } }
const response = await fetch('https://localhost:8600/PMI/BeginPaymentSession', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({
    Message: {
      ClientID: 'KIOSK1', MessageType: 'Request',
      Operation: 'BeginPaymentSession', RequestID: 'k101', Version: '4.7.0',
      ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
    }
  })
});
const { Message } = await response.json();
if (Message.StatusCode !== 0) throw new Error('BeginPaymentSession failed');
const sessionTranID = Message.PaymentDetails.SessionTranID;
resp = requests.post(
    'https://localhost:8600/PMI/BeginPaymentSession',
    headers={'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1'},
    json={'Message': {
        'ClientID': 'KIOSK1', 'MessageType': 'Request',
        'Operation': 'BeginPaymentSession', 'RequestID': 'k101', 'Version': '4.7.0',
        'ConfigOptions': {'CompanyNumber': 185197, 'StoreNumber': 1, 'LaneNumber': 1}
    }}, verify=False
)
msg = resp.json()['Message']
if msg['StatusCode'] != 0: raise RuntimeError('BeginPaymentSession failed')
session_tran_id = msg['PaymentDetails']['SessionTranID']
<?php
$body = json_encode(['Message' => [
    'ClientID' => 'KIOSK1', 'MessageType' => 'Request',
    'Operation' => 'BeginPaymentSession', 'RequestID' => 'k101', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1]
]]);
$ch = curl_init('https://localhost:8600/PMI/BeginPaymentSession');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS => $body]);
$msg = json_decode(curl_exec($ch), true)['Message']; curl_close($ch);
if ($msg['StatusCode'] !== 0) throw new RuntimeException('BeginPaymentSession failed');
$sessionTranID = $msg['PaymentDetails']['SessionTranID'];
var payload = new { Message = new {
    ClientID = "KIOSK1", MessageType = "Request",
    Operation = "BeginPaymentSession", RequestID = "k101", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 }
}};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/BeginPaymentSession", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (root.GetProperty("Message").GetProperty("StatusCode").GetInt32() != 0)
    throw new Exception("BeginPaymentSession failed");
var sessionTranID = root.GetProperty("Message").GetProperty("PaymentDetails")
                        .GetProperty("SessionTranID").GetString();
require 'net/http'; require 'json'; require 'openssl'

uri = URI('https://localhost:8600/PMI/BeginPaymentSession')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true; http.verify_mode = OpenSSL::SSL::VERIFY_NONE

req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json', 'X-ClientId' => 'KIOSK1')
req.body = { Message: {
  ClientID: 'KIOSK1', MessageType: 'Request',
  Operation: 'BeginPaymentSession', RequestID: 'k101', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 }
}}.to_json

msg = JSON.parse(http.request(req).body)['Message']
raise 'BeginPaymentSession failed' unless msg['StatusCode'] == 0
session_tran_id = msg['PaymentDetails']['SessionTranID']
import java.net.http.*; import java.net.URI;

String body = """{"Message":{"ClientID":"KIOSK1","MessageType":"Request",
  "Operation":"BeginPaymentSession","RequestID":"k101","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1}}}""";

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/BeginPaymentSession"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Parse resp.body() → Message.PaymentDetails.SessionTranID
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID": "KIOSK1", "MessageType": "Request",
    "Operation": "BeginPaymentSession", "RequestID": "k101", "Version": "4.7.0",
    "ConfigOptions": map[string]any{"CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1},
}})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/BeginPaymentSession", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json"); req.Header.Set("X-ClientId", "KIOSK1")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
sessionTranID := result["Message"].(map[string]any)["PaymentDetails"].(map[string]any)["SessionTranID"].(string)
JSON — Request
{
  "LaneId":        "1",
  "TransactionId": "kiosk-20240115-0042",
  "Amount":        1250,
  "TipAmount":     150
}

Amount values are in cents (integer). A $12.50 subtotal with a $1.50 tip → Amount: 1250, TipAmount: 150.

JSON — Response
{
  "SessionId":   "sess_abc123",
  "ResultCode":  0,
  "ResultMessage": "Session opened"
}

Activate the PIN pad — StartNotifications

Activates the card reader and shows "Tap / Insert / Swipe" on the terminal. Call immediately after opening the session so the customer can present their card while your UI transitions to the waiting state.

curl -X POST https://localhost:8600/PMI/StartNotifications \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1", "Operation": "StartNotifications",
      "RequestID": "k102", "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 },
      "PaymentDetails": { "SessionTranID": "ccl-session-k42" }
    }
  }'

# Response: { "Message": { "StatusCode": 0 } }
# PIN pad now shows: TAP / INSERT / SWIPE
const resp = await fetch('https://localhost:8600/PMI/StartNotifications', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({ Message: {
    ClientID: 'KIOSK1', Operation: 'StartNotifications',
    RequestID: 'k102', Version: '4.7.0',
    ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
    PaymentDetails: { SessionTranID: sessionTranID }
  }})
});
// PIN pad now shows: TAP / INSERT / SWIPE
requests.post('https://localhost:8600/PMI/StartNotifications',
    headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
    json={'Message':{'ClientID':'KIOSK1','Operation':'StartNotifications',
          'RequestID':'k102','Version':'4.7.0',
          'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1},
          'PaymentDetails':{'SessionTranID':session_tran_id}}},
    verify=False)
# PIN pad now shows: TAP / INSERT / SWIPE
<?php
$body = json_encode(['Message' => [
    'ClientID' => 'KIOSK1', 'Operation' => 'StartNotifications',
    'RequestID' => 'k102', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1],
    'PaymentDetails' => ['SessionTranID' => $sessionTranID]
]]);
$ch = curl_init('https://localhost:8600/PMI/StartNotifications');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS => $body]);
curl_exec($ch); curl_close($ch);
// PIN pad now shows: TAP / INSERT / SWIPE
await client.PostAsJsonAsync("https://localhost:8600/PMI/StartNotifications", new { Message = new {
    ClientID = "KIOSK1", Operation = "StartNotifications",
    RequestID = "k102", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 },
    PaymentDetails = new { SessionTranID = sessionTranID }
}});
// PIN pad now shows: TAP / INSERT / SWIPE
req.body = { Message: { ClientID: 'KIOSK1', Operation: 'StartNotifications',
  RequestID: 'k102', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
  PaymentDetails: { SessionTranID: session_tran_id }}}.to_json
http.request(req)
# PIN pad now shows: TAP / INSERT / SWIPE
String body = String.format("""{"Message":{"ClientID":"KIOSK1","Operation":"StartNotifications",
  "RequestID":"k102","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
  "PaymentDetails":{"SessionTranID":"%s"}}}""", sessionTranID);
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/StartNotifications"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
client.send(req, HttpResponse.BodyHandlers.ofString());
// PIN pad now shows: TAP / INSERT / SWIPE
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID":"KIOSK1","Operation":"StartNotifications",
    "RequestID":"k102","Version":"4.7.0",
    "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
    "PaymentDetails":map[string]any{"SessionTranID":sessionTranID},
}})
req, _ := http.NewRequest("POST","https://localhost:8600/PMI/StartNotifications",bytes.NewBuffer(body))
req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
client.Do(req) // PIN pad now shows: TAP / INSERT / SWIPE

Card reader notification events

EventMeaningAction
CardInsertedCard inserted into chip readerCall GetCardDetails
CardTappedContactless card / phone detectedCall GetCardDetails
CardSwipedCard swiped through MSRCall GetCardDetails
CardRemovedCard removed before read completePrompt customer to re-present card
SessionTimeoutNo card presented in timeCall EndPaymentSession, show timeout message

Read the card — GetCardDetails

Call after the customer presents their card. Returns the masked PAN, card brand, and entry method. The returned PaymentTranID is required for the Purchase call.

curl -X POST https://localhost:8600/PMI/GetCardDetails \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1", "Operation": "GetCardDetails",
      "RequestID": "k103", "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 },
      "Amounts": { "AmountDue": "18.50", "AmountTendered": "18.50" },
      "PaymentDetails": { "SessionTranID": "ccl-session-k42" },
      "CardDetails": [{ "CardType": "Credit" }]
    }
  }'

# Response
# { "Message": { "StatusCode": 0,
#     "PaymentDetails": { "PaymentTranID": "pmt-k42-001" },
#     "CardDetails": [{ "CardBrand": "Visa", "LastFour": "4242", "EntryMethod": "Tap" }] } }
const response = await fetch('https://localhost:8600/PMI/GetCardDetails', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({
    Message: {
      ClientID: 'KIOSK1', Operation: 'GetCardDetails',
      RequestID: 'k103', Version: '4.7.0',
      ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
      Amounts: { AmountDue: '18.50', AmountTendered: '18.50' },
      PaymentDetails: { SessionTranID: sessionTranID },
      CardDetails: [{ CardType: 'Credit' }]
    }
  })
});
const { Message } = await response.json();
if (Message.StatusCode !== 0) throw new Error('GetCardDetails failed: ' + Message.StatusDescription);
const paymentTranID = Message.PaymentDetails.PaymentTranID;
const { CardBrand, LastFour, EntryMethod } = Message.CardDetails[0];
resp = requests.post(
    'https://localhost:8600/PMI/GetCardDetails',
    headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
    json={'Message':{'ClientID':'KIOSK1','Operation':'GetCardDetails',
          'RequestID':'k103','Version':'4.7.0',
          'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1},
          'Amounts':{'AmountDue':'18.50','AmountTendered':'18.50'},
          'PaymentDetails':{'SessionTranID':session_tran_id},
          'CardDetails':[{'CardType':'Credit'}]}},
    verify=False)
msg = resp.json()['Message']
if msg['StatusCode'] != 0: raise RuntimeError('GetCardDetails failed')
payment_tran_id = msg['PaymentDetails']['PaymentTranID']
card = msg['CardDetails'][0]  # CardBrand, LastFour, EntryMethod
<?php
$body = json_encode(['Message' => [
    'ClientID' => 'KIOSK1', 'Operation' => 'GetCardDetails',
    'RequestID' => 'k103', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1],
    'Amounts' => ['AmountDue' => '18.50', 'AmountTendered' => '18.50'],
    'PaymentDetails' => ['SessionTranID' => $sessionTranID],
    'CardDetails' => [['CardType' => 'Credit']]
]]);
$ch = curl_init('https://localhost:8600/PMI/GetCardDetails');
curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,
    CURLOPT_SSL_VERIFYPEER=>false,
    CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS=>$body]);
$msg = json_decode(curl_exec($ch),true)['Message']; curl_close($ch);
$paymentTranID = $msg['PaymentDetails']['PaymentTranID'];
var payload = new { Message = new {
    ClientID = "KIOSK1", Operation = "GetCardDetails",
    RequestID = "k103", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 },
    Amounts = new { AmountDue = "18.50", AmountTendered = "18.50" },
    PaymentDetails = new { SessionTranID = sessionTranID },
    CardDetails = new[] { new { CardType = "Credit" } }
}};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/GetCardDetails", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
var paymentTranID = root.GetProperty("Message").GetProperty("PaymentDetails")
                        .GetProperty("PaymentTranID").GetString();
req.body = { Message: { ClientID: 'KIOSK1', Operation: 'GetCardDetails',
  RequestID: 'k103', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
  Amounts: { AmountDue: '18.50', AmountTendered: '18.50' },
  PaymentDetails: { SessionTranID: session_tran_id },
  CardDetails: [{ CardType: 'Credit' }]}}.to_json
msg = JSON.parse(http.request(req).body)['Message']
raise 'GetCardDetails failed' unless msg['StatusCode'] == 0
payment_tran_id = msg['PaymentDetails']['PaymentTranID']
String body = String.format("""{"Message":{"ClientID":"KIOSK1","Operation":"GetCardDetails",
  "RequestID":"k103","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
  "Amounts":{"AmountDue":"18.50","AmountTendered":"18.50"},
  "PaymentDetails":{"SessionTranID":"%s"},
  "CardDetails":[{"CardType":"Credit"}]}}""", sessionTranID);
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/GetCardDetails"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Parse Message.PaymentDetails.PaymentTranID
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID":"KIOSK1","Operation":"GetCardDetails",
    "RequestID":"k103","Version":"4.7.0",
    "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
    "Amounts":map[string]any{"AmountDue":"18.50","AmountTendered":"18.50"},
    "PaymentDetails":map[string]any{"SessionTranID":sessionTranID},
    "CardDetails":[]any{map[string]any{"CardType":"Credit"}},
}})
req, _ := http.NewRequest("POST","https://localhost:8600/PMI/GetCardDetails",bytes.NewBuffer(body))
req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
resp, _ := client.Do(req)
// Parse Message.PaymentDetails.PaymentTranID

Authorize and capture — Purchase

Submits the authorization request to the payment host. Use both SessionTranID and PaymentTranID. Persist AuthCode and ReferenceID from an approved response before calling EndPaymentSession.

curl -X POST https://localhost:8600/PMI/Purchase \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1", "Operation": "Purchase",
      "RequestID": "k104", "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 },
      "Amounts": { "AmountDue": "18.50", "AmountTendered": "18.50" },
      "PaymentDetails": {
        "SessionTranID": "ccl-session-k42",
        "PaymentTranID": "pmt-k42-001"
      }
    }
  }'

# Approved: { "Message": { "StatusCode": 0,
#   "PaymentDetails": { "AuthCode": "AUTH847291", "ResponseMessage": "APPROVED" },
#   "HostData": { "ReferenceID": "ref-host-xyz" },
#   "Amounts": { "AmountApproved": "18.50" } } }
#
# Declined: { "Message": { "StatusCode": 2, "PaymentDetails": { "ResponseMessage": "DECLINED" } } }
const response = await fetch('https://localhost:8600/PMI/Purchase', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
  body: JSON.stringify({
    Message: {
      ClientID: 'KIOSK1', Operation: 'Purchase',
      RequestID: 'k104', Version: '4.7.0',
      ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
      Amounts: { AmountDue: '18.50', AmountTendered: '18.50' },
      PaymentDetails: { SessionTranID: sessionTranID, PaymentTranID: paymentTranID }
    }
  })
});
const { Message } = await response.json();

if (Message.StatusCode === 0) {
  // Approved — persist before EndPaymentSession
  const authCode    = Message.PaymentDetails.AuthCode;
  const referenceID = Message.HostData.ReferenceID;
  const amtApproved = Message.Amounts.AmountApproved;
} else {
  // Declined — show reason to customer
  const reason = Message.PaymentDetails?.ResponseMessage ?? Message.StatusDescription;
}
resp = requests.post(
    'https://localhost:8600/PMI/Purchase',
    headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
    json={'Message':{'ClientID':'KIOSK1','Operation':'Purchase',
          'RequestID':'k104','Version':'4.7.0',
          'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1},
          'Amounts':{'AmountDue':'18.50','AmountTendered':'18.50'},
          'PaymentDetails':{'SessionTranID':session_tran_id,'PaymentTranID':payment_tran_id}}},
    verify=False)
msg = resp.json()['Message']
if msg['StatusCode'] == 0:
    # Approved — persist before EndPaymentSession
    auth_code    = msg['PaymentDetails']['AuthCode']
    reference_id = msg['HostData']['ReferenceID']
else:
    # Declined
    reason = msg.get('PaymentDetails',{}).get('ResponseMessage', msg['StatusDescription'])
<?php
$body = json_encode(['Message' => [
    'ClientID' => 'KIOSK1', 'Operation' => 'Purchase',
    'RequestID' => 'k104', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1],
    'Amounts' => ['AmountDue' => '18.50', 'AmountTendered' => '18.50'],
    'PaymentDetails' => ['SessionTranID' => $sessionTranID, 'PaymentTranID' => $paymentTranID]
]]);
$ch = curl_init('https://localhost:8600/PMI/Purchase');
curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,
    CURLOPT_SSL_VERIFYPEER=>false,
    CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS=>$body]);
$msg = json_decode(curl_exec($ch),true)['Message']; curl_close($ch);
if ($msg['StatusCode'] === 0) {
    $authCode    = $msg['PaymentDetails']['AuthCode'];     // persist
    $referenceID = $msg['HostData']['ReferenceID'];        // persist
}
var payload = new { Message = new {
    ClientID = "KIOSK1", Operation = "Purchase",
    RequestID = "k104", Version = "4.7.0",
    ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 },
    Amounts = new { AmountDue = "18.50", AmountTendered = "18.50" },
    PaymentDetails = new { SessionTranID = sessionTranID, PaymentTranID = paymentTranID }
}};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/Purchase", payload);
var root = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (root.GetProperty("Message").GetProperty("StatusCode").GetInt32() == 0) {
    var authCode    = root.GetProperty("Message").GetProperty("PaymentDetails").GetProperty("AuthCode").GetString();
    var referenceID = root.GetProperty("Message").GetProperty("HostData").GetProperty("ReferenceID").GetString();
}
req.body = { Message: { ClientID: 'KIOSK1', Operation: 'Purchase',
  RequestID: 'k104', Version: '4.7.0',
  ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
  Amounts: { AmountDue: '18.50', AmountTendered: '18.50' },
  PaymentDetails: { SessionTranID: session_tran_id, PaymentTranID: payment_tran_id }}}.to_json
msg = JSON.parse(http.request(req).body)['Message']
if msg['StatusCode'] == 0
  auth_code    = msg['PaymentDetails']['AuthCode']   # persist
  reference_id = msg['HostData']['ReferenceID']      # persist
end
String body = String.format("""{"Message":{"ClientID":"KIOSK1","Operation":"Purchase",
  "RequestID":"k104","Version":"4.7.0",
  "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
  "Amounts":{"AmountDue":"18.50","AmountTendered":"18.50"},
  "PaymentDetails":{"SessionTranID":"%s","PaymentTranID":"%s"}}}""",
  sessionTranID, paymentTranID);
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://localhost:8600/PMI/Purchase"))
    .headers("Content-Type","application/json","X-ClientId","KIOSK1")
    .POST(HttpRequest.BodyPublishers.ofString(body)).build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// StatusCode 0=Approved, 2=Declined
// On approval: persist Message.PaymentDetails.AuthCode + Message.HostData.ReferenceID
body, _ := json.Marshal(map[string]any{"Message": map[string]any{
    "ClientID":"KIOSK1","Operation":"Purchase","RequestID":"k104","Version":"4.7.0",
    "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
    "Amounts":map[string]any{"AmountDue":"18.50","AmountTendered":"18.50"},
    "PaymentDetails":map[string]any{"SessionTranID":sessionTranID,"PaymentTranID":paymentTranID},
}})
req, _ := http.NewRequest("POST","https://localhost:8600/PMI/Purchase",bytes.NewBuffer(body))
req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
resp, _ := client.Do(req)
// StatusCode 0=Approved, 2=Declined
// On approval: persist Message.PaymentDetails.AuthCode + Message.HostData.ReferenceID

Close the session — EndPaymentSession

Always call this — on approval, decline, timeout, or error. Use a finally / ensure / defer block so it cannot be skipped. Failing to close the session leaves the PIN pad locked, preventing subsequent transactions.

curl -X POST https://localhost:8600/PMI/EndPaymentSession \
  -H "Content-Type: application/json" -H "X-ClientId: KIOSK1" --insecure \
  -d '{
    "Message": {
      "ClientID": "KIOSK1", "Operation": "EndPaymentSession",
      "RequestID": "k105", "Version": "4.7.0",
      "ConfigOptions": { "CompanyNumber": 185197, "StoreNumber": 1, "LaneNumber": 1 },
      "PaymentDetails": { "SessionTranID": "ccl-session-k42" }
    }
  }'

# Response: { "Message": { "StatusCode": 0, "StatusDescription": "Session closed" } }
# PIN pad returns to idle — ready for the next customer
// Always call in a finally block
try {
  // ... BeginPaymentSession, StartNotifications, GetCardDetails, Purchase
} finally {
  if (sessionTranID) {
    await fetch('https://localhost:8600/PMI/EndPaymentSession', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-ClientId': 'KIOSK1' },
      body: JSON.stringify({ Message: {
        ClientID: 'KIOSK1', Operation: 'EndPaymentSession',
        RequestID: 'k105', Version: '4.7.0',
        ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
        PaymentDetails: { SessionTranID: sessionTranID }
      }})
    }).catch(() => {}); // swallow — already in cleanup
    // PIN pad returns to idle
  }
}
try:
    # ... purchase flow
    pass
finally:
    if session_tran_id:
        try:
            requests.post('https://localhost:8600/PMI/EndPaymentSession',
                headers={'Content-Type':'application/json','X-ClientId':'KIOSK1'},
                json={'Message':{'ClientID':'KIOSK1','Operation':'EndPaymentSession',
                      'RequestID':'k105','Version':'4.7.0',
                      'ConfigOptions':{'CompanyNumber':185197,'StoreNumber':1,'LaneNumber':1},
                      'PaymentDetails':{'SessionTranID':session_tran_id}}},
                verify=False)
        except Exception: pass  # already in cleanup
<?php
// Call in a finally block — runs on success, decline, and error alike
$body = json_encode(['Message' => [
    'ClientID' => 'KIOSK1', 'Operation' => 'EndPaymentSession',
    'RequestID' => 'k105', 'Version' => '4.7.0',
    'ConfigOptions' => ['CompanyNumber' => 185197, 'StoreNumber' => 1, 'LaneNumber' => 1],
    'PaymentDetails' => ['SessionTranID' => $sessionTranID]
]]);
$ch = curl_init('https://localhost:8600/PMI/EndPaymentSession');
curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,
    CURLOPT_SSL_VERIFYPEER=>false,
    CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-ClientId: KIOSK1'],
    CURLOPT_POSTFIELDS=>$body]);
curl_exec($ch); curl_close($ch);
// PIN pad returns to idle — ready for the next customer
try {
    // ... purchase flow
} finally {
    try {
        await client.PostAsJsonAsync("https://localhost:8600/PMI/EndPaymentSession", new { Message = new {
            ClientID = "KIOSK1", Operation = "EndPaymentSession",
            RequestID = "k105", Version = "4.7.0",
            ConfigOptions = new { CompanyNumber = 185197, StoreNumber = 1, LaneNumber = 1 },
            PaymentDetails = new { SessionTranID = sessionTranID }
        }});
    } catch { /* swallow — already in cleanup */ }
    // PIN pad returns to idle
}
begin
  # ... purchase flow
ensure
  req.body = { Message: { ClientID: 'KIOSK1', Operation: 'EndPaymentSession',
    RequestID: 'k105', Version: '4.7.0',
    ConfigOptions: { CompanyNumber: 185197, StoreNumber: 1, LaneNumber: 1 },
    PaymentDetails: { SessionTranID: session_tran_id }}}.to_json
  http.request(req) rescue nil
  # PIN pad returns to idle
end
try {
    // ... purchase flow
} finally {
    String body = String.format("""{"Message":{"ClientID":"KIOSK1","Operation":"EndPaymentSession",
      "RequestID":"k105","Version":"4.7.0",
      "ConfigOptions":{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
      "PaymentDetails":{"SessionTranID":"%s"}}}""", sessionTranID);
    try {
        client.send(HttpRequest.newBuilder()
            .uri(URI.create("https://localhost:8600/PMI/EndPaymentSession"))
            .headers("Content-Type","application/json","X-ClientId","KIOSK1")
            .POST(HttpRequest.BodyPublishers.ofString(body)).build(),
            HttpResponse.BodyHandlers.ofString());
    } catch (Exception ignored) {}
    // PIN pad returns to idle
}
defer func() {
    body, _ := json.Marshal(map[string]any{"Message": map[string]any{
        "ClientID":"KIOSK1","Operation":"EndPaymentSession",
        "RequestID":"k105","Version":"4.7.0",
        "ConfigOptions":map[string]any{"CompanyNumber":185197,"StoreNumber":1,"LaneNumber":1},
        "PaymentDetails":map[string]any{"SessionTranID":sessionTranID},
    }})
    req, _ := http.NewRequest("POST","https://localhost:8600/PMI/EndPaymentSession",bytes.NewBuffer(body))
    req.Header.Set("Content-Type","application/json"); req.Header.Set("X-ClientId","KIOSK1")
    client.Do(req) // best-effort — ignore error in cleanup
    // PIN pad returns to idle
}()

Error handling

Startup errors

If any startup call returns a non-zero StatusCode, do not proceed to payment. Surface an "Out of Order" state and log the error for the service team. The customer should not be able to reach the payment flow.

Payment errors

StatusCodeMeaningAction
0ApprovedPersist AuthCode + ReferenceID, show confirmation, call EndPaymentSession
2DeclinedShow decline message, call EndPaymentSession, offer retry
3Partial approvalAmountApproved < AmountDue — collect remaining balance, call EndPaymentSession
4Card removedCustomer removed card mid-read — call EndPaymentSession, prompt to re-present
5TimeoutNo card presented in time — call EndPaymentSession, offer retry or cancel
6Communication errorRetry once; if still failing, call EndPaymentSession
9Engine not initializedLane closed unexpectedly — surface Out of Order state
Any non-zeroGeneral errorAlways call EndPaymentSession before allowing the next transaction
⚠️ Always call EndPaymentSession

Even when your code throws an unexpected exception, EndPaymentSession must be called before the next transaction. Use a finally / ensure / defer block so it cannot be skipped.

Result codes

CodeNameDescription
0Success / ApprovedOperation completed successfully
1General errorSee StatusDescription for details
2DeclinedIssuer declined the card
3Partial approvalAmountApproved is less than AmountDue
4Card removedCard pulled out before read completed
5TimeoutNo card presented within the session timeout window
6Communication errorNetwork or gateway unreachable
7Session not foundSessionTranID is invalid or expired
9Engine not initializedCall GetEngineStatus and re-run startup sequence
10Lane not openCall OpenLane before making payment operations

API summary

OperationEndpointPhaseRequired?
GetEngineStatusPOST /GetEngineStatusStartupAlways
StartPOST /StartStartupWhen EngineStatusCode = 1
InitializePOST /InitializeStartupWhen EngineStatusCode ≤ 2
OpenLanePOST /OpenLaneStartupWhen EngineStatusCode ≤ 3
BeginPaymentSessionPOST /BeginPaymentSessionPaymentAlways
StartNotificationsPOST /StartNotificationsPaymentAlways
GetCardDetailsPOST /GetCardDetailsPaymentAlways
PurchasePOST /PurchasePaymentAlways
EndPaymentSessionPOST /EndPaymentSessionPaymentAlways (even on error)