A practical Node.js + Express project that simulates how real-world systems integrate with payment providers.
This service allows you to:
- create payment transactions,
- mark them as
PENDING, - simulate asynchronous gateway callbacks,
- verify callback authenticity with HMAC signatures,
- and finalize transactions as
SUCCESSorFAILED.
It is useful for learning and testing payment integration patterns such as webhook handling, signature validation, idempotency checks, and status transitions.
- Overview
- Features
- Tech Stack
- Project Structure
- How It Works
- Requirements
- Getting Started
- Environment Variables
- Database Setup
- API Reference
- Testing
- Security Notes
- Security Recommendations
- Roadmap Ideas
- Author
In many payment integrations, your application sends a payment request to a provider and later receives an asynchronous callback (webhook) with the final payment result.
This project reproduces that flow locally:
- Client submits payment data.
- Server creates a
PENDINGtransaction. - A simulated gateway sends a delayed callback to
/webhook. - Server verifies signature and timestamp.
- Transaction is updated to
SUCCESSorFAILED.
- REST endpoints for payment creation and webhook processing.
- EJS payment form for quick manual testing.
- PostgreSQL persistence for transactions.
- Simulated payment gateway callback with random delay and random final status.
- HMAC SHA-256 signature verification for webhook authenticity.
- Replay protection via callback age limit.
- Rate limiting on payment and webhook endpoints.
- Unit tests for key validation and webhook behavior.
- Node.js (CommonJS)
- Express 5
- express-rate-limit
- PostgreSQL (
pg) - EJS
- Axios
- Node built-in test runner (
node:test)
.
|-- config/
| `-- database.js
|-- controllers/
| |-- logics.js
| `-- simulation.js
|-- middleware/
| `-- ratelimiter.js
|-- routes/
| `-- urls.js
|-- tests/
| `-- logics.test.js
|-- views/
| `-- home.ejs
|-- server.js
`-- package.json
flowchart LR
A[Client or UI] -->|POST /payments| B[Application Server]
B -->|PENDING| C[(PostgreSQL)]
B --> D[Simulated Gateway]
D -->|POST /webhook| B
B -->|verify signature + timestamp| C
C --> E[Final status SUCCESS or FAILED]
- Node.js 18+ (recommended)
- PostgreSQL 13+ (recommended)
-
Install dependencies:
npm install
-
Configure environment variables in
.env(see section below). -
Create the database and transactions table.
-
Start the server:
npm start
-
Open:
http://localhost:3000/paymentsfor the form UI- or call the API directly
For development with auto-reload:
npm run devCreate a .env file in the project root with:
DB_USER=postgres
DB_HOST=localhost
DB_NAME=payments
DB_PORT=5432
DB_PASSWORD=your_password
PORT=3000
WEBHOOK_URL=http://localhost:3000/webhook
WEBHOOK_SECRET=replace_with_strong_secret
WEBHOOKMAXAGE=300000Variable notes:
WEBHOOK_SECRET: shared secret used to sign/verify callbacks.WEBHOOKMAXAGE: maximum acceptable callback age in milliseconds.
Example PostgreSQL setup:
CREATE DATABASE payments;
\c payments;
CREATE TABLE IF NOT EXISTS transactions (
id BIGSERIAL PRIMARY KEY,
phone VARCHAR(20) NOT NULL,
amount NUMERIC(12,2) NOT NULL CHECK (amount > 0),
reference VARCHAR(64) UNIQUE NOT NULL,
status VARCHAR(16) NOT NULL CHECK (status IN ('PENDING', 'SUCCESS', 'FAILED')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);Note: the application logic requires at least phone, amount, reference, and status columns in transactions.
Base URL: http://localhost:3000
Health/welcome route.
Response:
Welcome to the payment API
Returns the EJS payment form page.
This route is rate limited to 5 requests per minute per IP address.
Creates a transaction and triggers simulated callback flow.
This route is rate limited to 5 requests per minute per IP address.
Request body:
{
"phone": "0712345678",
"amount": 2500
}Validation rules:
phonemust be exactly 10 digits.amountmust be numeric and greater than 0.
Success response (201):
{
"message": "Transaction created successfully",
"transaction": {
"phone": "0712345678",
"amount": "2500.00",
"reference": "ORD1713262000000",
"status": "PENDING"
}
}Receives gateway callback and updates transaction state.
This route is rate limited to 100 requests per minute per IP address.
Request body:
{
"reference": "ORD1713262000000",
"status": "SUCCESS",
"timestamp": 1713262005000,
"signature": "<hmac_sha256_hex>"
}Signature formula used by simulator and verifier:
HMAC_SHA256(secret, reference + status + timestamp + secret)
Possible responses:
200: updated successfully200: already finalized (idempotent behavior)400: invalid input/signature/stale callback404: transaction not found429: too many requests, please try again later
Run tests:
npm testCurrent automated tests cover:
- invalid phone rejection in payment creation,
- stale webhook callback rejection,
- rollback behavior when transaction is already finalized.
- Webhook authenticity is enforced via HMAC signature verification.
- Replay attacks are reduced by timestamp freshness checks.
- Route-level rate limiting helps reduce abuse and unnecessary callback spikes.
- Finalized transactions are protected from duplicate updates.
The project already includes basic protections for a simulated payment flow. For a more production-ready setup, apply the following recommendations:
- Store all secrets in environment variables or a secrets manager; never hard-code them in source code.
- Use a strong
WEBHOOK_SECRETwith at least 32 characters and rotate it regularly. - Keep
WEBHOOKMAXAGEshort enough to reduce replay risk while still allowing normal callback delivery. - Serve the application over HTTPS in any non-local environment.
- Keep rate limiting enabled on all public endpoints, especially payment and webhook routes.
- Validate all request fields strictly before any database operation or external call.
- Use database least-privilege credentials so the application can only access the tables it needs.
- Log security-relevant events such as invalid signatures, stale callbacks, and excessive request bursts.
- Do not expose
.envfiles, database credentials, or webhook secrets in commits or logs. - Review webhook IP allowlisting or provider-side verification if the project is extended to a real gateway.
- Add migration support (e.g., Knex/Prisma/Flyway).
- Add callback delivery retry and dead-letter logging.
- Add transaction query endpoint (
GET /payments/:reference). - Add structured logging and observability metrics.
- Add Docker and CI workflow.
Gwamaka A Mwakabuta - Mlue Technology