Overview
Every approved merchant gets its own public key, secret key and webhook secret under API & Webhooks in the dashboard (keys appear once your business verification is approved). You use them to:
- initialize a payment and redirect the payer to the hosted checkout, which shows your merchant's logo and the payment channels enabled for your business;
- receive a signed server-to-server webhook when the payment succeeds, fails or is reversed;
- verify the payment before giving value, and pull transaction and settlement reports.
You never talk to Paystack, Flutterwave, Monnify, Remita, Interswitch, Squad, Korapay, OPay or CoralPay directly — the gateway handles the processors, fees, split settlement and reconciliation.
No system to integrate? Use payment links from the portal instead — no code needed.
Quick start
- Sign in to the merchant portal → API & Webhooks. Copy your public key and secret key, and set your callback URL (webhook endpoint) and return URL.
- From your server, call POST /transactions/initialize with the amount, your own unique reference and the payer's details.
- Redirect the payer to the checkout_url in the response.
- Handle the webhook at your callback URL (verify the signature, respond 200).
- Before giving value, call GET /transactions/{reference}/verify and check status = "successful" and the amount.
Authentication
Every API request must include four headers. Requests are only accepted over HTTPS, from whitelisted IPs if your merchant has an IP whitelist, and within 5 minutes of the timestamp.
| Header | Value |
|---|---|
| X-EG-Public-Key | Your public key, e.g. pk_live_… |
| Authorization | Bearer sk_live_… (your secret key) |
| X-EG-Timestamp | Current Unix time in seconds, e.g. 1793622532 |
| X-EG-Signature | hex( HMAC_SHA512( timestamp + "." + raw_body, secret_key ) ) — for GET requests raw_body is an empty string |
| Content-Type | application/json (POST requests) |
Every request needs a fresh timestamp and signature: a signature that has already been used is rejected as a replay (HTTP 401). Sign the exact bytes you send as the body. Keep your secret key on your server only — never in a browser or mobile app. If it leaks, regenerate keys from the portal; old keys stop working immediately.
// PHP
$ts = (string) time();
$body = json_encode($payload);
$sig = hash_hmac('sha512', $ts . '.' . $body, $secretKey);
Payment flow
- Your server → POST /transactions/initialize → receives reference and checkout_url.
- Payer is redirected to checkout_url, sees your logo and amount, chooses a channel (card, transfer, USSD, wallet, Remita or PayDirect at a bank…) and pays.
- The gateway confirms the payment with the processor server-side (never trusting browser parameters).
- The gateway sends a signed transaction.successful webhook to your callback URL and redirects the payer to your return URL.
- You verify and give value. Funds are split and settled to your bank account on your settlement cycle.
PayDirect / Remita: the payer may complete payment hours later at a bank branch. Keep the order open and rely on the webhook (or poll verify) rather than the browser return.
Initialize a transaction
POST https://echogateway.ng/api/v1/transactions/initialize
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | Amount in Naira, e.g. 45000.00. This is what your merchant is owed; any processor fee is added on top when your fee bearer is customer. |
| merchant_reference | string ≤100 | Yes | Your unique reference (invoice/order ID). Re-using it returns the existing transaction (idempotent). |
| customer.name | string | Yes | Payer's full name. |
| customer.email | Yes | Receipt is emailed here. | |
| customer.phone | string | No | Payer's phone number. |
| description | string ≤255 | No | Shown on checkout and receipt, e.g. "Order #10234 – 2 items". |
| processor | string | No | Skip channel selection by pre-selecting a processor code (see processors). |
| callback_url | url | No | Override your default webhook URL for this transaction. |
| return_url | url | No | Where the payer is sent after payment (overrides default). |
| metadata | object | No | Any JSON you want echoed back (matric number, session, fee type…). |
{
"amount": 45000,
"merchant_reference": "INV-2026-000123",
"description": "2026/2027 Tuition - 200 Level",
"customer": { "name": "Amina Yusuf", "email": "amina@example.com", "phone": "08031234567" },
"metadata": { "matric_no": "CSC/2024/123", "session": "2026/2027" }
}
Response 201 Created
{
"status": true,
"message": "Transaction initialized",
"data": {
"reference": "EGW261027K3J9QX2L1M",
"merchant_reference": "INV-2026-000123",
"status": "pending",
"amount": 45000,
"fee": 0,
"fee_bearer": "customer",
"total_amount": 45000,
"currency": "NGN",
"processor": null,
"checkout_url": "https://echogateway.ng/pay/EGW261027K3J9QX2L1M",
"customer": { "name": "Amina Yusuf", "email": "amina@example.com", "phone": "08031234567" },
"metadata": { "matric_no": "CSC/2024/123", "session": "2026/2027" },
"created_at": "2026-10-27T10:15:22+01:00"
}
}
Redirect the payer to data.checkout_url. fee and total_amount are updated once the payer picks a channel.
Verify a transaction
GET https://echogateway.ng/api/v1/transactions/{reference}/verify
Accepts either the gateway reference or your merchant_reference. If still pending, the gateway re-queries the processor before answering. Always verify before giving value, and confirm data.status == "successful" and data.amount matches what you expect.
{
"status": true,
"message": "Transaction successful",
"data": {
"reference": "EGW261027K3J9QX2L1M",
"merchant_reference": "INV-2026-000123",
"status": "successful",
"amount": 45000, "fee": 675, "fee_bearer": "customer", "total_amount": 45675,
"processor": "paystack", "channel": "card", "processor_reference": "4099260516",
"paid_at": "2026-10-27T10:17:03+01:00",
"settlement_status": "unsettled",
...
}
}
Get / list transactions
GET https://echogateway.ng/api/v1/transactions/{reference} — returns the stored record without re-querying.
GET https://echogateway.ng/api/v1/transactions?status=successful&from=2026-10-01&to=2026-10-31&per_page=50&page=1
{ "status": true, "data": [ { ...transaction... } ], "meta": { "page": 1, "per_page": 50, "total": 1284, "last_page": 26 } }
For GET requests with a query string, sign an empty body: HMAC(timestamp + ".", secret).
Payment processors
GET https://echogateway.ng/api/v1/processors — channels currently enabled for your merchant.
| Code | Processor | Notes |
|---|---|---|
| paystack | Paystack | Cards, bank transfer, USSD and bank via Paystack checkout. |
| flutterwave | Flutterwave | Cards, transfer, USSD and more via Flutterwave Standard. |
| remita | Remita | Remita Retrieval Reference (RRR) - pay online or at any bank branch. |
| interswitch | Interswitch Web Checkout | Verve, Mastercard, Visa, transfer and USSD via Interswitch Webpay. |
| paydirect | Interswitch PayDirect | Pay at any bank branch or Quickteller using the payment reference. |
| coralpay | CoralPay Verge | C'Gate USSD, cards, NQR and bank transfer via CoralPay Verge. |
| monnify | Monnify | Cards, bank transfer (dedicated account), USSD and phone number via Monnify by Moniepoint. |
| squad | Squad by GTCO | Cards, bank transfer, USSD and GTBank account payments via Squad. |
| korapay | Korapay | Cards, bank transfer and Pay with Bank via Korapay checkout. |
| echomoney | Bank Transfer (NET MFB) | Pay by transfer from any bank into a one-time NET Microfinance Bank account number (EchoMoney). |
| opay | OPay | OPay wallet, cards, bank transfer and USSD via OPay Cashier. |
Settlements
Successful payments are split (platform commission, partner commission, other beneficiaries, remainder to your account) and batched into settlements on your cycle — Instant, T+0, T+1, weekly or custom.
GET https://echogateway.ng/api/v1/settlements — list settlement batches with destination accounts.
GET https://echogateway.ng/api/v1/settlements/{reference} — one batch, its destination accounts and the transactions it covers.
{
"status": true,
"data": {
"reference": "STL261028AB12CD34", "status": "settled", "cycle": "t1",
"transactions_count": 482, "gross_amount": 21690000, "fees_amount": 0, "net_amount": 21690000,
"settled_at": "2026-10-28T11:02:10+01:00",
"accounts": [
{ "beneficiary": "Ade Fashion Stores", "type": "merchant", "bank": "Guaranty Trust Bank", "account_number": "0123456789", "amount": 20605500, "status": "paid", "payout_reference": "NIP-88210" },
{ "beneficiary": "EchoGateway (platform commission)", "type": "platform", "amount": 1084500, "status": "paid" }
],
"transactions": [ { "reference": "EGW…", "merchant_reference": "INV-…", "amount": 45000, "paid_at": "…" } ]
}
}
Payment links
Merchants without their own system can create links in the portal (Payment Links) with a fixed amount, payer-entered amount, or a list of priced items, plus custom fields to collect (size, delivery address, ticket type…). Share the link; payers fill the form and pay.
You can also create simple links by API:
POST https://echogateway.ng/api/v1/payment-links
{ "title": "Lagos Fashion Week ticket", "amount_type": "fixed", "amount": 25000, "expires_at": "2026-12-31", "redirect_url": "https://shop.example.com/thanks" }
GET https://echogateway.ng/api/v1/payment-links
Webhooks (callbacks)
When a transaction changes status the gateway sends a POST with a JSON body to your callback URL.
| Event | When |
|---|---|
| transaction.successful | Payment confirmed by the processor |
| transaction.failed | Payment declined, failed or amount mismatch |
| transaction.reversed | Payment reversed (e.g. PayDirect reversal) |
| test.ping | Sent from the portal's "Send test webhook" button |
Headers
X-EG-Event: transaction.successful X-EG-Timestamp: 1793112456 X-EG-Signature: hex( HMAC_SHA512( timestamp + "." + raw_body, webhook_secret ) )
Body
{ "event": "transaction.successful", "data": { ...same shape as the verify response... } }
Verifying (PHP)
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_EG_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_EG_SIGNATURE'] ?? '';
$expected = hash_hmac('sha512', $ts . '.' . $raw, getenv('EG_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }
$event = json_decode($raw, true);
// Look up your order by $event['data']['merchant_reference'], call /verify, then give value once.
http_response_code(200);
Respond with any 2xx within 20 seconds. Non-2xx responses and timeouts are retried after 1 min, 5 min, 15 min, 1 h and 2 h (6 attempts). Webhooks can arrive more than once — make your handler idempotent. Delivery history is visible in the portal.
Return URL
After payment the payer sees a gateway receipt with a Return to … button that opens your return URL with:
https://your-portal/return?reference=EGW261027K3J9QX2L1M&merchant_reference=INV-2026-000123&status=successful
These parameters are for display only — always confirm with /verify or the webhook.
Statuses & errors
| Transaction status | Meaning |
|---|---|
| Pending | Created; awaiting payment or confirmation |
| Successful | Paid and confirmed — give value |
| Failed | Declined / failed / amount mismatch |
| Abandoned | Expired without payment |
| Reversed | Reversed after success |
| HTTP | Meaning |
|---|---|
| 200 / 201 | OK / created |
| 401 | Missing/invalid keys, timestamp or signature |
| 403 | Merchant suspended or IP not whitelisted |
| 404 | Transaction / settlement not found |
| 409 | merchant_reference reused with a different amount |
| 422 | Validation failed — see errors |
| 429 | Rate limit (120 requests/minute) |
{ "status": false, "message": "Validation failed", "errors": { "customer.email": ["The customer.email field must be a valid email address."] } }
Settlement status on a transaction: unsettled → batched (in a pending settlement) → settled.
Fees & splits
Each processor has a fee (percentage + flat, optionally capped) shown on the pricing page. Your business's fee bearer decides who pays it:
- Customer — the fee is added to the amount at checkout; you receive the full amount before splits.
- Merchant — the payer pays amount; the fee is deducted before splits.
Split rules (platform commission, partner commission, other beneficiaries) are then applied in order as fixed amounts or percentages, and the remainder goes to your settlement account. Each transaction's split lines are visible in the portal.
Payouts are automatic. When the payer chooses Paystack, Flutterwave, Monnify, Remita or Interswitch, the gateway sends the split to the processor with the payment, and the processor pays each beneficiary account directly on its normal settlement schedule. PayDirect payments settle according to the split configured with Interswitch. CoralPay, Squad, Korapay and OPay have no split settlement, so Echo Systems pays those out to you in settlement batches. The settlement_status and the settlement endpoints show which accounts were paid.
Code samples
<?php
function eg(string $method, string $path, ?array $payload = null): array {
$pk = getenv('EG_PUBLIC_KEY'); $sk = getenv('EG_SECRET_KEY');
$body = $payload ? json_encode($payload) : '';
$ts = (string) time();
$ch = curl_init('https://echogateway.ng/api/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $body ?: null,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json', 'Accept: application/json',
'X-EG-Public-Key: ' . $pk,
'Authorization: Bearer ' . $sk,
'X-EG-Timestamp: ' . $ts,
'X-EG-Signature: ' . hash_hmac('sha512', $ts . '.' . $body, $sk),
],
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
return $res;
}
$init = eg('POST', '/transactions/initialize', [
'amount' => 45000, 'merchant_reference' => 'INV-2026-000123',
'customer' => ['name' => 'Amina Yusuf', 'email' => 'amina@example.com'],
]);
header('Location: ' . $init['data']['checkout_url']);
// later
$v = eg('GET', '/transactions/INV-2026-000123/verify');
if ($v['data']['status'] === 'successful' && $v['data']['amount'] == 45000) { /* give value */ }
const crypto = require('crypto');
async function eg(method, path, payload) {
const body = payload ? JSON.stringify(payload) : '';
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha512', process.env.EG_SECRET_KEY).update(ts + '.' + body).digest('hex');
const res = await fetch('https://echogateway.ng/api/v1' + path, {
method,
headers: {
'Content-Type': 'application/json', Accept: 'application/json',
'X-EG-Public-Key': process.env.EG_PUBLIC_KEY,
Authorization: 'Bearer ' + process.env.EG_SECRET_KEY,
'X-EG-Timestamp': ts, 'X-EG-Signature': sig,
},
body: body || undefined,
});
return res.json();
}
const init = await eg('POST', '/transactions/initialize', {
amount: 45000, merchant_reference: 'INV-2026-000123',
customer: { name: 'Amina Yusuf', email: 'amina@example.com' },
});
// res.redirect(init.data.checkout_url)
// Webhook (Express): use express.raw({ type: 'application/json' }) so you sign the raw bytes
app.post('/echogateway/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-EG-Timestamp');
const expected = crypto.createHmac('sha512', process.env.EG_WEBHOOK_SECRET).update(ts + '.' + req.body).digest('hex');
if (expected !== req.get('X-EG-Signature')) return res.sendStatus(401);
const event = JSON.parse(req.body);
res.sendStatus(200);
});
import hmac, hashlib, json, os, time, requests
def eg(method, path, payload=None):
body = json.dumps(payload, separators=(',', ':')) if payload else ''
ts = str(int(time.time()))
sk = os.environ['EG_SECRET_KEY']
sig = hmac.new(sk.encode(), f'{ts}.{body}'.encode(), hashlib.sha512).hexdigest()
headers = {
'Content-Type': 'application/json', 'Accept': 'application/json',
'X-EG-Public-Key': os.environ['EG_PUBLIC_KEY'],
'Authorization': f'Bearer {sk}', 'X-EG-Timestamp': ts, 'X-EG-Signature': sig,
}
return requests.request(method, 'https://echogateway.ng/api/v1' + path, data=body or None, headers=headers).json()
init = eg('POST', '/transactions/initialize', {
'amount': 45000, 'merchant_reference': 'INV-2026-000123',
'customer': {'name': 'Amina Yusuf', 'email': 'amina@example.com'},
})
print(init['data']['checkout_url'])
PK="pk_live_xxx"; SK="sk_live_xxx"
BODY='{"amount":45000,"merchant_reference":"INV-2026-000123","customer":{"name":"Amina Yusuf","email":"amina@example.com"}}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha512 -hmac "$SK" | sed 's/^.* //')
curl -X POST https://echogateway.ng/api/v1/transactions/initialize \
-H "Content-Type: application/json" \
-H "X-EG-Public-Key: $PK" -H "Authorization: Bearer $SK" \
-H "X-EG-Timestamp: $TS" -H "X-EG-Signature: $SIG" \
-d "$BODY"
# Test your credentials
TS=$(date +%s); SIG=$(printf '%s' "$TS." | openssl dgst -sha512 -hmac "$SK" | sed 's/^.* //')
curl https://echogateway.ng/api/v1/ping -H "X-EG-Public-Key: $PK" -H "Authorization: Bearer $SK" -H "X-EG-Timestamp: $TS" -H "X-EG-Signature: $SIG"
Go-live checklist
- GET /ping returns your merchant name (keys and signature are correct).
- Your server clock is accurate (NTP) — timestamps more than 5 minutes off are rejected.
- merchant_reference is unique per order; retries reuse it.
- Webhook endpoint verifies X-EG-Signature, is idempotent and returns 200 quickly.
- You call /verify and check status and amount before giving value.
- Return URL page does not give value by itself.
- Secret key and webhook secret are stored server-side only (environment variables / vault).
- Optional: add your server IPs to the allow-list under API & Webhooks.
Support: support@echosystems.ng · +2347053545602