E
Elro Pay API
v1
Client app integration reference

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.

REST · JSON Header-based auth HMAC-signed callbacks GHS via Paystack

This reference covers the three endpoints you'll actually call: initiate a payment, check its status, and list your history.

How this differs from Paystack's own API: you're not talking to Paystack directly. Elro Pay sits in front of it, holds the Paystack secret key on your behalf, and adds the one thing Paystack doesn't — strict isolation between the multiple apps sharing this service.
POST /subaccounts/create/ staff only

There's no self-serve signup. Elro Pay ops (staff, logged into Django Admin) create one sub-account per merchant/business being onboarded — that single call is also where your API key is born:

  1. Ops resolves the merchant's bank/mobile-money account and creates its Paystack sub-account.
  2. Elro Pay provisions a brand-new client app dedicated to that merchant — this is what isolates it from every other merchant on the platform.
  3. The response includes api_key, shown exactly this once. Ops copies it out and sends it to you (the integrating developer) over a secure channel — Slack DM, a password manager share, not email/plaintext.
This is your one shot at the key. Elro Pay never stores the raw value — only a one-way hash — so if it's lost, ops has to issue a fresh one (which is safe and instant, but the old key still has to be revoked separately if it might have leaked).

The merchant id (a uuid) passed into this call already needs to exist as an Elro Pay Merchant record, created by ops beforehand. If you're the frontend developer reading this: you won't call this endpoint yourself — this is here so you understand where your key comes from and why it's scoped the way it is (one key per merchant, not one shared key for the whole platform).

Response 201
{
  "id": "9c1e2a4d-...-b7f0",
  "merchant": "b6a9b3d0-...-9e10",
  "name": "Ama Boateng",
  "bank_name": "GCB Bank",
  "percentage_charge": "5.00",
  "is_verified": true,

  // the two fields that matter for integration —
  // api_key is never shown again after this response
  "client_app_code": "ama-boateng",
  "api_key": "epk_live_3f9a1c2d5e6b7a80k7Hs9v..."
}

Authentication

Every request carries your credential in a dedicated header — not Authorization, so it never collides with any bearer-token scheme.

Your key comes from the sub-account creation call above, shown exactly one time — it can't be retrieved again after that response. If it leaks, ask ops to revoke it; revoking your key never affects any other merchant's key.

Keep it server-side only. It authenticates your backend, not your end users — never ship it to a browser or mobile app.

Header
X-Elro-App-Key: epk_live_3f9a1c2d5e6b7a80k7Hs9vQ2pXyZAbCdEfGhIjKlMnOpQrStUvWx

Base URL

EnvironmentURL
Productionhttps://pay.elropay.com/api/v1
Local / devhttp://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.

StatusMeaning
201Payment initiated.
200Request succeeded.
400Validation failed, or the merchant has no payout account configured yet.
403Missing, malformed, revoked, or invalid credential.
404Not found — including a valid id that belongs to a different app.
502Paystack itself is unreachable or returned an error.
POST /apps/payments/initiate/

Starts a Paystack charge against one of your merchants. Redirect your customer to the returned authorization_url to complete payment.

Body parameters

FieldTypeDescription
merchantuuidrequiredId of the merchant being paid. Must belong to your app.
amountstringrequiredCharge amount in GHS, e.g. "150.00". Not pesewas.
customer_emailstringrequiredYour customer's email — passed straight to Paystack.
user_referencestringoptionalYour own internal user/order id. Stored verbatim, echoed back on status checks and callbacks.
metadataobjectoptionalFreeform, forwarded to Paystack's own transaction metadata.
Anti-enumeration: pass a merchant id that isn't yours and you get a plain 404, identical to an id that doesn't exist.
Request curl
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"}
  }'
Response 201
{
  "success": true,
  "authorization_url": "https://checkout.paystack.com/abc123xyz",
  "reference": "PAY-9F3C7A21B8D4",
  "access_code": "0peioxfhpn"
}
GET /apps/payments/status/{reference}/

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.

Request curl
curl https://pay.elropay.com/api/v1/apps/payments/status/PAY-9F3C7A21B8D4/ \
  -H "X-Elro-App-Key: epk_live_..."
Response 200
{
  "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"
}
GET /apps/payments/

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

ParamTypeDescription
statusstringoptionalOne of the payment statuses — see reference below.
user_referencestringoptionalFilter to a single order/user id you supplied at initiate time.
pageintegeroptionalDefault 1.
page_sizeintegeroptionalDefault 25, max 100.
Request curl
curl "https://pay.elropay.com/api/v1/apps/payments/?status=SUCCESS&user_reference=order-4821" \
  -H "X-Elro-App-Key: epk_live_..."
Response 200
{
  "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.

Currently ops-configured: your 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.
Incoming request
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 ==.

Node.js
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)
  );
}
Python
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

StatusMeaning
PENDINGCreated, awaiting the customer to complete checkout.
SUCCESSPaystack confirmed the charge.
FAILEDThe charge was declined or errored.
ABANDONEDThe customer left checkout without completing it.
EXPIREDThe 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.