Controlling xGate Cashless Payment Device with APIs and ESP32

Protonest IoT
10 min readAug 12, 2024

--

Introduction

In today’s world, cashless payment systems are becoming the norm, especially at events where speed, security, and convenience are paramount. The Xgate cashless payment device is an excellent example of how technology can streamline payment processes, making it easier for event organizers and attendees alike. This guide will walk you through the process of setting up and controlling the Xgate system using APIs, testing them with Postman, and automating the entire process using an ESP32 microcontroller.

We’ll dive deep into the features of the provided code, explaining each part in detail so you can confidently implement and customize the system for your specific needs.

Understanding Cashless Payment Systems

Before diving into the code, let’s take a moment to understand the basic concepts of cashless payment systems. At events, these systems allow attendees to make purchases without the need for physical cash, using RFID technology to track transactions. Attendees preload their accounts before the event and use these accounts to pay for goods and services on-site.

Xgate offers two primary types of systems:

  • Closed-loop systems: Only the event’s RFID media can be used for payments.
  • Open-loop systems: In addition to RFID media, other payment methods like credit or debit cards are accepted.

This flexibility allows event organizers to choose the best system based on their needs. In this article, we will explain how the closed-loop system can be implemented.

Setting Up Postman for API Testing

Before integrating the Xgate system with ESP32, it’s crucial to test the APIs using Postman. Postman is a user-friendly tool that allows you to make HTTP requests and view responses, making it perfect for testing APIs.

  1. Install Postman
  • If you don’t have Postman installed, download and install it from (https://www.postman.com/downloads/).

2. Create a New Request

  • Open Postman and create a new request by clicking the “+” button.

3. Set the Request Type

  • Select the type of request you want to make (e.g., GET, POST). For Xgate, most interactions will be POST requests (sending data) or GET requests (retrieving data).

4. Enter the URL

  • Use the API URL provided in the documentation. For example, to display a message with a button,
https://<IP>:9443/api/display/v5/<RANDOM_UUID>

5. Set Headers

  • Add the required headers:
Authorization: Basic <TOKEN>
Content-Type: application/json

6. Add the Body (for POST requests)

  • For a POST request, include the JSON payload. For example, to display a purchase button:
   {
"message": "Click the button to purchase 1x Photo",
"buttons": [
{
"title": "Purchase",
"id": "activateButton",
"style": "Neutral"
}
]
}

7. Send the Request

  • Click “Send” and observe the response. A successful response indicates that the API is functioning correctly.

Automating the Process with ESP32

Now that you’ve tested the APIs, it’s time to automate the process using an ESP32. The ESP32 microcontroller, with its built-in Wi-Fi, is ideal for connecting to the internet and handling API requests. Let’s break down the provided code and explain how each feature works.

Setting Up Wi-Fi Connection

The first step is to connect your ESP32 to a Wi-Fi network. This is essential for sending API requests over the internet.

const char* ssid = "Your_SSID";
const char* password = "Your_PASSWORD";

void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println(" Connected!");
}
  • ssid and password: Replace these with your Wi-Fi network credentials.
  • WiFi.begin: This function initiates the connection to the Wi-Fi network.
  • Serial.println: Provides feedback in the Serial Monitor about the connection status.

Sending POST Requests

The code uses POST requests to interact with the Xgate APIs, such as displaying text and buttons on the device.

void sendPostRequest() {
WiFiClientSecure client;
client.setInsecure(); // Disables SSL certificate verification

HTTPClient http;
String url = String("https://") + server + ":" + port + "/api/display/v5/" + generateUuid();
http.begin(client, url);
http.addHeader("Authorization", token);
http.addHeader("Content-Type", "application/json");

String payload = "{\"message\": \"Click the button to purchase 1x Photo\", \"buttons\": [{\"title\": \"Purchase\", \"id\": \"activateButton\", \"style\": \"Neutral\"}]}";
int httpResponseCode = http.POST(payload);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println(response);
} else {
Serial.println("Error on sending POST");
}
http.end();
}
  • WiFiClientSecure: This object handles the secure connection to the server.
  • client.setInsecure(): Disables SSL certificate verification, which is useful during development. However, for production, ensure that SSL is properly handled.
  • HTTPClient: Manages the HTTP requests and responses.
  • sendPostRequest(): This function constructs and sends a POST request to the specified API endpoint.

Generating UUIDs

UUIDs (Universally Unique Identifiers) are essential for tracking individual transactions or displays. The code includes a function to generate these UUIDs.

String generateUuid() {
String uuid = "";
for (int i = 0; i < 8; i++) uuid += String(random(0, 16), HEX);
uuid += "-";
for (int i = 0; i < 4; i++) uuid += String(random(0, 16), HEX);
uuid += "-4"; // UUID version 4
for (int i = 0; i < 3; i++) uuid += String(random(0, 16), HEX);
uuid += "-";
uuid += String(random(8, 12), HEX); // UUID variant
for (int i = 0; i < 3; i++) uuid += String(random(0, 16), HEX);
uuid += "-";
for (int i = 0; i < 12; i++) uuid += String(random(0, 16), HEX);
return uuid;
}
  • generateUuid(): This function generates a random UUID, which is then used to uniquely identify requests and transactions.

Checking Button Status

The code periodically checks if the button on the Xgate device has been pressed, triggering the next steps in the purchase process.

bool checkButtonStatus() {
WiFiClientSecure client;
client.setInsecure();

HTTPClient http;
String url = String("https://") + server + ":" + port + "/api/status/v5/display/" + displayUuid;
http.begin(client, url);
http.addHeader("Authorization", token);

int httpResponseCode = http.GET();
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println(response);

// Deserialize and check the button status here

return true; // Replace with actual logic to check if the button was pressed
} else {
Serial.println("Error on sending GET");
return false;
}
http.end();
}
  • checkButtonStatus(): This function sends a GET request to check the status of the button. The response is analyzed to determine whether the button was pressed.

Creating and Managing Purchases

Once the button is pressed, the system initiates a purchase. The code handles this by sending a POST request with the purchase details.


bool createPurchase() {
purchaseUuid = generateUuid();
String url = String("https://") + server + ":" + port + "/api/purchase/v5/" + purchaseUuid;
String payload = "{\"paymentType\": \"cashless\", \"cartItems\": [{\"count\": 1, \"productKey\": \"photobox\", \"variantKey\": \"standard\", \"name\": \"Photo\", \"priceInCredits\": 4, \"accountingCategoryKey\": \"common.service\", \"taxRate\": 0.19}], \"allowUserCancel\": false}";

bool success = sendPostRequest(url, payload);
if (success) {
cancellationUrl = payload; // Update with actual cancellationUrl from response
}
return success;
}
  • createPurchase(): This function sends a POST request to create a new purchase. The purchase details, such as item name, price, and tax rate, are included in the payload.

Canceling Purchases

In case the purchase process needs to be canceled (e.g., if the button wasn’t pressed within a certain time frame), the code provides a function to handle this.

bool cancelPurchase() {
String url = String

("https://") + server + ":" + port + cancellationUrl;
return sendPostRequest(url, "");
}
  • cancelPurchase(): Sends a POST request to cancel the purchase using the provided cancellation URL.

Sending Pulses to External Devices

After a successful purchase, the ESP32 sends a pulse to an external device, like a photo booth system, indicating that the transaction is complete.

void sendPulse() {
digitalWrite(pulsePin, HIGH);
delay(100); // Pulse duration
digitalWrite(pulsePin, LOW);
Serial.println("Pulse sent to Fellman board");
}
  • sendPulse(): This function sends a short electrical pulse to an external device to trigger an action, such as starting a photo booth.

Main Loop and State Management

The main loop in the code manages the state transitions, ensuring that the system responds appropriately to each step in the process.

void loop() {
static unsigned long stateStartTime = millis();

switch (state) {
case DISPLAY_TEXT:
state = CHECK_BUTTON;
break;

case CHECK_BUTTON:
if (millis() - stateStartTime >= requestInterval) {
if (checkButtonStatus()) {
state = CREATE_PURCHASE;
stateStartTime = millis();
}
}
break;

case CREATE_PURCHASE:
if (attemptWithRetry(createPurchase)) {
state = CHECK_PURCHASE;
stateStartTime = millis();
}
break;

case CHECK_PURCHASE:
if (millis() - stateStartTime >= requestInterval) {
if (checkPurchaseStatus()) {
sendPulse();
displayThankYou();
state = DISPLAY_THANKS;
stateStartTime = millis();
} else if (millis() - stateStartTime >= purchaseTimeout) {
if (attemptWithRetry(cancelPurchase)) {
displayTextAndButton();
state = CHECK_BUTTON;
stateStartTime = millis();
}
}
}
break;

case DISPLAY_THANKS:
if (millis() - stateStartTime >= thanksDisplayTime) {
displayTextAndButton();
state = CHECK_BUTTON;
stateStartTime = millis();
}
break;
}
}
  • loop(): The core of the ESP32 program. This loop continuously checks the state and performs the appropriate actions (e.g., checking button status, creating a purchase, canceling a purchase, etc.).
  • State Management: The program uses a state machine approach to handle the various stages of the payment process, ensuring smooth transitions between actions.

Final Code

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

// Replace with your network credentials
const char* ssid = "YOUR SSID";
const char* password = "YOUR PASSWORD";

const char* server = "THE IP THAT THE XGATE IS CONNECTED";
const int port = 9443;
const char* token = "Basic <ADD THE TOKEN>"; // Replace with your authorization token

const int pulsePin = 25; // GPIO pin to send pulse

String displayUuid;
String purchaseUuid;
String cancellationUrl;

enum State {DISPLAY_TEXT, CHECK_BUTTON, CREATE_PURCHASE, CHECK_PURCHASE, DISPLAY_THANKS};
State state = DISPLAY_TEXT;

unsigned long lastRequestTime = 0;
const unsigned long requestInterval = 2000; // 2 seconds
const unsigned long purchaseTimeout = 20000; // 20 seconds
const unsigned long thanksDisplayTime = 15000; // 15 seconds

template <typename Function>
bool attemptWithRetry(Function func, int maxRetries = 3, unsigned long retryDelay = 5000) {
int attempts = 0;
while (attempts < maxRetries) {
if (func()) {
return true;
}
attempts++;
delay(retryDelay);
}
return false;
}

void setup() {
Serial.begin(115200);
delay(1000);

// Set up the pulse pin
pinMode(pulsePin, OUTPUT);
digitalWrite(pulsePin, LOW);

// Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println(" Connected!");

// Start by displaying the text and button
displayTextAndButton();
}

void loop() {
static unsigned long stateStartTime = millis();

switch (state) {
case DISPLAY_TEXT:
// No action needed in loop for display, it is handled in setup
state = CHECK_BUTTON;
break;

case CHECK_BUTTON:
if (millis() - stateStartTime >= requestInterval) {
if (checkButtonStatus()) {
state = CREATE_PURCHASE;
stateStartTime = millis();
}
}
break;

case CREATE_PURCHASE:
if (attemptWithRetry(createPurchase)) {
state = CHECK_PURCHASE;
stateStartTime = millis();
}
break;

case CHECK_PURCHASE:
if (millis() - stateStartTime >= requestInterval) {
if (checkPurchaseStatus()) {
sendPulse();
displayThankYou();
state = DISPLAY_THANKS;
stateStartTime = millis();
} else if (millis() - stateStartTime >= purchaseTimeout) {
if (attemptWithRetry(cancelPurchase)) {
displayTextAndButton();
state = CHECK_BUTTON;
stateStartTime = millis();
}
}
}
break;

case DISPLAY_THANKS:
if (millis() - stateStartTime >= thanksDisplayTime) {
displayTextAndButton();
state = CHECK_BUTTON;
stateStartTime = millis();
}
break;
}
}

String generateUuid() {
String uuid = "";
for (int i = 0; i < 8; i++) uuid += String(random(0, 16), HEX);
uuid += "-";
for (int i = 0; i < 4; i++) uuid += String(random(0, 16), HEX);
uuid += "-4"; // set version to 4 (randomly generated UUID)
for (int i = 0; i < 3; i++) uuid += String(random(0, 16), HEX);
uuid += "-";
uuid += String(random(8, 12), HEX); // set variant to DCE 1.1
for (int i = 0; i < 3; i++) uuid += String(random(0, 16), HEX);
uuid += "-";
for (int i = 0; i < 12; i++) uuid += String(random(0, 16), HEX);
return uuid;
}

void displayTextAndButton() {
displayUuid = generateUuid();
String url = String("https://") + server + ":" + port + "/api/display/v5/" + displayUuid;
String payload = "{\"message\": \"Click the button to buy your photos for €4\", \"buttons\": [{\"title\": \"Buy\", \"id\": \"activateButton\", \"style\": \"Neutral\"}]}";

bool success = false;
while (!success) {
success = attemptWithRetry([=](){ return sendPostRequest(url, payload); });
if (!success) {
Serial.println("Failed to send display text, retrying...");
delay(5000); // Adjust the delay as needed for retry interval
}
}
}

void displayThankYou() {
displayUuid = generateUuid();
String url = String("https://") + server + ":" + port + "/api/display/v5/" + displayUuid;
String payload = "{\"message\": \"Thank you\"}";

attemptWithRetry([=](){ return sendPostRequest(url, payload); });
}

bool checkButtonStatus() {
String url = String("https://") + server + ":" + port + "/api/status/v5/display/" + displayUuid;
return sendGetRequest(url, "Success");
}

bool createPurchase() {
purchaseUuid = generateUuid();
String url = String("https://") + server + ":" + port + "/api/purchase/v5/" + purchaseUuid;
String payload = "{\"paymentType\": \"cashless\", \"cartItems\": [{\"count\": 1, \"productKey\": \"photobox\", \"variantKey\": \"standard\", \"name\": \"Photo\", \"priceInCredits\": 4, \"accountingCategoryKey\": \"common.service\", \"taxRate\": 0.19}], \"allowUserCancel\": false}";

bool success = sendPostRequest(url, payload);
if (success) {
cancellationUrl = payload; // This should be updated with actual cancellationUrl from response
}
return success;
}

bool checkPurchaseStatus() {
String url = String("https://") + server + ":" + port + "/api/status/v5/" + purchaseUuid;
return sendGetRequest(url, "Success");
}

bool cancelPurchase() {
String url = String("https://") + server + ":" + port + cancellationUrl;
return sendPostRequest(url, "");
}

void sendPulse() {
digitalWrite(pulsePin, HIGH);
delay(100); // Pulse duration in milliseconds
digitalWrite(pulsePin, LOW);
Serial.println("Pulse sent to Fellman board");
}

bool sendPostRequest(const String& url, const String& payload) {
WiFiClientSecure client;
client.setInsecure(); // Use with caution: disables SSL certificate verification

HTTPClient http;
http.begin(client, url);
http.addHeader("Authorization", token);
http.addHeader("Content-Type", "application/json");

int httpResponseCode = http.POST(payload);
delay(1500);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("Response:");
Serial.println(response);
http.end();
return true;
} else {
Serial.print("Error on sending POST: ");
Serial.println(httpResponseCode);
http.end();
return false;
}
}

bool sendGetRequest(const String& url, const String& expectedStatus) {
WiFiClientSecure client;
client.setInsecure(); // Use with caution: disables SSL certificate verification

HTTPClient http;
http.begin(client, url);
http.addHeader("Authorization", token);

int httpResponseCode = http.GET();
delay(1500);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("Response:");
Serial.println(response);

StaticJsonDocument<1024> doc;
deserializeJson(doc, response);
const char* status = doc["data"]["status"];
if (String(status) == expectedStatus) {
http.end();
return true;
}
} else {
Serial.print("Error on sending GET: ");
Serial.println(httpResponseCode);
}
http.end();
return false;
}

Testing and Debugging

Once you’ve written your code, it’s time to test it thoroughly. Use the Serial Monitor in the Arduino IDE to check for any issues or errors in the process. Debugging is an essential part of development, and having clear feedback from the system will help you identify and fix problems quickly.

Conclusion

You’ve now walked through the entire process of setting up and controlling the Xgate cashless payment device using APIs, Postman, and an ESP32. By breaking down the code and explaining each part, I hope you have a clear understanding of how the system works and how to customize it for your specific needs.

Remember, this guide is just a starting point. Feel free to tweak the code, experiment with different configurations, and explore additional features. The possibilities are endless, and with the power of automation, your events can become smoother, more secure, and more enjoyable for everyone involved.

Contact us for any consultations or projects related to Custom API programming related to Xgate.

Email: udara@protonest.co

Protonest for more details.

Protonest specializes in transforming IoT ideas into reality. We offer prototyping services from concept to completion. Our commitment ensures that your visionary IoT concepts become tangible, innovative, and advanced prototypes.

Our Website: https://www.protonest.co/

Cheers!

--

--

Protonest IoT
Protonest IoT

Written by Protonest IoT

We make your IoT ideas a reality

Responses (1)