Accept in-person payments for your POS business
Learn how to accept card payments with the NCR Payment Management Interface (PMI) REST API for your in-person point of sale system.
This guide describes how to set up your PMI integration to accept payments with an in-person point of sale (POS) system. The NCR Common Client Layer (CCL) is a cross-platform payment engine that abstracts PIN pad hardware and payment gateway complexity behind a unified JSON interface. This guide covers the HTTP / REST API integration method using the PMI Web Server.
Onboarding & prerequisites
Before writing integration code, NCR Voyix works with you to provision a lab environment using the Azure RLSG-CFR platform. This environment is configured end-to-end during the customer onboarding process and includes all credentials, device configuration, and host connectivity needed for end-to-end transaction testing.
Onboarding package
Your onboarding package is assembled based on your business profile. It covers payment host connectivity, PIN pad device configuration, platform support, and repository access.
Supported payment hosts
Your environment is provisioned against one or more of the following payment processors:
- Chase
- Corpay / Comdata
- Worldpay
- Wex
- Voyix Pay
- Additional hosts as applicable
Supported PIN pad devices
| Device | Supported connection |
|---|---|
| Ingenico Lane/8000 | TCP/IP, USB |
| Moby 5500 | TCP/IP, USB |
| Verifone M425 Neo / M400 | TCP/IP, USB |
Platform support
CCL and PMIWebServer are distributed for the following operating systems:
- Windows
- Linux
- Android
Product feature enablement
Features are enabled based on your business segment during onboarding:
- Hospitality — restaurant, bar, and table-service scenarios
- Retail — Grocery
- Retail — Fuel
- Commercial Fuel
- Additional features as applicable to your use case
SDK build & JFrog access
Your onboarding package specifies the CCL build version for your deployment (for example, v26.6.1) along with JFrog Artifactory credentials for downloading SDK packages and build artifacts.
SDK packages are hosted under npg-ccl-generic-releases/CCL/26.6/ on NCR's JFrog Artifactory. Your onboarding team provides the access URL and credentials.
Configuration details
In addition to environment setup, your onboarding package includes merchant configuration for the following components:
DataManager & ConMan
Your CCL configuration is provisioned with the identifiers required for every API call:
| Parameter | Description |
|---|---|
CompanyNumber |
Merchant organization identifier. Included in every request body under ConfigOptions. |
StoreNumber |
Store location identifier. Scoped to a single physical site. |
LaneNumber |
POS terminal / register number within the store. |
MID (Merchant ID) |
Host-specific identifier assigned by the payment processor for your merchant account. |
TID (Terminal ID) |
Terminal-level identifier assigned per lane by the payment host. |
Store-level and lane-level configurations are managed in DataManager. ConMan handles configuration delivery to the CCL engine at Initialize and OpenLane time.
The MID and TID values provided in your lab onboarding package are for test use only. Contact your NCR account team to obtain production values before go-live.
Before you begin
A server-driven PMI integration uses HTTP requests to communicate directly with the CCL engine running on your POS terminal. Your application makes API calls to create payments and control the PIN pad device.
To integrate, you need:
- A POS application that can make HTTP requests
- NCR Common Client SDK (CCL 26.6) installed on the terminal
- A compatible PIN pad device (e.g., Verifone MX Series, Ingenico Lane/3000)
- Merchant credentials from NCR Voyix
- (Optional) A physical test card for sandbox testing
Get your credentials
Contact NCR to obtain your merchant credentials. You'll need:
| Credential | Description |
|---|---|
CompanyNumber |
Your merchant organization ID |
StoreNumber |
Store location identifier |
LaneNumber |
POS terminal/register ID |
HMACClientKey |
Host authentication client key |
HMACSecretKey |
Host authentication secret (keep secure) |
Never hardcode credentials in source code. Store them in environment variables or a secure vault service.
Install PMI WebServer
Download the NCR Common Client SDK for your platform from the JFrog Artifactory repository. The SDK package includes PMIWebServer, the CCLTestApp Flutter test application, Postman collections, and sample code.
| Platform | Package | Format |
|---|---|---|
| Windows (x64) | WindowsCommonClientSDK-26.6.3783.zip | ZIP |
| Linux (x64) | LinuxCommonClientSDK-26.6.3783.tar.gz | TAR.GZ — .deb / .rpm installers included |
After installation, PMIWebServer starts automatically as a background service and listens on https://localhost:8600. All PMI API calls are sent to this endpoint.
Set up your PIN pad
After receiving your PIN pad device:
- Connect the device via USB or serial cable
- Install any required device drivers
- Verify the device appears in your system's device manager
- Note the device's connection GUID (you'll get this from the
Initializeresponse)
CCL Overview
Common Client (CCL) is NCR Voyix's embedded payment engine. It ships as a native library and a local web server, and it manages all communication between your POS application, the PIN pad device, and the payment gateway.
CCL exposes two interfaces:
- PMI (Payment Management Interface) — POS-facing API for payment operations: GetCardDetails, Purchase, Void, Refund, Preauth, and more.
- DMI (Device Management Interface) — Device-facing API for direct PIN pad communication: firmware updates, display control, key injection.
The current release is CCL 26.6 (SDK package version 26.6.3783). SDK packages are distributed via JFrog Artifactory under npg-ccl-generic-releases/CCL/26.6/ for Windows, Linux, and Android.
Integration methods
CCL supports two integration patterns. Both use the same JSON message format. This guide uses the HTTP / Web Server method.
| Method | How it works | Platforms | Best for |
|---|---|---|---|
| Library / SDK | Link the CCL library directly into your POS process (C++, C#, Java) | Windows, Linux, Android | Tightest coupling, lowest latency, native apps |
| HTTP / Web Server ✓ | Send HTTP POST requests to PMIWebServer running on https://localhost:8600 |
Windows, Linux | Language-agnostic, any stack, this guide |
PMIWebServer is included in the Windows and Linux CCL SDK packages. It runs as a local service and exposes a REST API that any language can call — no native library linking required.
End-to-end payment flow
The diagram below shows the complete message sequence between your POS application, the PMI Web Server (CCL engine), the PIN pad device, and the payment gateway. It reflects the updated flow including device notification polling during OpenLane, the swipe-ahead EnableCardEntry pattern, and the conditional CloseLane guard.
Create the payment flow
Collecting payments with PMI requires creating a payment flow in your application. Use the PMI API to create and manage a payment session, an object representing a single payment transaction.
Check engine status
Before every payment flow, check if CCL needs initialization. This determines whether you need to call Initialize and OpenLane.
curl https://localhost:8600/PMI/GetEngineStatus \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: GetEngineStatus" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "GetEngineStatus",
"RequestID": "1001",
"Version": "4.7.0",
"ConfigOptions": {
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0
}
}
}'const response = await fetch('https://localhost:8600/PMI/GetEngineStatus', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'GetEngineStatus'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'GetEngineStatus',
RequestID: String(Date.now()),
Version: '4.7.0',
ConfigOptions: {
CompanyNumber: 185197,
StoreNumber: 0,
LaneNumber: 0
}
}
})
});
const { Message } = await response.json();
const statusCode = Message.StatusInfo.EngineStatusCode;
// 0 = ready, 2 = needs init, 3 = needs open laneimport requests
response = requests.post(
"https://localhost:8600/PMI/GetEngineStatus",
headers={
"Content-Type": "application/json",
"X-ClientId": "POS1",
"X-Operation": "GetEngineStatus"
},
json={
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "GetEngineStatus",
"RequestID": "1001",
"Version": "4.7.0",
"ConfigOptions": {
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0
}
}
},
verify=False
)
message = response.json()["Message"]
status_code = message["StatusInfo"]["EngineStatusCode"] [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'GetEngineStatus',
'RequestID' => '1001',
'Version' => '4.7.0',
'ConfigOptions' => [
'CompanyNumber' => 185197,
'StoreNumber' => 0,
'LaneNumber' => 0
]
]
]);
$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: POS1', 'X-Operation: GetEngineStatus'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
// StatusCode 0=ready, 2=needs init, 3=needs open laneusing System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "GetEngineStatus");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "GetEngineStatus",
RequestID = "1001",
Version = "4.7.0",
ConfigOptions = new {
CompanyNumber = 185197,
StoreNumber = 0,
LaneNumber = 0
}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/GetEngineStatus", payload);
var root = await resp.Content.ReadFromJsonAsync();
// root.GetProperty("Message").GetProperty("StatusInfo").GetProperty("EngineStatusCode").GetInt32(); 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 # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'GetEngineStatus'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'GetEngineStatus',
RequestID: '1001',
Version: '4.7.0',
ConfigOptions: {
CompanyNumber: 185197,
StoreNumber: 0,
LaneNumber: 0
}
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
# msg['StatusInfo']['EngineStatusCode'] # 0=ready, 2=needs init, 3=needs open laneimport java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"GetEngineStatus","RequestID":"1001","Version":"4.7.0","ConfigOptions":{"CompanyNumber":185197,"StoreNumber":0,"LaneNumber":0}}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/GetEngineStatus"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "GetEngineStatus")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Parse resp.body() → Message.StatusInfo.EngineStatusCode package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "GetEngineStatus",
"RequestID": "1001",
"Version": "4.7.0",
"ConfigOptions": map[string]any{
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0,
},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/GetEngineStatus", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "GetEngineStatus")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["StatusInfo"].(map[string]any)["EngineStatusCode"]}
The response includes EngineStatusCode:
0— Engine ready for transactions1— CCL service not started2— Needs initialization3— Lane closed, needs open
Initialize the terminal (if needed)
If EngineStatusCode is 2 or 3, initialize CCL. This downloads config from the payment gateway and registers your credentials.
curl https://localhost:8600/PMI/Initialize \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: Initialize" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "Initialize",
"RequestID": "1002",
"Version": "4.7.0",
"ConfigOptions": {
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0,
"HostAddress": "webeps1-rlsg.paymentslab.ncrvoyix.com",
"HostPort": 443,
"ConfigAddress": "svc1-rlsg.paymentslab.ncrvoyix.com",
"ConfigPort": 443,
"HMACClientKey": "your-client-key",
"HMACSecretKey": "your-secret-key"
},
"PosOptions": {
"AppName": "MyPOS",
"AppVersion": "1.0.0"
}
}
}'const response = await fetch('https://localhost:8600/PMI/Initialize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'Initialize'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'Initialize', RequestID: '1002', Version: '4.7.0',
ConfigOptions: {
CompanyNumber: 185197, StoreNumber: 0, LaneNumber: 0,
HostAddress: 'webeps1-rlsg.paymentslab.ncrvoyix.com', HostPort: 443,
ConfigAddress: 'svc1-rlsg.paymentslab.ncrvoyix.com', ConfigPort: 443,
HMACClientKey: 'your-client-key', HMACSecretKey: 'your-secret-key'
},
PosOptions: { AppName: 'MyPOS', AppVersion: '1.0.0' }
}
})
});
const { Message } = await response.json();
// Message.ConnectionInfo → ["DisplayName|GUID", ...]
// Extract GUID: const guid = entry.split('|')[1]import requests
response = requests.post(
'https://localhost:8600/PMI/Initialize',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'Initialize'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'Initialize', 'RequestID': '1002', 'Version': '4.7.0',
'ConfigOptions': {
'CompanyNumber': 185197, 'StoreNumber': 0, 'LaneNumber': 0,
'HostAddress': 'webeps1-rlsg.paymentslab.ncrvoyix.com', 'HostPort': 443,
'ConfigAddress': 'svc1-rlsg.paymentslab.ncrvoyix.com', 'ConfigPort': 443,
'HMACClientKey': 'your-client-key', 'HMACSecretKey': 'your-secret-key'
},
'PosOptions': {'AppName': 'MyPOS', 'AppVersion': '1.0.0'}
}
},
verify=False
)
message = response.json()['Message']
# Extract GUIDs: [entry.split('|')[1] for entry in message['ConnectionInfo']] [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'Initialize',
'RequestID' => '1002',
'Version' => '4.7.0',
'ConfigOptions' => [
'CompanyNumber' => 185197,
'StoreNumber' => 0,
'LaneNumber' => 0,
'HostAddress' => 'webeps1-rlsg.paymentslab.ncrvoyix.com',
'HostPort' => 443,
'ConfigAddress' => 'svc1-rlsg.paymentslab.ncrvoyix.com',
'ConfigPort' => 443,
'HMACClientKey' => 'your-client-key',
'HMACSecretKey' => 'your-secret-key'
],
'PosOptions' => [
'AppName' => 'MyPOS',
'AppVersion' => '1.0.0'
]
]
]);
$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: POS1', 'X-Operation: Initialize'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
// Split ConnectionInfo entry on '|', take GUID after the pipeusing System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "Initialize");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "Initialize",
RequestID = "1002",
Version = "4.7.0",
ConfigOptions = new {
CompanyNumber = 185197,
StoreNumber = 0,
LaneNumber = 0,
HostAddress = "webeps1-rlsg.paymentslab.ncrvoyix.com",
HostPort = 443,
ConfigAddress = "svc1-rlsg.paymentslab.ncrvoyix.com",
ConfigPort = 443,
HMACClientKey = "your-client-key",
HMACSecretKey = "your-secret-key"
},
PosOptions = new {
AppName = "MyPOS",
AppVersion = "1.0.0"
}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/Initialize", payload);
var root = await resp.Content.ReadFromJsonAsync();
// ConnectionInfo: root.GetProperty("Message").GetProperty("ConnectionInfo").EnumerateArray() require 'net/http'
require 'json'
require 'openssl'
uri = URI('https://localhost:8600/PMI/Initialize')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'Initialize'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'Initialize',
RequestID: '1002',
Version: '4.7.0',
ConfigOptions: {
CompanyNumber: 185197,
StoreNumber: 0,
LaneNumber: 0,
HostAddress: 'webeps1-rlsg.paymentslab.ncrvoyix.com',
HostPort: 443,
ConfigAddress: 'svc1-rlsg.paymentslab.ncrvoyix.com',
ConfigPort: 443,
HMACClientKey: 'your-client-key',
HMACSecretKey: 'your-secret-key'
},
PosOptions: {
AppName: 'MyPOS',
AppVersion: '1.0.0'
}
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
# guids = [e.split('|')[1] for e in msg['ConnectionInfo']]import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"Initialize","RequestID":"1002","Version":"4.7.0","ConfigOptions":{"CompanyNumber":185197,"StoreNumber":0,"LaneNumber":0,"HostAddress":"webeps1-rlsg.paymentslab.ncrvoyix.com","HostPort":443,"ConfigAddress":"svc1-rlsg.paymentslab.ncrvoyix.com","ConfigPort":443,"HMACClientKey":"your-client-key","HMACSecretKey":"your-secret-key"},"PosOptions":{"AppName":"MyPOS","AppVersion":"1.0.0"}}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/Initialize"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "Initialize")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Parse resp.body() → Message.ConnectionInfo[] (split on "|", take GUID) package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "Initialize",
"RequestID": "1002",
"Version": "4.7.0",
"ConfigOptions": map[string]any{
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0,
"HostAddress": "webeps1-rlsg.paymentslab.ncrvoyix.com",
"HostPort": 443,
"ConfigAddress": "svc1-rlsg.paymentslab.ncrvoyix.com",
"ConfigPort": 443,
"HMACClientKey": "your-client-key",
"HMACSecretKey": "your-secret-key",
},
"PosOptions": map[string]any{
"AppName": "MyPOS",
"AppVersion": "1.0.0",
},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/Initialize", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "Initialize")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// Extract GUIDs from result["Message"].(map[string]any)["ConnectionInfo"].([]any)}
The response includes ConnectionInfo — an array of PIN pad device GUIDs. Pass these to OpenLane.
Open the lane
Connect to the PIN pad device using the connection GUIDs from the Initialize response. Two important details:
- Extract the GUID only.
ConnectionInfoentries are formatted as"DisplayName|GUID"(e.g."Engage COM9|315F2791-…"). Pass only the portion after the|— this is what the NCR test app does and what CCL requires. - Send full
ConfigOptions. Unlike a minimal subset,OpenLanerequires all config fields (includingCompanyNumber,ConfigAddress, etc.) so CCL can resolve the device, apply firmware updates, and validate keys on a cold start.
curl https://localhost:8600/PMI/OpenLane \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: OpenLane" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "OpenLane",
"RequestID": "1003",
"Version": "4.7.0",
"ConfigOptions": {
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0,
"ConfigAddress": "svc1-rlsg.paymentslab.ncrvoyix.com",
"ConfigPort": 443,
"HostAddress": "webeps1-rlsg.paymentslab.ncrvoyix.com",
"HostPort": 443,
"HostAddressSecondary": "webeps2-rlsg.paymentslab.ncrvoyix.com",
"HostPortSecondary": 443,
"HMACClientKey": "your-client-key",
"HMACSecretKey": "your-secret-key"
},
"ConnectionInfo": ["315F2791-ABCD-1234-5678-64DD96D33219"]
}
}'const response = await fetch('https://localhost:8600/PMI/OpenLane', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'OpenLane'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'OpenLane', RequestID: '1003', Version: '4.7.0',
ConfigOptions: {
CompanyNumber: 185197, StoreNumber: 0, LaneNumber: 0,
ConfigAddress: 'svc1-rlsg.paymentslab.ncrvoyix.com', ConfigPort: 443,
HostAddress: 'webeps1-rlsg.paymentslab.ncrvoyix.com', HostPort: 443,
HostAddressSecondary: 'webeps2-rlsg.paymentslab.ncrvoyix.com', HostPortSecondary: 443,
HMACClientKey: 'your-client-key', HMACSecretKey: 'your-secret-key'
},
ConnectionInfo: ['315F2791-ABCD-1234-5678-64DD96D33219']
}
})
});
const { Message } = await response.json();
// Message.Status === 'Success' → lane is open, PIN pad readyimport requests
response = requests.post(
'https://localhost:8600/PMI/OpenLane',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'OpenLane'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'OpenLane', 'RequestID': '1003', 'Version': '4.7.0',
'ConfigOptions': {
'CompanyNumber': 185197, 'StoreNumber': 0, 'LaneNumber': 0,
'ConfigAddress': 'svc1-rlsg.paymentslab.ncrvoyix.com', 'ConfigPort': 443,
'HostAddress': 'webeps1-rlsg.paymentslab.ncrvoyix.com', 'HostPort': 443,
'HostAddressSecondary': 'webeps2-rlsg.paymentslab.ncrvoyix.com', 'HostPortSecondary': 443,
'HMACClientKey': 'your-client-key', 'HMACSecretKey': 'your-secret-key'
},
'ConnectionInfo': ['315F2791-ABCD-1234-5678-64DD96D33219']
}
},
verify=False
)
message = response.json()['Message']
# message['Status'] == 'Success' → lane is open, PIN pad ready [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'OpenLane',
'RequestID' => '1003',
'Version' => '4.7.0',
'ConfigOptions' => [
'CompanyNumber' => 185197,
'StoreNumber' => 0,
'LaneNumber' => 0,
'ConfigAddress' => 'svc1-rlsg.paymentslab.ncrvoyix.com',
'ConfigPort' => 443,
'HostAddress' => 'webeps1-rlsg.paymentslab.ncrvoyix.com',
'HostPort' => 443,
'HostAddressSecondary' => 'webeps2-rlsg.paymentslab.ncrvoyix.com',
'HostPortSecondary' => 443,
'HMACClientKey' => 'your-client-key',
'HMACSecretKey' => 'your-secret-key'
],
'ConnectionInfo' => ['315F2791-ABCD-1234-5678-64DD96D33219']
]
]);
$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: POS1', 'X-Operation: OpenLane'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
// msg['Status'] === 'Success' → lane open, PIN pad readyusing System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "OpenLane");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "OpenLane",
RequestID = "1003",
Version = "4.7.0",
ConfigOptions = new {
CompanyNumber = 185197,
StoreNumber = 0,
LaneNumber = 0,
ConfigAddress = "svc1-rlsg.paymentslab.ncrvoyix.com",
ConfigPort = 443,
HostAddress = "webeps1-rlsg.paymentslab.ncrvoyix.com",
HostPort = 443,
HostAddressSecondary = "webeps2-rlsg.paymentslab.ncrvoyix.com",
HostPortSecondary = 443,
HMACClientKey = "your-client-key",
HMACSecretKey = "your-secret-key"
},
ConnectionInfo = new[] {"315F2791-ABCD-1234-5678-64DD96D33219"}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/OpenLane", payload);
var root = await resp.Content.ReadFromJsonAsync();
// root.GetProperty("Message").GetProperty("Status").GetString(); // "Success" require 'net/http'
require 'json'
require 'openssl'
uri = URI('https://localhost:8600/PMI/OpenLane')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'OpenLane'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'OpenLane',
RequestID: '1003',
Version: '4.7.0',
ConfigOptions: {
CompanyNumber: 185197,
StoreNumber: 0,
LaneNumber: 0,
ConfigAddress: 'svc1-rlsg.paymentslab.ncrvoyix.com',
ConfigPort: 443,
HostAddress: 'webeps1-rlsg.paymentslab.ncrvoyix.com',
HostPort: 443,
HostAddressSecondary: 'webeps2-rlsg.paymentslab.ncrvoyix.com',
HostPortSecondary: 443,
HMACClientKey: 'your-client-key',
HMACSecretKey: 'your-secret-key'
},
ConnectionInfo: ['315F2791-ABCD-1234-5678-64DD96D33219']
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
# msg['Status'] == 'Success' → lane open, PIN pad readyimport java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"OpenLane","RequestID":"1003","Version":"4.7.0","ConfigOptions":{"CompanyNumber":185197,"StoreNumber":0,"LaneNumber":0,"ConfigAddress":"svc1-rlsg.paymentslab.ncrvoyix.com","ConfigPort":443,"HostAddress":"webeps1-rlsg.paymentslab.ncrvoyix.com","HostPort":443,"HostAddressSecondary":"webeps2-rlsg.paymentslab.ncrvoyix.com","HostPortSecondary":443,"HMACClientKey":"your-client-key","HMACSecretKey":"your-secret-key"},"ConnectionInfo":["315F2791-ABCD-1234-5678-64DD96D33219"]}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/OpenLane"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "OpenLane")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Message.Status === "Success" → lane open package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "OpenLane",
"RequestID": "1003",
"Version": "4.7.0",
"ConfigOptions": map[string]any{
"CompanyNumber": 185197,
"StoreNumber": 0,
"LaneNumber": 0,
"ConfigAddress": "svc1-rlsg.paymentslab.ncrvoyix.com",
"ConfigPort": 443,
"HostAddress": "webeps1-rlsg.paymentslab.ncrvoyix.com",
"HostPort": 443,
"HostAddressSecondary": "webeps2-rlsg.paymentslab.ncrvoyix.com",
"HostPortSecondary": 443,
"HMACClientKey": "your-client-key",
"HMACSecretKey": "your-secret-key",
},
"ConnectionInfo": []string{"315F2791-ABCD-1234-5678-64DD96D33219"},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/OpenLane", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "OpenLane")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["Status"] == "Success"}
OpenLane can take 15–60 seconds on first use while CCL initialises the device, validates encryption keys, and applies firmware updates. Call StartNotifications immediately before OpenLane and poll GetNotifications in parallel to surface live device status to your UI (e.g. "Updating firmware…", "Initializing device…", "Device ready"). Call StopNotifications once OpenLane returns.
Begin payment session
Create a new payment session. This reserves a CCL session slot.
curl https://localhost:8600/PMI/BeginPaymentSession \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: BeginPaymentSession" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "BeginPaymentSession",
"RequestID": "2001",
"Version": "4.7.0",
"PosTranID": "501"
}
}'const response = await fetch('https://localhost:8600/PMI/BeginPaymentSession', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'BeginPaymentSession'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'BeginPaymentSession', RequestID: '2001', Version: '4.7.0',
PosTranID: '501'
}
})
});
const { Message } = await response.json();
const sessionTranID = Message.PaymentDetails.SessionTranID;import requests
response = requests.post(
'https://localhost:8600/PMI/BeginPaymentSession',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'BeginPaymentSession'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'BeginPaymentSession', 'RequestID': '2001', 'Version': '4.7.0',
'PosTranID': '501'
}
},
verify=False
)
session_tran_id = response.json()['Message']['PaymentDetails']['SessionTranID'] [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'BeginPaymentSession',
'RequestID' => '2001',
'Version' => '4.7.0',
'PosTranID' => '501'
]
]);
$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: POS1', 'X-Operation: BeginPaymentSession'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
$sessionTranID = $msg['PaymentDetails']['SessionTranID'];using System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "BeginPaymentSession");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "BeginPaymentSession",
RequestID = "2001",
Version = "4.7.0",
PosTranID = "501"
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/BeginPaymentSession", payload);
var root = await resp.Content.ReadFromJsonAsync();
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 # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'BeginPaymentSession'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'BeginPaymentSession',
RequestID: '2001',
Version: '4.7.0',
PosTranID: '501'
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
session_tran_id = msg['PaymentDetails']['SessionTranID']import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"BeginPaymentSession","RequestID":"2001","Version":"4.7.0","PosTranID":"501"}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/BeginPaymentSession"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "BeginPaymentSession")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Message.PaymentDetails.SessionTranID package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "BeginPaymentSession",
"RequestID": "2001",
"Version": "4.7.0",
"PosTranID": "501",
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/BeginPaymentSession", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "BeginPaymentSession")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["PaymentDetails"].(map[string]any)["SessionTranID"]}Store the returned SessionTranID for subsequent calls.
Enable card entry (swipe-ahead)
After opening a session, call EnableCardEntry to activate the PIN pad immediately — before the cashier presses Pay. This lets the customer tap or swipe while items are still being scanned, reducing checkout time. This is the swipe-ahead pattern.
Required order: BeginPaymentSession → EnableCardEntry → GetCardDetails. Never call EnableCardEntry without an active session.
curl https://localhost:8600/PMI/EnableCardEntry \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: EnableCardEntry" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "EnableCardEntry",
"RequestID": "2002",
"Version": "4.7.0",
"PaymentDetails": {
"SessionTranID": "ccl-session-789"
},
"PosOptions": {
"DisableOptionalPrompts": true
}
}
}'const response = await fetch('https://localhost:8600/PMI/EnableCardEntry', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'EnableCardEntry'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'EnableCardEntry', RequestID: '2002', Version: '4.7.0',
PaymentDetails: { SessionTranID: 'ccl-session-789' },
PosOptions: { DisableOptionalPrompts: true }
}
})
});
const { Message } = await response.json();
// Message.Status === 'Success' → PIN pad ready for card entryimport requests
response = requests.post(
'https://localhost:8600/PMI/EnableCardEntry',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'EnableCardEntry'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'EnableCardEntry', 'RequestID': '2002', 'Version': '4.7.0',
'PaymentDetails': {'SessionTranID': 'ccl-session-789'},
'PosOptions': {'DisableOptionalPrompts': True}
}
},
verify=False
)
message = response.json()['Message']
# message['Status'] == 'Success' → PIN pad ready for card entry [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'EnableCardEntry',
'RequestID' => '2002',
'Version' => '4.7.0',
'PaymentDetails' => [
'SessionTranID' => 'ccl-session-789'
],
'PosOptions' => [
'DisableOptionalPrompts' => true
]
]
]);
$ch = curl_init('https://localhost:8600/PMI/EnableCardEntry');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-ClientId: POS1', 'X-Operation: EnableCardEntry'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
// msg['Status'] === 'Success' → PIN pad ready for card entryusing System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "EnableCardEntry");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "EnableCardEntry",
RequestID = "2002",
Version = "4.7.0",
PaymentDetails = new {
SessionTranID = "ccl-session-789"
},
PosOptions = new {
DisableOptionalPrompts = true
}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/EnableCardEntry", payload);
var root = await resp.Content.ReadFromJsonAsync();
// "Success" → PIN pad ready for card entry require 'net/http'
require 'json'
require 'openssl'
uri = URI('https://localhost:8600/PMI/EnableCardEntry')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'EnableCardEntry'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'EnableCardEntry',
RequestID: '2002',
Version: '4.7.0',
PaymentDetails: {
SessionTranID: 'ccl-session-789'
},
PosOptions: {
DisableOptionalPrompts: true
}
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
# msg['Status'] == 'Success' → PIN pad readyimport java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"EnableCardEntry","RequestID":"2002","Version":"4.7.0","PaymentDetails":{"SessionTranID":"ccl-session-789"},"PosOptions":{"DisableOptionalPrompts":true}}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/EnableCardEntry"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "EnableCardEntry")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Message.Status === "Success" package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "EnableCardEntry",
"RequestID": "2002",
"Version": "4.7.0",
"PaymentDetails": map[string]any{
"SessionTranID": "ccl-session-789",
},
"PosOptions": map[string]any{
"DisableOptionalPrompts": true,
},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/EnableCardEntry", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "EnableCardEntry")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["Status"] == "Success"}
When the customer eventually taps their card, call GetCardDetails with the same SessionTranID — CCL will return immediately because the card was already read.
Prompt the customer to present their card
Call GetCardDetails to long-poll until the card is read. If EnableCardEntry was called earlier, CCL returns immediately with the already-captured card data.
curl https://localhost:8600/PMI/GetCardDetails \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: GetCardDetails" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "GetCardDetails",
"RequestID": "2002",
"Version": "4.7.0",
"PosTranID": "502",
"Amounts": {
"AmountDue": "42.50",
"AmountTendered": "42.50"
},
"PaymentDetails": {
"SessionTranID": "ccl-session-789"
},
"PosOptions": {
"TranType": "Purchase"
}
}
}'const response = await fetch('https://localhost:8600/PMI/GetCardDetails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'GetCardDetails'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'GetCardDetails', RequestID: '2002', Version: '4.7.0',
PosTranID: '502',
Amounts: { AmountDue: '42.50', AmountTendered: '42.50' },
PaymentDetails: { SessionTranID: 'ccl-session-789' },
PosOptions: { TranType: 'Purchase' }
}
})
});
const { Message } = await response.json();
const paymentTranID = Message.PaymentDetails.PaymentTranID;import requests
response = requests.post(
'https://localhost:8600/PMI/GetCardDetails',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'GetCardDetails'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'GetCardDetails', 'RequestID': '2002', 'Version': '4.7.0',
'PosTranID': '502',
'Amounts': {'AmountDue': '42.50', 'AmountTendered': '42.50'},
'PaymentDetails': {'SessionTranID': 'ccl-session-789'},
'PosOptions': {'TranType': 'Purchase'}
}
},
verify=False
)
payment_tran_id = response.json()['Message']['PaymentDetails']['PaymentTranID'] [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'GetCardDetails',
'RequestID' => '2002',
'Version' => '4.7.0',
'PosTranID' => '502',
'Amounts' => [
'AmountDue' => '42.50',
'AmountTendered' => '42.50'
],
'PaymentDetails' => [
'SessionTranID' => 'ccl-session-789'
],
'PosOptions' => [
'TranType' => 'Purchase'
]
]
]);
$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: POS1', 'X-Operation: GetCardDetails'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
$paymentTranID = $msg['PaymentDetails']['PaymentTranID'];using System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "GetCardDetails");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "GetCardDetails",
RequestID = "2002",
Version = "4.7.0",
PosTranID = "502",
Amounts = new {
AmountDue = "42.50",
AmountTendered = "42.50"
},
PaymentDetails = new {
SessionTranID = "ccl-session-789"
},
PosOptions = new {
TranType = "Purchase"
}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/GetCardDetails", payload);
var root = await resp.Content.ReadFromJsonAsync();
var paymentTranID = root.GetProperty("Message").GetProperty("PaymentDetails").GetProperty("PaymentTranID").GetString(); require 'net/http'
require 'json'
require 'openssl'
uri = URI('https://localhost:8600/PMI/GetCardDetails')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'GetCardDetails'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'GetCardDetails',
RequestID: '2002',
Version: '4.7.0',
PosTranID: '502',
Amounts: {
AmountDue: '42.50',
AmountTendered: '42.50'
},
PaymentDetails: {
SessionTranID: 'ccl-session-789'
},
PosOptions: {
TranType: 'Purchase'
}
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
payment_tran_id = msg['PaymentDetails']['PaymentTranID']import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"GetCardDetails","RequestID":"2002","Version":"4.7.0","PosTranID":"502","Amounts":{"AmountDue":"42.50","AmountTendered":"42.50"},"PaymentDetails":{"SessionTranID":"ccl-session-789"},"PosOptions":{"TranType":"Purchase"}}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/GetCardDetails"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "GetCardDetails")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Message.PaymentDetails.PaymentTranID package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "GetCardDetails",
"RequestID": "2002",
"Version": "4.7.0",
"PosTranID": "502",
"Amounts": map[string]any{
"AmountDue": "42.50",
"AmountTendered": "42.50",
},
"PaymentDetails": map[string]any{
"SessionTranID": "ccl-session-789",
},
"PosOptions": map[string]any{
"TranType": "Purchase",
},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/GetCardDetails", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "GetCardDetails")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["PaymentDetails"].(map[string]any)["PaymentTranID"]}Store the returned PaymentTranID for the purchase call.
Process the payment
Submit the transaction to the payment gateway for authorization.
curl https://localhost:8600/PMI/Purchase \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: Purchase" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "Purchase",
"RequestID": "2003",
"Version": "4.7.0",
"PosTranID": "503",
"Amounts": {
"AmountDue": "42.50",
"AmountTendered": "42.50"
},
"PaymentDetails": {
"SessionTranID": "ccl-session-789",
"PaymentTranID": "pmt-tran-456"
}
}
}'const response = await fetch('https://localhost:8600/PMI/Purchase', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'Purchase'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'Purchase', RequestID: '2003', Version: '4.7.0',
PosTranID: '503',
Amounts: { AmountDue: '42.50', AmountTendered: '42.50' },
PaymentDetails: {
SessionTranID: 'ccl-session-789',
PaymentTranID: 'pmt-tran-456'
}
}
})
});
const { Message } = await response.json();
// Message.PaymentDetails.AuthCode + Message.PaymentDetails.ReferenceIDimport requests
response = requests.post(
'https://localhost:8600/PMI/Purchase',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'Purchase'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'Purchase', 'RequestID': '2003', 'Version': '4.7.0',
'PosTranID': '503',
'Amounts': {'AmountDue': '42.50', 'AmountTendered': '42.50'},
'PaymentDetails': {
'SessionTranID': 'ccl-session-789',
'PaymentTranID': 'pmt-tran-456'
}
}
},
verify=False
)
message = response.json()['Message']
# message['PaymentDetails']['AuthCode'], message['PaymentDetails']['ReferenceID'] [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'Purchase',
'RequestID' => '2003',
'Version' => '4.7.0',
'PosTranID' => '503',
'Amounts' => [
'AmountDue' => '42.50',
'AmountTendered' => '42.50'
],
'PaymentDetails' => [
'SessionTranID' => 'ccl-session-789',
'PaymentTranID' => 'pmt-tran-456'
]
]
]);
$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: POS1', 'X-Operation: Purchase'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
$authCode = $msg['PaymentDetails']['AuthCode'];using System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "Purchase");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "Purchase",
RequestID = "2003",
Version = "4.7.0",
PosTranID = "503",
Amounts = new {
AmountDue = "42.50",
AmountTendered = "42.50"
},
PaymentDetails = new {
SessionTranID = "ccl-session-789",
PaymentTranID = "pmt-tran-456"
}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/Purchase", payload);
var root = await resp.Content.ReadFromJsonAsync();
var authCode = root.GetProperty("Message").GetProperty("PaymentDetails").GetProperty("AuthCode").GetString(); require 'net/http'
require 'json'
require 'openssl'
uri = URI('https://localhost:8600/PMI/Purchase')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'Purchase'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'Purchase',
RequestID: '2003',
Version: '4.7.0',
PosTranID: '503',
Amounts: {
AmountDue: '42.50',
AmountTendered: '42.50'
},
PaymentDetails: {
SessionTranID: 'ccl-session-789',
PaymentTranID: 'pmt-tran-456'
}
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
auth_code = msg['PaymentDetails']['AuthCode']import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"Purchase","RequestID":"2003","Version":"4.7.0","PosTranID":"503","Amounts":{"AmountDue":"42.50","AmountTendered":"42.50"},"PaymentDetails":{"SessionTranID":"ccl-session-789","PaymentTranID":"pmt-tran-456"}}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/Purchase"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "Purchase")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Message.PaymentDetails.AuthCode + Message.HostData.ReferenceID package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "Purchase",
"RequestID": "2003",
"Version": "4.7.0",
"PosTranID": "503",
"Amounts": map[string]any{
"AmountDue": "42.50",
"AmountTendered": "42.50",
},
"PaymentDetails": map[string]any{
"SessionTranID": "ccl-session-789",
"PaymentTranID": "pmt-tran-456",
},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/Purchase", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "Purchase")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["PaymentDetails"].(map[string]any)["AuthCode"]}On approval, you'll receive an AuthCode and ReferenceID.
End the session
Always close the session to return the PIN pad to idle state.
curl https://localhost:8600/PMI/EndPaymentSession \
-H "Content-Type: application/json" \
-H "X-ClientId: POS1" \
-H "X-Operation: EndPaymentSession" \
--insecure \
-d '{
"Message": {
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "EndPaymentSession",
"RequestID": "2004",
"Version": "4.7.0",
"PaymentDetails": {
"SessionTranID": "ccl-session-789"
}
}
}'const response = await fetch('https://localhost:8600/PMI/EndPaymentSession', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ClientId': 'POS1',
'X-Operation': 'EndPaymentSession'
},
body: JSON.stringify({
Message: {
ClientID: 'POS1', MessageType: 'Request',
Operation: 'EndPaymentSession', RequestID: '2004', Version: '4.7.0',
PaymentDetails: { SessionTranID: 'ccl-session-789' }
}
})
});
const { Message } = await response.json();
// Message.Status === 'Success' → PIN pad returned to idleimport requests
response = requests.post(
'https://localhost:8600/PMI/EndPaymentSession',
headers={'Content-Type': 'application/json', 'X-ClientId': 'POS1', 'X-Operation': 'EndPaymentSession'},
json={
'Message': {
'ClientID': 'POS1', 'MessageType': 'Request',
'Operation': 'EndPaymentSession', 'RequestID': '2004', 'Version': '4.7.0',
'PaymentDetails': {'SessionTranID': 'ccl-session-789'}
}
},
verify=False
)
message = response.json()['Message']
# message['Status'] == 'Success' → PIN pad returned to idle [
'ClientID' => 'POS1',
'MessageType' => 'Request',
'Operation' => 'EndPaymentSession',
'RequestID' => '2004',
'Version' => '4.7.0',
'PaymentDetails' => [
'SessionTranID' => 'ccl-session-789'
]
]
]);
$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: POS1', 'X-Operation: EndPaymentSession'],
CURLOPT_POSTFIELDS => $body,
]);
$msg = json_decode(curl_exec($ch), true)['Message'];
curl_close($ch);
// msg['Status'] === 'Success'using System.Net.Http.Json;
var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true };
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("X-ClientId", "POS1");
client.DefaultRequestHeaders.Add("X-Operation", "EndPaymentSession");
var payload = new {
Message = new {
ClientID = "POS1",
MessageType = "Request",
Operation = "EndPaymentSession",
RequestID = "2004",
Version = "4.7.0",
PaymentDetails = new {
SessionTranID = "ccl-session-789"
}
}
};
var resp = await client.PostAsJsonAsync("https://localhost:8600/PMI/EndPaymentSession", payload);
var root = await resp.Content.ReadFromJsonAsync();
// root.GetProperty("Message").GetProperty("Status").GetString(); require 'net/http'
require 'json'
require 'openssl'
uri = URI('https://localhost:8600/PMI/EndPaymentSession')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # dev only
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['X-ClientId'] = 'POS1'
req['X-Operation'] = 'EndPaymentSession'
req.body = {
Message: {
ClientID: 'POS1',
MessageType: 'Request',
Operation: 'EndPaymentSession',
RequestID: '2004',
Version: '4.7.0',
PaymentDetails: {
SessionTranID: 'ccl-session-789'
}
}
}.to_json
msg = JSON.parse(http.request(req).body)['Message']
# msg['Status'] == 'Success'import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.sslContext(buildTrustAllContext()) // see pmi-integration guide
.build();
String jsonBody = """{"Message":{"ClientID":"POS1","MessageType":"Request","Operation":"EndPaymentSession","RequestID":"2004","Version":"4.7.0","PaymentDetails":{"SessionTranID":"ccl-session-789"}}}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8600/PMI/EndPaymentSession"))
.header("Content-Type", "application/json")
.header("X-ClientId", "POS1")
.header("X-Operation", "EndPaymentSession")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Message.Status === "Success" → PIN pad idle package main
import (
"bytes"; "crypto/tls"; "encoding/json"; "net/http"
)
func main() {
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
body, _ := json.Marshal(map[string]any{
"Message": map[string]any{
"ClientID": "POS1",
"MessageType": "Request",
"Operation": "EndPaymentSession",
"RequestID": "2004",
"Version": "4.7.0",
"PaymentDetails": map[string]any{
"SessionTranID": "ccl-session-789",
},
},
})
req, _ := http.NewRequest("POST", "https://localhost:8600/PMI/EndPaymentSession", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ClientId", "POS1")
req.Header.Set("X-Operation", "EndPaymentSession")
resp, _ := client.Do(req)
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
// result["Message"].(map[string]any)["Status"]}API reference
All operations use POST https://localhost:8600/PMI/{Operation} with JSON body. Every request requires two HTTP headers:
| Header | Required | Description |
|---|---|---|
X-ClientId | Yes | Unique identifier for this POS client (e.g. POS1) |
X-Operation | No | Operation name — mirrors the URL path (e.g. GetEngineStatus) |
Content-Type | Yes | application/json |
Every request body wraps fields inside a Message object. These common fields appear in every request:
| Field | Type | Required | Description |
|---|---|---|---|
ClientID | string | Yes | Same value as X-ClientId header. Echoed back in response. |
MessageType | string | Yes | Always "Request" for outgoing messages. Response will be "Response". |
Operation | string | Yes | The operation name. Must match the endpoint path (e.g. "Purchase"). |
RequestID | string | Yes | Unique ID per request. Echoed back in response for correlation. |
Version | string | Yes | API version. Use "4.7.0" for current release. |
PosTranID | string | No | POS-generated transaction ID, unique per customer interaction. Echoed back. Ties all CCL requests for one POS transaction. |
POST /GetEngineStatus
Determines the current state of the PMI WebServer engine. Call this at startup and after any UI restart to determine whether Initialize or OpenLane are needed before processing transactions.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
ConfigOptions.CompanyNumber | integer | Yes | Merchant company ID at the host. 6 digits or less. |
ConfigOptions.StoreNumber | integer | Yes | Store number within the company. Pass 0 for status-only check. |
ConfigOptions.LaneNumber | integer | Yes | Lane/register number within the store. Pass 0 for status-only check. |
Response fields
| Field | Type | Description |
|---|---|---|
StatusInfo.EngineStatusCode | integer | Current engine state (see table below) |
StatusInfo.EngineStatusMessage | string | Human-readable description of the status |
ConnectionInfo | string[] | List of available PIN pad device GUIDs. Present when EngineStatusCode is 3. |
Status | string | Success or Failure |
StatusCode | integer | 0 = success. See status codes. |
EngineStatusCode values
| Code | Meaning | Action required |
|---|---|---|
0 | Ready for transactions | None — proceed to BeginPaymentSession |
1 | Start needed | Call StartEngine → Initialize → OpenLane |
2 | Initialize needed | Call Initialize → OpenLane |
3 | OpenLane needed | Call OpenLane with returned ConnectionInfo GUIDs |
POST /Initialize
Performs the initial configuration download from the NCR payment gateway. Downloads EMV profiles, card profiles, and returns a list of available PIN pad devices. Call once at application startup when EngineStatusCode is 1 or 2.
Request fields — ConfigOptions
| Field | Type | Required | Description |
|---|---|---|---|
CompanyNumber | integer | Yes | Merchant company ID. 6 digits or less. Provided by NCR during onboarding. |
StoreNumber | integer | Yes | Store number within the company. |
LaneNumber | integer | Yes | Lane number within the store. 4 digits or less. |
HostAddress | string | Yes | Primary transaction host address (e.g. seps1-rls.paymentslab.ncr.com) |
HostPort | integer | Yes | Port for the primary host. Typically 443. |
HostAddressSecondary | string | No | Backup host address used when primary is unavailable. |
HostPortSecondary | integer | No | Port for the secondary host. |
ConfigAddress | string | Yes | Configuration host address for downloading card/EMV profiles. |
ConfigPort | integer | Yes | Port for the config host. Typically 443. |
HMACClientKey | string | Yes | HMAC client key provided by NCR. Prefix with sha256: for hashed keys. |
HMACSecretKey | string | Yes | HMAC secret key provided by NCR. Never log or expose this value. |
Request fields — PosOptions
| Field | Type | Required | Description |
|---|---|---|---|
AppName | string | No | Name of your POS application (e.g. "MyPOS"). Reported to host. |
AppVersion | string | No | Version of your POS application. |
TreatCLasEPS | boolean | No | If false, on CloseLane CCL will reverse any approved-but-open financial transactions. |
Response fields
| Field | Type | Description |
|---|---|---|
ConnectionInfo | string[] | List of available PIN pad GUIDs. Format: "DeviceName|GUID" e.g. "Engage COM9|315F2791-...". Pass to OpenLane. |
DeviceConnectionType | string | Connection type, e.g. USB, Bluetooth |
VersionInfo.CCL | string | CCL engine version installed on this terminal |
Timeout.WaitForPinpadInitializeSeconds | integer | How long to wait for PIN pad initialization |
Status / StatusCode | string / int | Success + 0, or Failure + error code |
POST /OpenLane
Opens the lane and connects to the PIN pad device. Registers the device with the payment gateway, applies any pending firmware updates, and validates encryption keys. Call at minimum once per business day. Must be called after Initialize when EngineStatusCode is 2 or 3.
Pass the complete ConfigOptions object (same fields as Initialize) in every OpenLane request. Omitting CompanyNumber, ConfigAddress, or similar fields causes "device connection failed" on cold starts because CCL needs them to resolve the device and download firmware.
Also extract the GUID-only portion from ConnectionInfo: entries are "DisplayName|GUID" — split on | and pass only the GUID (e.g. "315F2791-…").
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
ConnectionInfo | string[] | Yes | GUID-only portion of the device identifiers from Initialize. Split "DisplayName|GUID" on | and pass the GUID part. |
ConfigOptions.CompanyNumber | integer | Yes | Merchant company ID. Same value as used in Initialize. |
ConfigOptions.StoreNumber | integer | Yes | Store number within the company. |
ConfigOptions.LaneNumber | integer | Yes | Lane/register number. |
ConfigOptions.ConfigAddress | string | Yes | Configuration host address. Same as Initialize. |
ConfigOptions.ConfigPort | integer | Yes | Config host port. |
ConfigOptions.HostAddress | string | Yes | Primary transaction host address. |
ConfigOptions.HostPort | integer | Yes | Primary host port. Typically 443. |
ConfigOptions.HostAddressSecondary | string | No | Backup host address. |
ConfigOptions.HostPortSecondary | integer | No | Backup host port. |
ConfigOptions.HMACClientKey | string | Yes | HMAC client key for gateway authentication. |
ConfigOptions.HMACSecretKey | string | Yes | HMAC secret key for gateway authentication. |
Response fields
| Field | Type | Description |
|---|---|---|
Status | string | Success — lane is open, PIN pad ready for transactions |
StatusCode | integer | 0 = success. 76 = OpenLane already in progress. |
TransactionDateTime | string | Local timestamp: yyyy-MM-dd HH:mm:ss |
POST /BeginPaymentSession
Starts a new payment session for a customer transaction. Must be called before any financial operations (GetCardDetails, Purchase, etc.). Multiple Purchase requests are allowed within a single session for partial approval scenarios.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
PosTranID | string | Recommended | Your POS transaction ID. Ties all CCL requests for this customer interaction together. Echoed in all subsequent responses. |
PosOptions.DisableCashback | boolean | No | Set true to suppress the cashback prompt on the PIN pad. |
PosOptions.TrainingMode | boolean | No | Set true to process a training/demo transaction. Not submitted to the host. |
Response fields
| Field | Type | Description |
|---|---|---|
PaymentDetails.SessionTranID | string | Store this value. Required in all subsequent operations within this session (GetCardDetails, Purchase, EndPaymentSession). |
Status | string | Success or Failure. Failure with StatusCode: 78 = device offline. |
StatusCode | integer | 0 = success. 78 = device offline. 39 = already processing. |
POST /GetCardDetails
Activates the PIN pad and prompts the customer to present their card. This is a long-polling call — it blocks until the card is read or the request times out. Returns card type information that can be used for pre-processing (e.g., discounting), and returns a PaymentTranID for use in Purchase.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
Amounts.AmountDue | string | Yes | Total amount owed by the customer. Format: "15.00" |
Amounts.AmountTendered | string | Yes | Amount the customer is paying. Usually equal to AmountDue. Must include cashback/donation if applicable. |
Amounts.TipAmount | string | No | Pre-set tip amount. If not set and PromptTip is enabled, PIN pad will prompt. |
Amounts.CashbackAmount | string | No | Cashback amount requested. If provided, must also be included in AmountTendered. |
Amounts.DonationAmount | string | No | Donation amount. Only submit if donations are disabled in config or DisableOptionalPrompts is true. |
PaymentDetails.SessionTranID | string | Yes | Session ID from BeginPaymentSession response. |
PosOptions.TranType | string | Yes | Transaction type. Common values: Purchase, Return, Void, Preauth. |
PosOptions.DisableOptionalPrompts | boolean | No | Set true to suppress cashback, donation, phone number, and transaction type prompts on the PIN pad. |
CardDetails[].EntryMethod | string | No | Force a specific entry method: Manual (key entry). Omit to allow any method (chip/tap/swipe). |
Response fields
| Field | Type | Description |
|---|---|---|
PaymentDetails.PaymentTranID | string | Store this value. Pass to Purchase to finalize the transaction. |
PaymentDetails.SessionTranID | string | Echoed from request. |
CardDetails[].CardType | string | Card type: Credit, Debit, GiftCard, EBT, etc. |
CardDetails[].CardBrand | string | Card network: Visa, Mastercard, Discover, Amex, etc. |
CardDetails[].EntryMethod | string | How card was read: ContactEmv, ContactlessEmv, Swiped, Manual |
CardDetails[].FirstSix | string | First 6 digits of PAN (BIN). Can be used for loyalty/discount lookups. |
CardDetails[].LastFour | string | Last 4 digits of PAN for display on receipt. |
CardDetails[].AID | string | EMV Application Identifier (chip cards only). |
CardDetails[].CardProfiles | string | Comma-separated card profile list, e.g. "Credit,Debit" |
DeviceData.SerialNumber | string | PIN pad serial number. |
Amounts.CashbackAmount | string | Cashback amount if prompted on PIN pad. Must be re-submitted in the Purchase request. |
POST /Purchase
Submits the transaction to the payment gateway for authorization. Must be called within an active BeginPaymentSession. If GetCardDetails was called first, pass the returned PaymentTranID. If cashback or donation amounts were returned in GetCardDetails, they must be re-submitted here and included in AmountTendered.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
Amounts.AmountDue | string | Yes | Total amount due. Must match the amount used in GetCardDetails. |
Amounts.AmountTendered | string | Yes | Amount being charged. Must include cashback and/or donation if applicable. |
Amounts.CashbackAmount | string | Conditional | Required if cashback was returned by GetCardDetails. Omitting it removes cashback from the transaction. |
Amounts.DonationAmount | string | Conditional | Required if donation was collected during GetCardDetails. Omitting it removes the donation. |
Amounts.TipAmount | string | No | Tip amount. Must be included in AmountTendered. |
Amounts.FeeAmount | string | No | Convenience/service fee amount. |
PaymentDetails.SessionTranID | string | Yes | Session ID from BeginPaymentSession. |
PaymentDetails.PaymentTranID | string | Conditional | Card token from GetCardDetails. Required when GetCardDetails was called. Omit for direct-to-purchase flow. |
PaymentDetails.FeeType | string | No | Fee type when FeeAmount is set. E.g. Service. |
PosOptions.AllowAdjustment | boolean | No | Whether to allow post-auth tip/amount adjustment. |
PosOptions.UseToken | boolean | No | Set true to authorize using a stored token instead of card present data. |
PosOptions.TaxExempt | boolean | No | Set true to flag this transaction as tax-exempt. |
PosOptions.AdjustmentType | string | No | Incremental for incremental auth, Adjustment for amount adjustment. |
Response fields
| Field | Type | Description |
|---|---|---|
PaymentDetails.AuthCode | string | Authorization code from the issuer. Print on customer receipt. |
PaymentDetails.ResponseCode | string | Host response code (e.g. "000" = approved). |
PaymentDetails.ResponseMessage | string | Human-readable host response (e.g. "APPROVED"). |
PaymentDetails.ReferenceID | string | Unique host reference ID. Store for voids/refunds. |
PaymentDetails.MerchantID | string | Merchant ID at the host. |
PaymentDetails.TerminalID | string | Terminal ID at the host. |
PaymentDetails.Stan | string | System Trace Audit Number — unique per transaction at the host. |
PaymentDetails.VerificationMethod | string | CVM used: Pin, Signature, NotRequired, etc. |
Amounts.AmountApproved | string | Amount actually approved. May be less than AmountTendered on partial approvals. |
CardDetails[].CardBrand | string | Card network used for this transaction. |
CardDetails[].LastFour | string | Last 4 digits of PAN. Print on receipt. |
Receipts | array | Ready-to-print receipt lines for Merchant and Customer copies. Each entry has ReceiptType, ReceiptSection, and ReceiptLines. |
HostData.ReferenceID | string | Host-level reference ID. |
Status / StatusCode | string / int | Success/0 = approved. Failure/20 = declined. |
SoftDeclineRetryCount | integer | Present on soft declines (StatusCode 200–299). Indicates retry attempt number. |
POST /EndPaymentSession
Closes the payment session and returns the PIN pad to idle state. Always call this after every transaction — approved, declined, or cancelled — to release the session slot and clear the PIN pad display.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
PaymentDetails.SessionTranID | string | Yes | Session ID from BeginPaymentSession to close. |
Response fields
| Field | Type | Description |
|---|---|---|
Status | string | Success — session closed, PIN pad returned to idle |
StatusCode | integer | 0 = success |
POST /EnableCardEntry
Activates the PIN pad for card presentation within an existing payment session. Use this for the swipe-ahead pattern: call it immediately after BeginPaymentSession while the cashier is still scanning items. The customer can tap or swipe their card; when the cashier presses Pay, GetCardDetails returns instantly because the card was already read.
BeginPaymentSession → EnableCardEntry → GetCardDetails. Never call EnableCardEntry without an active SessionTranID.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
PaymentDetails.SessionTranID | string | Yes | Session ID from BeginPaymentSession. |
PosOptions.DisableOptionalPrompts | boolean | No | Set true to suppress cashback, donation, and other optional prompts while the card is activated. |
Response fields
| Field | Type | Description |
|---|---|---|
Status | string | Success — PIN pad is now active and waiting for a card |
StatusCode | integer | 0 = success. 78 = device offline. |
POST /StartNotifications & StopNotifications
Control the CCL notification queue. Call StartNotifications to begin receiving device status messages via GetNotifications polling. Call StopNotifications to stop. There are two use cases:
- During
OpenLane— captures device status messages like "Updating firmware", "Initializing device", "Device ready". Call Start before OpenLane, poll GetNotifications in parallel, call Stop after OpenLane returns. - During card entry — captures customer-facing prompts like "Please Tap Card", "Processing…". Call Start before
GetCardDetails, poll in parallel, stop after card is read.
Neither StartNotifications nor StopNotifications requires a request body beyond the standard Message wrapper.
POST /CloseLane
Disconnects the PIN pad and closes the lane. Optionally reverses any approved-but-unsettled transactions depending on TreatCLasEPS set during Initialize. Only call CloseLane if OpenLane previously succeeded — calling it when no lane is open returns an error and is unnecessary.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
| (no additional fields) | — | — | Standard Message wrapper only. No ConfigOptions or ConnectionInfo required. |
Response fields
| Field | Type | Description |
|---|---|---|
Status | string | Success — lane closed, PIN pad disconnected |
StatusCode | integer | 0 = success. 75 = lane was not open. |
Status codes
Every response includes Status (string) and StatusCode (integer). Codes 1–199 are hard declines. Codes 200–299 are soft declines — the transaction can be resubmitted with additional data.
| Code | Meaning | Type |
|---|---|---|
0 | Success | — |
7 | Cancelled by operator or customer | Hard |
17 | Use chip reader (fallback not allowed) | Hard |
19 | Timed out waiting for card / response | Hard |
20 | Transaction declined by issuer | Hard |
22 | Bad card swipe — retry | Hard |
24 | Chip failure — try swipe | Hard |
42 | PIN pad firmware update in progress | Hard |
60 | Card expired | Hard |
61 | Invalid cashback amount | Hard |
77 | Host offline | Hard |
78 | Device offline | Hard |
200 | Input needed (prompt required on POS side) | Soft |
202 | Customer rejected amount — resubmit with updated AmountTendered | Soft |
205 | Insufficient funds | Soft |
300 | Training mode: Approved (not a real authorization) | — |
Test your integration
Use mock mode
For development without physical hardware, enable mock mode by setting VITE_PMI_MOCK=true in your environment. All API calls return simulated responses with realistic delays.
Test with physical cards
When testing with real PIN pads in a sandbox environment, use test card amounts ending in specific decimal values to trigger different responses:
| Amount ending | Response |
|---|---|
.00 |
Payment approved |
.05 |
Generic decline |
.55 |
Incorrect PIN |
.01 |
Call issuer |
For example, a payment of $25.00 succeeds; $10.05 is declined.
Go live
Before accepting real payments, complete these steps:
- Verify merchant credentials with NCR
- Test successful and declined transactions
- Implement error handling and session cleanup
- Configure logging (never log full card numbers)
- Switch from test mode to production credentials
- Process a test transaction in live mode
Your PMI integration is complete. Customers can now pay with chip, contactless, or swipe cards at your POS terminal.
Next steps
After setting up your integration, implement these features:
- Generate receipts for customers
- Handle refunds and voids
- Implement fuel prepay workflows
- Add offline fallback handling