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.

A1A2A3A4A5 PAY 100 PAID

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

  1. The driver presses Next. The kiosk calls POST /charge on the API.
  2. The API asks Paystack for a one-time virtual bank account (30-minute expiry) and assigns a parking space.
  3. The kiosk scrolls the account number across the LCD. The driver transfers ₦100.
  4. Paystack sends a charge.success event to /webhook. The API marks the order paid.
  5. The API calls POST /paid on the kiosk. The screen shows the space, the buzzer sounds and the gate opens.

Kiosk states

StateWhat happens
startWelcome screen. Waits for Next.
chargeRequests a billing profile from the API.
display_azaShows the account number. Next re-checks payment, Back cancels.
thanksShows 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.

FunctionGPIO
Next button13
Back button14
Buzzer26
Servo signal4
LCD SDA / SCL21 / 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

EndpointDescription
GET/Health check
POST/chargeCreate a payment session and assign a space
GET/paid/:referenceCheck whether a payment arrived
POST/webhookPaystack 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

EndpointDescription
GET/statusDevice status and uptime in seconds
POST/paidTell 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"}'