Connecting to the PamiPOP API

Device Discovery

PamiPOP device can be discovered on your local network by sending a UDP broadcast message to the port 7263.

The message should be Who has <ENDPOINT_ID> where <ENDPOINT_ID> is the unique identifier of your PamiPOP device. The device will respond with I have <ENDPOINT_ID> and you can then use the IP address of the responding device to create a connection for API calls.

An example of the discovery process implemented in C# (.NET) is shown below:

const int port = 7263;
const string endpointId = "NY_009_OS_PRAG";

async Task Main(string[] args)
{
	var tokens = new CancellationTokenSource();
	var udpClient = new UdpClient(port) { EnableBroadcast = true };
	
	Listen(udpClient, tokens.Token);
	await Send(udpClient);
	
	await Task.Delay(10000); //wait 10 seconds
	tokens.Cancel();
	udpClient.Close();
}

async Task Send(UdpClient client)
{
	var message = $"Who has {endpointId}";
	var messageBytes = Encoding.ASCII.GetBytes(message);
	var broadcastEp = new IPEndPoint(IPAddress.Broadcast, port);
	
	await client.SendAsync(messageBytes, messageBytes.Length, broadcastEp);
}

void Listen(UdpClient client, CancellationToken cancellationToken)
{
	Task.Run(async () =>
	{
		while (!cancellationToken.IsCancellationRequested)
		{
			var received = await client.ReceiveAsync(cancellationToken);

			var message = Encoding.ASCII.GetString(received.Buffer);
			Console.WriteLine($"{received.RemoteEndPoint.Address} sent '{message}'");
		}
	}, cancellationToken);
}

The output from the code above will look like this:

192.168.1.54 sent 'I have NY_009_OS_PRAG'

The IP address shown in the output is the address of the PamiPOP device on your network. You can use this address to connect via WebSocket as described in the Connecting section below.

API Key Authentication

To authenticate to the PamiPOP, you will need a valid API key. This key is used to identify your system and ensure secure communication with the PamiPOP device.

Two headers are required for every WebSocket connection:

X-API-Key should contain your API key, which you can obtain directly from PamiTV. This key is unique to your system and should be kept private.

X-API-Client should contain a string that identifies your system, such as the name of your application or service. This must be the same name you used to register for an API key.

Connecting

To interact with the API, establish a WebSocket connection to the following endpoint:

ws://<host>:7264/commands

Replace <host> with your PamiPOP's address found during the discovery process. See the Device Discovery section for more details.

Example (using C#):

var ipAddress = "192.168.1.54";
var webSocket = new ClientWebSocket();
webSocket.Options.SetRequestHeader("X-API-Key", "your_key_here\keep\private==");
webSocket.Options.SetRequestHeader("X-API-Client", "Your System's Name Goes Here");

await webSocket.ConnectAsync(new Uri($"ws://{ipAddress}:7264/commands"), CancellationToken.None);

Message Format

All communication is performed using structured messages. The primary message types are Command (sent by the client -- your system) and Result (sent by the PamiPOP device). Messages can be serialized as either JSON or XML.

When you connect your WebSocket, you can specify the desired format by setting the Content-Type header. If the Content-Type header is restricted in your programming framework (like .NET), you can instead set the X-Mime-Type header to select your message format.

Valid values for Content-Type or X-Mime-Type are:

If no header is provided, the default message format is JSON.

Notes:


Command Message

JSON Example

{
	"Name": "Prompt",
	"FlowId": "2e3c1f1a-4f6d-4b57-956a-800aaf3abba5",
	"DisplayText": "$10.23 - Approve Amount?",
	"FooterText": "",
	"Buttons": [
		{
			"Text": "Approve",
			"Value": "approve"
		},
		{
			"Text": "Reject",
			"Value": "reject",
			"AccentColor": "Red"
		}
	],
	"Data": null
}

XML Example

<?xml version="1.0" encoding="utf-8"?>
<Command>
	<Name>Prompt</Name>
	<FlowId>2e3c1f1a-4f6d-4b57-956a-800aaf3abba5</FlowId>
	<DisplayText>$10.23 - Approve Amount?</DisplayText>
	<FooterText />
	<Buttons>
		<Button>
			<Text>Approve</Text>
			<Value>approve</Value>
		</Button>
		<Button>
			<Text>Reject</Text>
			<Value>reject</Value>
			<AccentColor>Red</AccentColor>
		</Button>
	</Buttons>
	<Data />
</Command>

Fields:


Result Message

JSON Example

{
	"FlowId": "2e3c1f1a-4f6d-4b57-956a-800aaf3abba5",
	"CommandName": "Prompt",
	"SelectedButton": {
			"Text": "Approve",
			"Value": "approve"
		},
	"Value": null,
	"Data": null,
	"Error": null
}

XML Example

<?xml version="1.0" encoding="utf-8"?>
<Result>
	<FlowId>2e3c1f1a-4f6d-4b57-956a-800aaf3abba5</FlowId>
	<CommandName>Prompt</CommandName>
	<SelectedButton>
		<Text>Approve</Text>
		<Value>approve</Value>
	</SelectedButton>
	<Value />
	<Data />
	<Error />
</Result>

Fields:

Valid Command Names


Flows

A flow is a series of Command and Result messages that represent a complete interaction with the user. A flow must always begin with a StartFlow command and end with an EndFlow command. Between these two commands, you can send any number of other commands to guide the user through the desired interaction.

It is recommended to monitor for Result messages on a separate thread or task to ensure you can respond to user actions promptly. A common pattern is to treat interaction with PamiPOP as a pub-sub model where you send commands and listen for results asynchronously.

Example Flows

Prompting for User Input

In this example, we will prompt the user to approve a "round up" transaction. The steps are as follows:

  1. StartFlow (then wait for a FlowId to be returned)
  2. Prompt (then wait for a Result message with the selected button to be returned)
  3. EndFlow the WebSocket connection can be optionally closed after this command is sent.

Here is an example of this workflow in C#:

async Task Main()
{
	const string ipAddress = "192.168.1.54";
	const string _apiKey = "Keep/This/Private/XYZ==";
	const string _systemName = "Your System Name 1.0";
	
	//create the web socket connection
	var webSocket = new ClientWebSocket();
	webSocket.Options.SetRequestHeader("X-Mime-Type", "application/json");
	webSocket.Options.SetRequestHeader("X-API-Key", _apiKey);
	webSocket.Options.SetRequestHeader("X-API-Client", _systemName);
	await webSocket.ConnectAsync(new Uri($"ws://{ipAddress}:7264/commands"), CancellationToken.None);	
	
	//start the flow and get the flow id for future commands
	var startFlowCommand = new { Name = "StartFlow", DisplayText = "WELCOME!" };
	await Send(webSocket, startFlowCommand);
	var startFlowResult = await WaitForResponse(webSocket);
	Console.WriteLine($"Got new FlowId: {startFlowResult.FlowId}");

	//prompt the user to make a choice by tapping a button
	var promptCommand = new
	{
		FlowId = startFlowResult.FlowId,
		Name = "Prompt",
		DisplayText = "Would you like to round up for charity?",
		Buttons = new List<Button>() {
			new Button { Text = "Yes", Value = "yes", AccentColor = "Green" },
			new Button { Text = "No", Value = "no", AccentColor = "Red" }
		}
	};
	await Send(webSocket, promptCommand);
	var promptResult = await WaitForResponse(webSocket);
	Console.WriteLine($"The user chose the button with value '{promptResult.SelectedButton.Value}'");
	
	//end the flow and close the web socket
	var endFlowCommand = new { FlowId = startFlowResult.FlowId, Name = "EndFlow", DisplayText = "GOODBYE!" };
	await Send(webSocket, endFlowCommand);
	await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}

async Task Send(ClientWebSocket webSocket, object message)
{
	var json = JsonSerializer.Serialize(message);
	await webSocket.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);	
}

async Task<Result> WaitForResponse(ClientWebSocket webSocket)
{
	var buffer = new ArraySegment<byte>(new byte[8192]);
	WebSocketReceiveResult result = null;
	using var memStream = new MemoryStream();
	do
	{
		result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
		memStream.Write(buffer.Array, buffer.Offset, result.Count);
	}
	while (!result.EndOfMessage);
	
	memStream.Seek(0, SeekOrigin.Begin);
	
	return JsonSerializer.Deserialize<Result>(memStream);
}

public class Result
{
	public Guid FlowId { get; set; }
	public string CommandName { get; set; }
	public Button? SelectedButton { get; set; }
	public string? Data { get; set; }
	public decimal? Value { get; set; }
	public string? Error { get; set; }
}

public class Button
{
	public string Text { get; set; }
	public string Value { get; set; }
	public string AccentColor { get; set; }
}

The output from the code above will look like this:

Got new FlowId: 21bb4f7e-9185-4304-a706-d9b406a16b2c
The user chose the button with value 'yes'

Tips with Custom Amount Input

When capturing a custom amount from the user, you can use the NumberCapture command. An example of a flow that uses NumberCapture is below in C#:

async Task Main()
{
	const string ipAddress = "192.168.1.54";
	const string _apiKey = "Keep/This/Private/XYZ==";
	const string _systemName = "Your System Name 1.0";
	
	//create the web socket connection
	var webSocket = new ClientWebSocket();
	webSocket.Options.SetRequestHeader("X-Mime-Type", "application/json");
	webSocket.Options.SetRequestHeader("X-API-Key", _apiKey);
	webSocket.Options.SetRequestHeader("X-API-Client", _systemName);
	await webSocket.ConnectAsync(new Uri($"ws://{ipAddress}:7264/commands"), CancellationToken.None);
	
	//start the flow and get the flow id for future commands
	var startFlowCommand = new { Name = "StartFlow", DisplayText = "WELCOME!" };
	await Send(webSocket, startFlowCommand);
	var startFlowResult = await WaitForResponse(webSocket);
	Console.WriteLine($"Got new FlowId: {startFlowResult.FlowId}");

	//prompt the user to make a choice by tapping a button
	var promptCommand = new
	{
		FlowId = startFlowResult.FlowId,
		Name = "Prompt",
		DisplayText = "Would you like to add a tip?",
		Buttons = new List<Button>() {
			new Button { Text = "10% ($10.00)", Value = "10.00" },
			new Button { Text = "15% ($15.00)", Value = "15.00" },
			new Button { Text = "20% ($20.00)", Value = "20.00" },
			new Button { Text = "25% ($25.00)", Value = "25.00" },
			new Button { Text = "No Tip", Value = "0.00" },
			new Button { Text = "Custom", Value = "custom" },
		}
	};
	await Send(webSocket, promptCommand);
	var promptResult = await WaitForResponse(webSocket);
	Console.WriteLine($"The user chose the button with value '{promptResult.SelectedButton.Value}'");
	
	if (promptResult.SelectedButton.Value == "custom")
	{
		//ask the user to enter a numeric value for their custom tip
		var numberCaptureCommand = new
		{
			FlowId = startFlowResult.FlowId,
			Name = "NumberCapture",
			DisplayText = "Enter Tip Amount",
			Data = new { Mask = "_____.__" } //number of decimal places allowed (e.g. 1.11 allowed, but 1.111 not allowed)
		};
		await Send(webSocket, numberCaptureCommand);
		var numberCaptureResult = await WaitForResponse(webSocket);
		Console.WriteLine($"The user entered {numberCaptureResult.Value} for a tip amount.");
	}
	
	var processingCommand = new
	{
		FlowId = startFlowResult.FlowId,
		Name = "Processing",
		DisplayText = "Processing. Please wait..."
	};
	await Send(webSocket, processingCommand);
	//simulate doing some work / processing.
	await Task.Delay(5000);
	
	var approvedCommand = new
	{
		FlowId = startFlowResult.FlowId,
		Name = "Approved",
		DisplayText = "APPROVED"
	};
	await Send(webSocket, approvedCommand);
	//wait to give time for the user to read the screen
	await Task.Delay(2000);
	
	//end the flow and close the web socket
	var endFlowCommand = new { FlowId = startFlowResult.FlowId, Name = "EndFlow", DisplayText = "THANK YOU. GOODBYE." };
	await Send(webSocket, endFlowCommand);
	await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}

The output from the example above looks like this:

Got new FlowId: 5fbf52ca-a9ef-4661-8f65-7f661c1305bd
The user chose the button with value 'custom'
The user entered 17.98 for a tip amount.

Using the Card Reader

Card Reader Commands

When using the CardReaderCommand, the Data element of a Command must contain a dictionary of key/value pairs that describe which card reader-specific command to run and what values to pass into its arguments. You should reference the section related to your card reader for more detailed key names, but the following keys are always required:

JSON Command Example

{
	"Name":"CardReaderCommand",
	"FlowId":"2e3c1f1a-4f6d-4b57-956a-800aaf3abba5",
	"DisplayText":"TAP, INSERT or SWIPE",
	"Data": {
		"CommandName":"emv_autoAuthenticate",
		"Authenticate":"true"
	}
}

XML Command Example

<Command>
	<Name>CardReaderCommand</Name>
	<FlowId>2e3c1f1a-4f6d-4b57-956a-800aaf3abba5</FlowId>
	<DisplayText>TAP, INSERT or SWIPE</DisplayText>
	<Data>
		<Element Key="CommandName" Value="emv_autoAuthenticate" />
		<Element Key="Authenticate" Value="true" />
	</Data>
</Command>

The command above is the equivalent of calling IDT_Device.emv_autoAuthenticate(true) in the IDTech SDK.

Please note that all the values in the Data dictionary are strings. If your card reader SDK expects a different type (e.g., integer, boolean), the PamiPOP will convert the string to the appropriate type before passing it to the card reader's SDK.

Card Reader Results

Results from the CardReaderCommand will be returned in the Data element of the result message. The structure of this data is specific to your card reader type. Below is an example from an IDTech card reader. Please note the data in this example is a JSON string containing escaped JSON.

{
	"CommandName":"CardReaderCommand",
	"FlowId":"2e3c1f1a-4f6d-4b57-956a-800aaf3abba5",
	"Data":"{\u0022State\u0022:\u0022TransactionData\u0022,\u0022Card\u0022:{\u0022msr_keyManagement\u0022:\u0022KEY_MANAGEMENT_UNKNOWN\u0022,\u0022Base64\u0022:\u0022VmlWT3RlY2gyAAIjAh3h3\\u002B4mAuEA3\\u002B4SCmKZSQCCABwgAMff70wGACUAAAAA3\\u002B9NKBEnICu1pe5BvJgOtiEEMrM0Koa2PoXdPJFLmb\\u002BVZWspIDk2CvTIW5zfgSkIMPDw8KDw/wDfgRYWG/8AABMAAAAAAAAAAAAAAAAAAAAAAP\\u002BBBYIBUk8HoAAAAAMQEFALVklTQSBDUkVESVRXoRNHYXPMzMwAQ9MRLMzMzMzMzMzMV8EY4d2e7etuEjj/3cjMkx2gqFtyRCztFSMQggIgAIQHoAAAAAMQEJUFAAAAAACWAQOaAyUJApwBAF8qAghAXy0CZW5fNAEBXzYBAp8CBgAAAAABAJ8DBgAAAAAAAJ8GB6AAAAADEBCfCQIAAp8QBwYBCgOgAACfFQIAAJ8aAghAnxsEAAAAAJ8eCDZUNDEwNzU0nyEDFBk1nyYIrV7/b2tuv\\u002BKfJwGAnzMDYCjInzQDPwAAnzUBIZ82AgDPnzcENf21uZ85AQefQAVgAAAwAJ9BBAAAATmfTiIxMDcyMSBXYWxrZXIgU3QuIEN5cHJlc3MsIENBICxVU0Eun1oFMQhACECfZgQygEAAn2wCOACfbgQgcAAA34EmBgAAAABQAN/uTAEG/\\u002B4BCt/uMAEA3\\u002B5bAQXf7BweCAMABP8AGwAAABP/AAAAAAAAAAAAAAAAAAAAAP8A3\\u002BwRIAnrN8qOeGl8pom3E4ze158rtRfDDCMX/16DR4/6HJuqhCY=\u0022,\u0022Event\u0022:\u0022EVENT_TRANSACTION_DATA_EMV_DATA\u0022,\u0022Notification\u0022:\u0022EVENT_NOTIFICATION_UNKNOWN\u0022,\u0022msr_track1Length\u0022:0,\u0022msr_track2Length\u0022:0,\u0022msr_track3Length\u0022:0,\u0022msr_cardType\u0022:\u0022CAPTURE_ENCODE_TYPE_UNKNOWN\u0022,\u0022msr_captureEncodeStatus\u0022:0,\u0022captureEncryptType\u0022:\u0022CAPTURE_ENCRYPT_TYPE_TDES\u0022,\u0022captureEncryptTypeEMV\u0022:\u0022CAPTURE_ENCRYPT_TYPE_UNKNOWN\u0022,\u0022captureCardType\u0022:\u0022CAPTURE_CARD_TYPE_CTLS_EMV\u0022,\u0022msr_errorCode\u0022:0,\u0022emv_rfStateCode\u0022:0,\u0022iccPresent\u0022:0,\u0022msr_keyVariantType\u0022:\u0022KEY_VARIANT_TYPE_UNKNOWN\u0022,\u0022isCTLS\u0022:1,\u0022ctlsApplication\u0022:\u0022CTLS_APPLICATION_NONE\u0022,\u0022emv_unencryptedTags\u0022:\u002234EWFhv/AAATAAAAAAAAAAAAAAAAAAAAAADfgSkIMPDw8KDw/wDf7BEgCes3yo54aXymibcTjN7Xnyu1F8MMIxf/XoNHj/ocm6rf7BweCAMABP8AGwAAABP/AAAAAAAAAAAAAAAAAAAAAP8A3\u002B4SCmKZSQCCABwgAMff7iYC4QDf70wGACUAAAAA3\u002B9NKBEnICu1pe5BvJgOtiEEMrM0Koa2PoXdPJFLmb\u002BVZWspIDk2CvTIW5z/gQWCAVJPB6AAAAADEBBQC1ZJU0EgQ1JFRElUV6ETR2FzzMzMAEPTESzMzMzMzMzMzFfBGOHdnu3rbhI4/93IzJMdoKhbckQs7RUjEIICIACEB6AAAAADEBCVBQAAAAAAlgEDmgMlCQKcAQBfKgIIQF8tAmVuXzQBAV82AQKfAgYAAAAAAQCfAwYAAAAAAACfBgegAAAAAxAQnwkCAAKfEAcGAQoDoAAAnxUCAACfGgIIQJ8bBAAAAACfHgg2VDQxMDc1NJ8hAxQZNZ8mCK1e/29rbr/inycBgJ8zA2AoyJ80Az8AAJ81ASGfNgIAz583BDX9tbmfOQEHn0AFYAAAMACfQQQAAAE5n04iMTA3MjEgV2Fsa2VyIFN0LiBDeXByZXNzLCBDQSAsVVNBLp9aBTEIQAhAn2YEMoBAAJ9sAjgAn24EIHAAAN\u002BBJgYAAAAAUADf7kwBBv/uAQrf7jABAN/uWwEF\u0022,\u0022emv_resultCode\u0022:\u0022EMV_RESULT_CODE_FAILED\u0022,\u0022emv_hasAdvise\u0022:false,\u0022emv_hasReversal\u0022:false,\u0022SW1\u0022:0,\u0022SW2\u0022:0,\u0022hasMACVerificationData\u0022:false,\u0022emv_transaction_Error_Code\u0022:0,\u0022emv_RF_State\u0022:\u0022RF_STATE_NONE\u0022,\u0022emv_ESC\u0022:0,\u0022emv_appErrorFn\u0022:\u0022CEMV_APP_ERROR_FN_NONE\u0022,\u0022emv_appErrorState\u0022:\u0022CEMV_APP_ERROR_STATE_NONE\u0022,\u0022captured_SHA256\u0022:false},\u0022TransactionResultCode\u0022:\u0022RETURN_CODE_DO_SUCCESS\u0022,\u0022Ip\u0022:\u0022VP3350:USB-1\u0022}"
}

IDTech Card Reader Commands

If your PamiPOP is equipped with an IDTech card reader, you can send commands to it that are equivalent to those exposed through IDTech's SDK. Supported card reader commands and their associated key/value dictionary elements are:

It is important for you to understand how the IDTech SDK works, as the commands you send to the PamiPOP will behave as if you were using the SDK directly.

For IDTech card readers, please note the Data element returned from a CardReaderCommand will be a string containing a JSON serialized object. This will be JSON regardless of which Content-Type you have chosen for your WebSocket connection.

IDTech card readers will return the following object in the Data element of the result:

{
    State: <DeviceState from the SDK>,
    Data: <Base64-encoded byte array>,
    Card: <IDTTransactionData from the SDK>,
    EmvCallback: <EMV_Callback from the SDK>,
    TransactionResultCode: <RETURN_CODE from the SDK>,
    Ip: <String containing the device name>
}

This is the equivalent of the delegate signature used when registering for callbacks via the IDT_Device.setCallbackIP SDK method. Each of the elements can be deserialized back into their associated SDK types.