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
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.
| Capability | Description | Configuration | Kiosk 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. |
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).
| Device | Windows | Android | Linux (Ubuntu) | Connection |
|---|---|---|---|---|
| Lane8000 | ✓ | X | ✓ | USB, TCPIP |
| Lane7000 | ✓ | X | ✓ | USB |
| Link2500 | ✓ | ✓ | ✓ | USB, Bluetooth |
| Axium8000 | X | ✓ | X | USB |
| RP457 | X | ✓ | X | USB |
| MOBY5500-CL3 | X | ✓ | X | USB |
| 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 |
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
GetEngineStatus -> EngineStatusCode = 0 -> READY -> EngineStatusCode = 1 -> Start -> Initialize -> OpenLane -> READY -> EngineStatusCode = 2 -> Initialize -> OpenLane -> READY -> EngineStatusCode = 3 -> OpenLane -> READY
Purchase transaction flow
BeginPaymentSession -> StartNotifications -> GetCardDetails -> Purchase -> EndPaymentSession
Startup flow — runs on application load
GetEngineStatus
Query the current engine state. Returns a status code that determines which startup steps are needed.
Start
Only called when status = 1 (StartNeeded). Starts the PMI engine process.
Initialize
Only called when status ≤ 2. Downloads configuration and authenticates with the payment host.
OpenLane
Only called when status ≤ 3. Activates the PIN pad and opens the lane for card reads.
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"
BeginPaymentSession
Opens a session with the order amount and tip. Returns a SessionTranID used in all subsequent calls.
StartNotifications
Activates the PIN pad display so the customer sees "Tap / Insert / Swipe". The terminal is now waiting for a card.
GetCardDetails
Reads the card after the customer presents it. Returns the masked PAN, card brand, and entry method.
Purchase
Submits the authorization to the payment host. Returns approval or decline with an auth code.
EndPaymentSession
Always called — on approval, decline, timeout, or error. Resets the PIN pad for the next customer.
Purchase scenario diagram
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=OpenLaneNeededconst 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 = OpenLaneNeededimport 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 = OpenLaneNeededusing 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 = OpenLaneNeededrequire '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 = OpenLaneNeededimport 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=OpenLaneNeededpackage 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'] == 0String 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 == 0body, _ := 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 == 0Initialize (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'] == 0String 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 == 0body, _ := 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 == 0OpenLane (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 transactionsconst 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 transactionsresp = 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 transactionsvar 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 transactionsreq.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 transactionsString 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 transactionsbody, _ := 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 transactionsEngine status codes
| Code | Name | Meaning | Next step |
|---|---|---|---|
0 | Ready | Engine fully initialized, lane open | None — proceed to payment |
1 | StartNeeded | PMI engine process is not running | Call Start → Initialize → OpenLane |
2 | InitializeNeeded | Engine running but not configured | Call Initialize → OpenLane |
3 | OpenLaneNeeded | Configured but lane not open | Call 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
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
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.SessionTranIDbody, _ := 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){
"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.
{
"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 / SWIPEconst 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 / SWIPErequests.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 / SWIPEawait 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 / SWIPEreq.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 / SWIPEString 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 / SWIPEbody, _ := 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 / SWIPECard reader notification events
| Event | Meaning | Action |
|---|---|---|
CardInserted | Card inserted into chip reader | Call GetCardDetails |
CardTapped | Contactless card / phone detected | Call GetCardDetails |
CardSwiped | Card swiped through MSR | Call GetCardDetails |
CardRemoved | Card removed before read complete | Prompt customer to re-present card |
SessionTimeout | No card presented in time | Call 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.PaymentTranIDbody, _ := 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.PaymentTranIDAuthorize 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
endString 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.ReferenceIDbody, _ := 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.ReferenceIDClose 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 customertry {
// ... 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
endtry {
// ... 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
| StatusCode | Meaning | Action |
|---|---|---|
0 | Approved | Persist AuthCode + ReferenceID, show confirmation, call EndPaymentSession |
2 | Declined | Show decline message, call EndPaymentSession, offer retry |
3 | Partial approval | AmountApproved < AmountDue — collect remaining balance, call EndPaymentSession |
4 | Card removed | Customer removed card mid-read — call EndPaymentSession, prompt to re-present |
5 | Timeout | No card presented in time — call EndPaymentSession, offer retry or cancel |
6 | Communication error | Retry once; if still failing, call EndPaymentSession |
9 | Engine not initialized | Lane closed unexpectedly — surface Out of Order state |
| Any non-zero | General error | Always call EndPaymentSession before allowing the next transaction |
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
| Code | Name | Description |
|---|---|---|
0 | Success / Approved | Operation completed successfully |
1 | General error | See StatusDescription for details |
2 | Declined | Issuer declined the card |
3 | Partial approval | AmountApproved is less than AmountDue |
4 | Card removed | Card pulled out before read completed |
5 | Timeout | No card presented within the session timeout window |
6 | Communication error | Network or gateway unreachable |
7 | Session not found | SessionTranID is invalid or expired |
9 | Engine not initialized | Call GetEngineStatus and re-run startup sequence |
10 | Lane not open | Call OpenLane before making payment operations |
API summary
| Operation | Endpoint | Phase | Required? |
|---|---|---|---|
GetEngineStatus | POST /GetEngineStatus | Startup | Always |
Start | POST /Start | Startup | When EngineStatusCode = 1 |
Initialize | POST /Initialize | Startup | When EngineStatusCode ≤ 2 |
OpenLane | POST /OpenLane | Startup | When EngineStatusCode ≤ 3 |
BeginPaymentSession | POST /BeginPaymentSession | Payment | Always |
StartNotifications | POST /StartNotifications | Payment | Always |
GetCardDetails | POST /GetCardDetails | Payment | Always |
Purchase | POST /Purchase | Payment | Always |
EndPaymentSession | POST /EndPaymentSession | Payment | Always (even on error) |