Park, pay by transfer, drive in.
Pay Park is a self-service car-park kiosk. An ESP32 shows a one-time bank account, the driver pays ₦100, and the gate opens as soon as Paystack confirms it.
The kiosk's 16×2 screen, as the firmware scrolls it.
Pay, the gate lifts, and the car parks in its assigned space.
The system has two parts: a Node.js and Express API that talks to Paystack, and ESP32 firmware that runs the LCD, buttons, buzzer and servo gate.
How it works
- The driver presses Next. The kiosk calls
POST /chargeon the API. - The API asks Paystack for a one-time virtual bank account (30-minute expiry) and assigns a parking space.
- The kiosk scrolls the account number across the LCD. The driver transfers ₦100.
- Paystack sends a
charge.successevent to/webhook. The API marks the order paid. - The API calls
POST /paidon the kiosk. The screen shows the space, the buzzer sounds and the gate opens.
Kiosk states
| State | What happens |
|---|---|
start | Welcome screen. Waits for Next. |
charge | Requests a billing profile from the API. |
display_aza | Shows the account number. Next re-checks payment, Back cancels. |
thanks | Shows the space, opens the gate, returns to start. |
Hardware
Board: ESP-WROOM-32 (ESP32 Dev Module), 16×2 I2C LCD at address 0x27, two push buttons wired to ground (internal pull-ups), a buzzer and a servo.
| Function | GPIO |
|---|---|
| Next button | 13 |
| Back button | 14 |
| Buzzer | 26 |
| Servo signal | 4 |
| LCD SDA / SCL | 21 / 22 |
Getting started
You need Node.js 20.6 or later, a Paystack secret key, Arduino IDE 2.x with ESP32 support, and a Cloudflare Tunnel so the API can reach the kiosk.
1. Run the backend
npm install
echo "PAYSTACK_SECRET_KEY=sk_test_xxxxxxxx" > .env
npm test # development: nodemon, loads .env
npm start # production: set variables in your host
The API listens on port 3000. npm start does not load .env, so on Render or similar, set the key in the dashboard.
2. Flash the firmware
Install aWOT, ArduinoJson 7.x, ESP32Servo, LiquidCrystal_I2C and ESP32HTTPClient. In code.ino set:
const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
ESP32HTTPClient client("https://<your-api-host>");
Select ESP32 Dev Module, upload, then open the Serial Monitor at 115200 baud to read the kiosk's IP address.
3. Expose the kiosk
Give the ESP32 a fixed IP. In Cloudflare, create a tunnel with a published application route pointing to http://<ESP32_IP>:80, then set the notify URL in src/functions/webhook.js to https://<your-tunnel-hostname>/paid/.
4. Add the Paystack webhook
In the Paystack dashboard, open Settings → API Keys & Webhooks and set the webhook URL to https://<your-api-host>/webhook.
API reference
Backend
| Endpoint | Description |
|---|---|
GET/ | Health check |
POST/charge | Create a payment session and assign a space |
GET/paid/:reference | Check whether a payment arrived |
POST/webhook | Paystack webhook receiver |
POST /charge → 200
{
"reference": "string",
"account_number": "string",
"bank_name": "string",
"account_name": "string",
"expires_at": "2026-01-01T12:30:00.000Z"
}
GET /paid/<reference> → 200
{ "paid": true, "park_space_u": "A1" }
An unknown reference returns { "paid": false, "park_space_u": null }.
Kiosk
| Endpoint | Description |
|---|---|
GET/status | Device status and uptime in seconds |
POST/paid | Tell the kiosk a payment succeeded |
curl http://<ESP32_IP>/status
curl -X POST http://<ESP32_IP>/paid \
-H "Content-Type: application/json" \
-d '{"park_space":"A1"}'