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

DeviceSupported connection
Ingenico Lane/8000TCP/IP, USB
Moby 5500TCP/IP, USB
Verifone M425 Neo / M400TCP/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.

Repository access

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:

ParameterDescription
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.

Lab vs production credentials

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)
Keep credentials 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.

PlatformPackageFormat
Windows (x64)WindowsCommonClientSDK-26.6.3783.zipZIP
Linux (x64)LinuxCommonClientSDK-26.6.3783.tar.gzTAR.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:

  1. Connect the device via USB or serial cable
  2. Install any required device drivers
  3. Verify the device appears in your system's device manager
  4. Note the device's connection GUID (you'll get this from the Initialize response)

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
This guide uses the HTTP / Web Server method

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.

POS Application Your backend / UI PMI WebServer localhost:8600 PIN Pad Device Verifone / Ingenico Payment Gateway NCR Voyix Host 1 STARTUP GetEngineStatus EngineStatusCode: 2 (needs init) 2 INIT + NOTIFY Initialize (credentials + host config) Download config & register merchant Config + ConnectionInfo (device GUIDs) ConnectionInfo [ "Engage COM9|GUID…" ] → extract GUID after | StartNotifications Notification queue active OpenLane (full ConfigOptions + GUID only) Connect device, apply firmware updates GetNotifications ← "Updating firmware…" GetNotifications ← "Initializing device…" GetNotifications ← "Device ready" StopNotifications Lane open — StatusCode: 0 (Ready) ✓ laneOpenedRef = true 3 SWIPE-AHEAD Triggered on 1st item scan — card entry starts before cashier presses Pay BeginPaymentSession SessionTranID: "ccl-session-789" EnableCardEntry (SessionTranID) PIN pad activated — awaiting card 4 SESSION Swipe-ahead SessionTranID reused — BeginPaymentSession skipped GetCardDetails (amount, SessionTranID) Prompt: tap / insert / swipe card 5 PAYMENT Card data (EMV chip / NFC / swipe) PaymentTranID: "pmt-tran-456" Purchase (SessionTranID, PaymentTranID) Authorization request (card + amount) Approved — AuthCode, ReferenceID Display approval & receipt prompt APPROVED — AuthCode + ReferenceID 6 CLOSE SESSION EndPaymentSession (SessionTranID) Return to idle / clear display Session closed — PIN pad idle 7 CLOSE LANE Conditional — only called if laneOpenedRef = true (lane was successfully opened) CloseLane Disconnect device, reverse open transactions Lane closed — StatusCode: 0 solid = request dashed = response POS ↔ PMI PMI ↔ PIN Pad PMI ↔ Gateway Device notifications

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 lane
import 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 lane
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", "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 lane
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":"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 transactions
  • 1 — CCL service not started
  • 2 — Needs initialization
  • 3 — 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 pipe
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", "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. ConnectionInfo entries 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, OpenLane requires all config fields (including CompanyNumber, 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 ready
import 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 ready
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", "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 ready
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":"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"}
Device notifications during OpenLane

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 entry
import 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 entry
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", "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 ready
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":"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.ReferenceID
import 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 idle
import 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:

HeaderRequiredDescription
X-ClientIdYesUnique identifier for this POS client (e.g. POS1)
X-OperationNoOperation name — mirrors the URL path (e.g. GetEngineStatus)
Content-TypeYesapplication/json

Every request body wraps fields inside a Message object. These common fields appear in every request:

FieldTypeRequiredDescription
ClientIDstringYesSame value as X-ClientId header. Echoed back in response.
MessageTypestringYesAlways "Request" for outgoing messages. Response will be "Response".
OperationstringYesThe operation name. Must match the endpoint path (e.g. "Purchase").
RequestIDstringYesUnique ID per request. Echoed back in response for correlation.
VersionstringYesAPI version. Use "4.7.0" for current release.
PosTranIDstringNoPOS-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

FieldTypeRequiredDescription
ConfigOptions.CompanyNumberintegerYesMerchant company ID at the host. 6 digits or less.
ConfigOptions.StoreNumberintegerYesStore number within the company. Pass 0 for status-only check.
ConfigOptions.LaneNumberintegerYesLane/register number within the store. Pass 0 for status-only check.

Response fields

FieldTypeDescription
StatusInfo.EngineStatusCodeintegerCurrent engine state (see table below)
StatusInfo.EngineStatusMessagestringHuman-readable description of the status
ConnectionInfostring[]List of available PIN pad device GUIDs. Present when EngineStatusCode is 3.
StatusstringSuccess or Failure
StatusCodeinteger0 = success. See status codes.

EngineStatusCode values

CodeMeaningAction required
0Ready for transactionsNone — proceed to BeginPaymentSession
1Start neededCall StartEngine → Initialize → OpenLane
2Initialize neededCall Initialize → OpenLane
3OpenLane neededCall 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

FieldTypeRequiredDescription
CompanyNumberintegerYesMerchant company ID. 6 digits or less. Provided by NCR during onboarding.
StoreNumberintegerYesStore number within the company.
LaneNumberintegerYesLane number within the store. 4 digits or less.
HostAddressstringYesPrimary transaction host address (e.g. seps1-rls.paymentslab.ncr.com)
HostPortintegerYesPort for the primary host. Typically 443.
HostAddressSecondarystringNoBackup host address used when primary is unavailable.
HostPortSecondaryintegerNoPort for the secondary host.
ConfigAddressstringYesConfiguration host address for downloading card/EMV profiles.
ConfigPortintegerYesPort for the config host. Typically 443.
HMACClientKeystringYesHMAC client key provided by NCR. Prefix with sha256: for hashed keys.
HMACSecretKeystringYesHMAC secret key provided by NCR. Never log or expose this value.

Request fields — PosOptions

FieldTypeRequiredDescription
AppNamestringNoName of your POS application (e.g. "MyPOS"). Reported to host.
AppVersionstringNoVersion of your POS application.
TreatCLasEPSbooleanNoIf false, on CloseLane CCL will reverse any approved-but-open financial transactions.

Response fields

FieldTypeDescription
ConnectionInfostring[]List of available PIN pad GUIDs. Format: "DeviceName|GUID" e.g. "Engage COM9|315F2791-...". Pass to OpenLane.
DeviceConnectionTypestringConnection type, e.g. USB, Bluetooth
VersionInfo.CCLstringCCL engine version installed on this terminal
Timeout.WaitForPinpadInitializeSecondsintegerHow long to wait for PIN pad initialization
Status / StatusCodestring / intSuccess + 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.

Send all ConfigOptions — not just HMAC/Host

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

FieldTypeRequiredDescription
ConnectionInfostring[]YesGUID-only portion of the device identifiers from Initialize. Split "DisplayName|GUID" on | and pass the GUID part.
ConfigOptions.CompanyNumberintegerYesMerchant company ID. Same value as used in Initialize.
ConfigOptions.StoreNumberintegerYesStore number within the company.
ConfigOptions.LaneNumberintegerYesLane/register number.
ConfigOptions.ConfigAddressstringYesConfiguration host address. Same as Initialize.
ConfigOptions.ConfigPortintegerYesConfig host port.
ConfigOptions.HostAddressstringYesPrimary transaction host address.
ConfigOptions.HostPortintegerYesPrimary host port. Typically 443.
ConfigOptions.HostAddressSecondarystringNoBackup host address.
ConfigOptions.HostPortSecondaryintegerNoBackup host port.
ConfigOptions.HMACClientKeystringYesHMAC client key for gateway authentication.
ConfigOptions.HMACSecretKeystringYesHMAC secret key for gateway authentication.

Response fields

FieldTypeDescription
StatusstringSuccess — lane is open, PIN pad ready for transactions
StatusCodeinteger0 = success. 76 = OpenLane already in progress.
TransactionDateTimestringLocal 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

FieldTypeRequiredDescription
PosTranIDstringRecommendedYour POS transaction ID. Ties all CCL requests for this customer interaction together. Echoed in all subsequent responses.
PosOptions.DisableCashbackbooleanNoSet true to suppress the cashback prompt on the PIN pad.
PosOptions.TrainingModebooleanNoSet true to process a training/demo transaction. Not submitted to the host.

Response fields

FieldTypeDescription
PaymentDetails.SessionTranIDstringStore this value. Required in all subsequent operations within this session (GetCardDetails, Purchase, EndPaymentSession).
StatusstringSuccess or Failure. Failure with StatusCode: 78 = device offline.
StatusCodeinteger0 = 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

FieldTypeRequiredDescription
Amounts.AmountDuestringYesTotal amount owed by the customer. Format: "15.00"
Amounts.AmountTenderedstringYesAmount the customer is paying. Usually equal to AmountDue. Must include cashback/donation if applicable.
Amounts.TipAmountstringNoPre-set tip amount. If not set and PromptTip is enabled, PIN pad will prompt.
Amounts.CashbackAmountstringNoCashback amount requested. If provided, must also be included in AmountTendered.
Amounts.DonationAmountstringNoDonation amount. Only submit if donations are disabled in config or DisableOptionalPrompts is true.
PaymentDetails.SessionTranIDstringYesSession ID from BeginPaymentSession response.
PosOptions.TranTypestringYesTransaction type. Common values: Purchase, Return, Void, Preauth.
PosOptions.DisableOptionalPromptsbooleanNoSet true to suppress cashback, donation, phone number, and transaction type prompts on the PIN pad.
CardDetails[].EntryMethodstringNoForce a specific entry method: Manual (key entry). Omit to allow any method (chip/tap/swipe).

Response fields

FieldTypeDescription
PaymentDetails.PaymentTranIDstringStore this value. Pass to Purchase to finalize the transaction.
PaymentDetails.SessionTranIDstringEchoed from request.
CardDetails[].CardTypestringCard type: Credit, Debit, GiftCard, EBT, etc.
CardDetails[].CardBrandstringCard network: Visa, Mastercard, Discover, Amex, etc.
CardDetails[].EntryMethodstringHow card was read: ContactEmv, ContactlessEmv, Swiped, Manual
CardDetails[].FirstSixstringFirst 6 digits of PAN (BIN). Can be used for loyalty/discount lookups.
CardDetails[].LastFourstringLast 4 digits of PAN for display on receipt.
CardDetails[].AIDstringEMV Application Identifier (chip cards only).
CardDetails[].CardProfilesstringComma-separated card profile list, e.g. "Credit,Debit"
DeviceData.SerialNumberstringPIN pad serial number.
Amounts.CashbackAmountstringCashback 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

FieldTypeRequiredDescription
Amounts.AmountDuestringYesTotal amount due. Must match the amount used in GetCardDetails.
Amounts.AmountTenderedstringYesAmount being charged. Must include cashback and/or donation if applicable.
Amounts.CashbackAmountstringConditionalRequired if cashback was returned by GetCardDetails. Omitting it removes cashback from the transaction.
Amounts.DonationAmountstringConditionalRequired if donation was collected during GetCardDetails. Omitting it removes the donation.
Amounts.TipAmountstringNoTip amount. Must be included in AmountTendered.
Amounts.FeeAmountstringNoConvenience/service fee amount.
PaymentDetails.SessionTranIDstringYesSession ID from BeginPaymentSession.
PaymentDetails.PaymentTranIDstringConditionalCard token from GetCardDetails. Required when GetCardDetails was called. Omit for direct-to-purchase flow.
PaymentDetails.FeeTypestringNoFee type when FeeAmount is set. E.g. Service.
PosOptions.AllowAdjustmentbooleanNoWhether to allow post-auth tip/amount adjustment.
PosOptions.UseTokenbooleanNoSet true to authorize using a stored token instead of card present data.
PosOptions.TaxExemptbooleanNoSet true to flag this transaction as tax-exempt.
PosOptions.AdjustmentTypestringNoIncremental for incremental auth, Adjustment for amount adjustment.

Response fields

FieldTypeDescription
PaymentDetails.AuthCodestringAuthorization code from the issuer. Print on customer receipt.
PaymentDetails.ResponseCodestringHost response code (e.g. "000" = approved).
PaymentDetails.ResponseMessagestringHuman-readable host response (e.g. "APPROVED").
PaymentDetails.ReferenceIDstringUnique host reference ID. Store for voids/refunds.
PaymentDetails.MerchantIDstringMerchant ID at the host.
PaymentDetails.TerminalIDstringTerminal ID at the host.
PaymentDetails.StanstringSystem Trace Audit Number — unique per transaction at the host.
PaymentDetails.VerificationMethodstringCVM used: Pin, Signature, NotRequired, etc.
Amounts.AmountApprovedstringAmount actually approved. May be less than AmountTendered on partial approvals.
CardDetails[].CardBrandstringCard network used for this transaction.
CardDetails[].LastFourstringLast 4 digits of PAN. Print on receipt.
ReceiptsarrayReady-to-print receipt lines for Merchant and Customer copies. Each entry has ReceiptType, ReceiptSection, and ReceiptLines.
HostData.ReferenceIDstringHost-level reference ID.
Status / StatusCodestring / intSuccess/0 = approved. Failure/20 = declined.
SoftDeclineRetryCountintegerPresent 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

FieldTypeRequiredDescription
PaymentDetails.SessionTranIDstringYesSession ID from BeginPaymentSession to close.

Response fields

FieldTypeDescription
StatusstringSuccess — session closed, PIN pad returned to idle
StatusCodeinteger0 = 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.

Required order

BeginPaymentSession → EnableCardEntry → GetCardDetails. Never call EnableCardEntry without an active SessionTranID.

Request fields

FieldTypeRequiredDescription
PaymentDetails.SessionTranIDstringYesSession ID from BeginPaymentSession.
PosOptions.DisableOptionalPromptsbooleanNoSet true to suppress cashback, donation, and other optional prompts while the card is activated.

Response fields

FieldTypeDescription
StatusstringSuccess — PIN pad is now active and waiting for a card
StatusCodeinteger0 = 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

FieldTypeRequiredDescription
(no additional fields)——Standard Message wrapper only. No ConfigOptions or ConnectionInfo required.

Response fields

FieldTypeDescription
StatusstringSuccess — lane closed, PIN pad disconnected
StatusCodeinteger0 = 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.

CodeMeaningType
0Success—
7Cancelled by operator or customerHard
17Use chip reader (fallback not allowed)Hard
19Timed out waiting for card / responseHard
20Transaction declined by issuerHard
22Bad card swipe — retryHard
24Chip failure — try swipeHard
42PIN pad firmware update in progressHard
60Card expiredHard
61Invalid cashback amountHard
77Host offlineHard
78Device offlineHard
200Input needed (prompt required on POS side)Soft
202Customer rejected amount — resubmit with updated AmountTenderedSoft
205Insufficient fundsSoft
300Training 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:

  1. Verify merchant credentials with NCR
  2. Test successful and declined transactions
  3. Implement error handling and session cleanup
  4. Configure logging (never log full card numbers)
  5. Switch from test mode to production credentials
  6. Process a test transaction in live mode
You're ready to accept payments

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