Demo only production

Payment Simulator API

Fake M-Pesa, PesaLink & bank transfer API for local dev, demos, and webhook testing. Not for real money.

Authentication required.

Generate your own API key — no signup, no env vars:

Or call POST https://nobody-needs-this.vercel.app/api/keys with an empty JSON body.

Send your key on every request (except public routes):

Revoke: DELETE /api/keys with your key. Admin routes need ADMIN_API_KEY in production.

Base URL: https://nobody-needs-this.vercel.app
All requests use Content-Type: application/json unless noted.

Endpoints

GET Health check

/api/health

Public

Returns service status. No authentication required.

Example response

{ "status": "healthy", "service": "payment-simulator", "environment": "production", "auth_required": true, "registration_enabled": true, "api_key_store": "redis" }

POST Create API key

/api/keys

Public

Generate a personal API key (shown once). No auth required when ENABLE_API_KEY_REGISTRATION is true. Rate-limited per IP.

curl -X POST "https://nobody-needs-this.vercel.app/api/keys" \
  -H "Content-Type: application/json" \
  -d '{}'

Example response

{ "success": true, "api_key": "ps_live_…", "message": "Save this key now…" }

DELETE Revoke API key

/api/keys

API key required

Revokes the API key sent on this request. Bootstrap/env keys cannot be revoked via this endpoint.

Example response

{ "success": true, "message": "API key revoked" }

POST M-Pesa STK Push

/api/payments/mpesa/stk-push

API key required

Initiate a fake M-Pesa STK push. With auto_complete (default true), a callback is simulated after ~2 seconds.

FieldNotes
phone_number yes 254XXXXXXXXX
amount yes Number
account_reference no Default: TEST
description no Default: Payment
callback_url no Webhook URL when payment completes
auto_complete no true = simulate callback after ~2s (default true)
force_success no true / false / omit for random (~95% success)
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/mpesa/stk-push" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"254712345678","amount":1000,"account_reference":"ORDER-123","description":"Payment","callback_url":"https://your-app.com/webhooks/mpesa","auto_complete":true,"force_success":true}'

POST M-Pesa callback (manual)

/api/payments/mpesa/callback

API key required

Manually trigger the M-Pesa STK callback for a transaction. Use when auto_complete is false.

FieldNotes
transaction_id yes From stk-push response
force_success no true = success (0), false = cancelled (1032)
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/mpesa/callback" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id":"MPXABC123","force_success":true}'

POST Bank transfer

/api/payments/bank-transfer

API key required

Initiate a fake bank transfer. Auto-completes after ~3 seconds when auto_complete is true.

FieldNotes
account_number yes Beneficiary account
bank_code yes Bank identifier
amount yes Number
reference no Default: TEST
narration no Default: Payment
callback_url no Webhook URL on completion
auto_complete no Default true
force_success no true / false / random
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/bank-transfer" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account_number":"1234567890","bank_code":"01","amount":5000,"reference":"INV-123","narration":"Supplier payment","callback_url":"https://your-app.com/webhooks/bank","auto_complete":true,"force_success":true}'

POST Bank transfer complete (manual)

/api/payments/bank-transfer/complete

API key required

Manually complete a bank transfer when auto_complete is false.

FieldNotes
transaction_id yes From bank-transfer response
force_success no true = completed, false = failed
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/bank-transfer/complete" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id":"BNKABC123","force_success":true}'

POST PesaLink name inquiry

/api/payments/pesalink/name-inquiry

API key required

Resolve a beneficiary before sending. STA: bank_code + account_number. STP: phone_number. Deterministic fake names (same inputs always return the same name). Sentinels: account ending 00 → 14 not found (HTTP 404); phone with even last digit → not linked (HTTP 404).

FieldNotes
bank_code STA Illustrative sort code, e.g. 68 = Equity
account_number STA Ends in 00 → not found
phone_number STP Even last digit → not linked
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/pesalink/name-inquiry" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bank_code":"68","account_number":"0123456789"}'

Example response

{ "success": true, "found": true, "response_code": "00", "bank_code": "68", "bank_name": "Equity Bank Kenya", "account_number": "0123456789", "account_name": "WANJIKU MWANGI" }

POST PesaLink send

/api/payments/pesalink/send

API key required

Initiate a fake IPSL credit transfer. type=account (STA) needs bank_code+account_number; type=phone (STP) needs phone_number. Amounts outside KES 10–999,999 return 422 with code 61. Retries with the same Idempotency-Key header (or idempotency_key / a non-default reference) return the original PSL… id; a different payload with that key returns 409 / 94. auto_complete (default true) fires a callback after ~2s — unreliable on Vercel; use POST /complete there. force_outcome: success | insufficient_funds | issuer_unavailable | invalid_account | duplicate.

FieldNotes
type no account (STA, default) or phone (STP)
bank_code STA Required for type=account
account_number STA Required for type=account
phone_number STP Required for type=phone; odd last digit = linked
amount yes KES 10–999,999 or 422 / code 61
reference no Default: TEST (not used for dedupe). Any other value keys retries
idempotency_key no Or Idempotency-Key header. Same key + same payload → original PSL…; mismatch → 409 / 94
narration no Default: Payment
callback_url no Webhook URL on completion
auto_complete no Default true (~2s). On Vercel use /complete instead
force_outcome no success | insufficient_funds | issuer_unavailable | invalid_account | duplicate
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/pesalink/send" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"account","bank_code":"68","account_number":"0123456789","amount":500,"reference":"INV-123","narration":"Payment","idempotency_key":"order-123-send","callback_url":"https://your-app.com/webhooks/pesalink","auto_complete":true,"force_outcome":"success"}'

POST PesaLink complete (manual)

/api/payments/pesalink/complete

API key required

Manually complete a PesaLink transfer. This is the reliable path on Vercel — background threads die after the HTTP response, so do not depend on auto_complete there. Returns ISO 8583-style codes (00 success + rrn, 51, 91 reversed, 14, 94).

FieldNotes
transaction_id yes From /send response
force_outcome no Same values as /send
curl -X POST "https://nobody-needs-this.vercel.app/api/payments/pesalink/complete" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id":"PSLABC123","force_outcome":"success"}'

GET PesaLink banks

/api/payments/pesalink/banks

API key required

Illustrative Kenyan sort codes used by this fake switch. Not the official IPSL participant list.

Example response

{ "success": true, "note": "Illustrative sort codes, not the official IPSL participant list.", "banks": [{ "code": "01", "name": "KCB Bank Kenya" }, { "code": "68", "name": "Equity Bank Kenya" }] }

GET Transaction status

/api/payments/:transaction_id

API key required

Get the current state of a single transaction.

FieldNotes
transaction_id yes Path parameter, e.g. MPX…, BNK…, or PSL…

GET List transactions

/api/payments

API key required

List in-memory transactions. Optional query filters.

FieldNotes
status query pending | processing | completed | failed | cancelled
method query mpesa | bank_transfer | pesalink

POST Reset all transactions

/api/payments/reset

Admin key required in production

Clears all stored transactions. In production requires ADMIN_API_KEY.

curl -X POST "https://nobody-needs-this.vercel.app/api/payments/reset" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Webhooks

Include callback_url when starting a payment. When the flow completes, the simulator POSTs JSON to that URL (same shape as real providers).

For local testing, run ruby webhook_receiver.rb on port 4567 and use:

On serverless hosts (e.g. Vercel), prefer auto_complete: false and call the manual callback endpoints, or use a public inbox like webhook.site.

M-Pesa result codes

CodeMeaning
0Success
1032User cancelled
1037Timeout (no PIN)
2001Wrong PIN
1Insufficient balance

ISO 8583-shaped, fake. force_outcome on /send and /complete: success, insufficient_funds, issuer_unavailable, invalid_account, duplicate.

CodeMeaning
00Approved (sets an rrn)
14Invalid / not found account (or phone not linked)
51Insufficient funds
61Exceeds PesaLink amount limit (HTTP 422 on /send)
91Receiving bank timeout — reversed: true
94Duplicate transmission