Integrate with Elro Pay
Elro Pay is a shared Paystack-backed payments microservice. Your backend registers once as a client app, gets a credential, and every payment it creates is scoped to that credential — you never see another app's merchants or transactions, and no one sees yours.
This reference covers the three endpoints you'll actually call: initiate a payment, check its status, and list your history.
Authentication
Every request carries your credential in a dedicated header — not Authorization, so it never collides with any bearer-token scheme.
Your key is issued once, out-of-band, by Elro Pay ops — it's shown to you exactly one time and can't be retrieved again. If it leaks, ask ops to revoke it; revoking your key never affects any other app's key.
Keep it server-side only. It authenticates your backend, not your end users — never ship it to a browser or mobile app.
X-Elro-App-Key: epk_live_3f9a1c2d5e6b7a80k7Hs9vQ2pXyZAbCdEfGhIjKlMnOpQrStUvWx
Base URL
| Environment | URL |
|---|---|
| Production | https://pay.elropay.com/api/v1 |
| Local / dev | http://localhost:8010/api/v1 |
All paths below are relative to this base.
Errors
Errors are a JSON object with a detail string, plus the status codes below. A few are deliberate: a merchant id that belongs to another app returns the same 404 as one that doesn't exist at all — Elro Pay never confirms what it can't show you.
| Status | Meaning |
|---|---|
| 201 | Payment initiated. |
| 200 | Request succeeded. |
| 400 | Validation failed, or the merchant has no payout account configured yet. |
| 403 | Missing, malformed, revoked, or invalid credential. |
| 404 | Not found — including a valid id that belongs to a different app. |
| 502 | Paystack itself is unreachable or returned an error. |
Starts a Paystack charge against one of your merchants. Redirect your customer to the returned authorization_url to complete payment.
Body parameters
| Field | Type | Description | |
|---|---|---|---|
| merchant | uuid | required | Id of the merchant being paid. Must belong to your app. |
| amount | string | required | Charge amount in GHS, e.g. "150.00". Not pesewas. |
| customer_email | string | required | Your customer's email — passed straight to Paystack. |
| user_reference | string | optional | Your own internal user/order id. Stored verbatim, echoed back on status checks and callbacks. |
| metadata | object | optional | Freeform, forwarded to Paystack's own transaction metadata. |
curl -X POST https://pay.elropay.com/api/v1/apps/payments/initiate/ \ -H "X-Elro-App-Key: epk_live_..." \ -H "Content-Type: application/json" \ -d '{ "merchant": "b6a9b3d0-1234-4a1b-9c2e-8f6a2d5c9e10", "amount": "150.00", "customer_email": "tenant@example.com", "user_reference": "order-4821", "metadata": {"lease_id": "L-2026-0192"} }'
{
"success": true,
"authorization_url": "https://checkout.paystack.com/abc123xyz",
"reference": "PAY-9F3C7A21B8D4",
"access_code": "0peioxfhpn"
}
Read-only lookup of a payment by its reference — no live Paystack call, just what Elro Pay has recorded so far.
A reference that belongs to another app returns 404, same as one that doesn't exist.
curl https://pay.elropay.com/api/v1/apps/payments/status/PAY-9F3C7A21B8D4/ \
-H "X-Elro-App-Key: epk_live_..."
{
"reference": "PAY-9F3C7A21B8D4",
"user_reference": "order-4821",
"amount": "150.00",
"currency": "GHS",
"status": "PENDING",
"customer_email": "tenant@example.com",
"authorization_url": "https://checkout.paystack.com/abc123xyz",
"paid_at": null,
"created_at": "2026-07-29T09:12:03.745Z"
}
Paginated history — always scoped to your app, regardless of what you pass. Filters can only narrow the results, never widen them past your own data.
Query parameters
| Param | Type | Description | |
|---|---|---|---|
| status | string | optional | One of the payment statuses — see reference below. |
| user_reference | string | optional | Filter to a single order/user id you supplied at initiate time. |
| page | integer | optional | Default 1. |
| page_size | integer | optional | Default 25, max 100. |
curl "https://pay.elropay.com/api/v1/apps/payments/?status=SUCCESS&user_reference=order-4821" \ -H "X-Elro-App-Key: epk_live_..."
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"reference": "PAY-9F3C7A21B8D4",
"user_reference": "order-4821",
"amount": "150.00",
"currency": "GHS",
"status": "SUCCESS",
"customer_email": "tenant@example.com",
"authorization_url": "https://checkout.paystack.com/abc123xyz",
"paid_at": "2026-07-29T09:14:51.002Z",
"created_at": "2026-07-29T09:12:03.745Z"
}
]
}
Receiving updates
You don't poll for status changes — Elro Pay pushes them. When a payment you initiated succeeds or fails, Elro Pay sends a signed POST to your callback URL. Nothing is ever broadcast to any other app.
The payload is deliberately minimal — reference, status, amount, currency, and your own user_reference. Nothing about the merchant or Elro Pay's internals is included.
callback_url and signing secret are set up by Elro Pay ops when your app is registered — there's no self-serve endpoint for this yet. Send your callback URL to ops to get it wired up.
POST https://your-backend.example.com/webhooks/elro Content-Type: application/json X-Elro-Signature: 91a21683b94af8dbea332f0505ce4f5ed9ac55cd366cbbbcacf0e3beea862f2 {"reference":"PAY-9F3C7A21B8D4","status":"SUCCESS","amount":"150.00","currency":"GHS","user_reference":"order-4821"}
Verifying signatures
Always verify X-Elro-Signature before trusting a callback. It's an HMAC-SHA256 of the raw request body, keyed with your app's signing secret.
Use a constant-time comparison (timingSafeEqual / hmac.compare_digest) — never === or ==.
const crypto = require('crypto'); function isValidElroSignature(rawBody, signatureHeader, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader) ); }
import hmac, hashlib def is_valid_elro_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header)
Payment statuses
| Status | Meaning |
|---|---|
| PENDING | Created, awaiting the customer to complete checkout. |
| SUCCESS | Paystack confirmed the charge. |
| FAILED | The charge was declined or errored. |
| ABANDONED | The customer left checkout without completing it. |
| EXPIRED | The checkout session timed out. |
Rate limits
Today, throttling is applied per source IP, not yet per app credential — if you're behind a shared NAT/proxy with high volume, be aware limits are currently coarser than "per app." Per-credential throttling is on the roadmap; ask ops if your integration needs it sooner.