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.

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.

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.