Accept payments with IbejuPay
The merchant API is a small REST surface over HTTPS. You create a transaction from your server, send your customer to the hosted checkout to pay by bank transfer, and confirm the result by webhook and/or the verify endpoint.
| Fact | Value |
|---|---|
| Base URL | https://ibejupay.com/api/v1 |
| Format | JSON over HTTPS. Responses use one envelope: {status, message, data} on success, {status:false, code, message} on error. |
| Amounts | Integers in kobo (₦1.00 = 100). ₦5,000.00 is 500000. Between 10,000 and 1,000,000,000 kobo per transaction. |
| Currency | NGN only. |
| Request id | Every response carries an X-Request-Id header — quote it to support. |
| Checkout window | A transaction stays payable for 30 minutes, then expires. |
Authentication
Authenticate every call with your secret key in the
Authorization header. Keys come in two modes and the data they
see is fully isolated:
sk_test_…— test mode. Issued at onboarding; payments are simulated.sk_live_…— live mode. Works after your account is approved for live payments.
curl https://ibejupay.com/api/v1/merchant \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxx"Accept a payment
Step 1 — Initialize the transaction
From your server, create the transaction with the amount in kobo, the customer's email and your own unique reference (this is the id you'll verify with later — an order id works well).
curl https://ibejupay.com/api/v1/transactions/initialize \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"email": "customer@example.com",
"reference": "order-8021",
"callback_url": "https://yourapp.com/payment/complete",
"customer_name": "Adaeze Obi",
"customer_phone": "08012345678",
"description": "Order #8021 — 2 bags of rice",
"metadata": {"order_id": 8021, "cart_items": 2}
}'$payload = [
'amount' => 500000, // ₦5,000.00 in kobo
'email' => 'customer@example.com',
'reference' => 'order-8021', // unique per transaction
'callback_url' => 'https://yourapp.com/payment/complete',
'metadata' => ['order_id' => 8021],
];
$ch = curl_init('https://ibejupay.com/api/v1/transactions/initialize');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('IBEJUPAY_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($res && $res['status'] === true) {
// Send the customer to the hosted checkout:
header('Location: ' . $res['data']['authorization_url']);
exit;
}const res = await fetch('https://ibejupay.com/api/v1/transactions/initialize', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.IBEJUPAY_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 500000, // ₦5,000.00 in kobo
email: 'customer@example.com',
reference: 'order-8021', // unique per transaction
callback_url: 'https://yourapp.com/payment/complete',
metadata: { order_id: 8021 },
}),
});
const body = await res.json();
if (body.status === true) {
// Redirect the customer to body.data.authorization_url
console.log(body.data.authorization_url);
}import os, requests
res = requests.post(
"https://ibejupay.com/api/v1/transactions/initialize",
headers={"Authorization": f"Bearer {os.environ['IBEJUPAY_SECRET_KEY']}"},
json={
"amount": 500000, # ₦5,000.00 in kobo
"email": "customer@example.com",
"reference": "order-8021", # unique per transaction
"callback_url": "https://yourapp.com/payment/complete",
"metadata": {"order_id": 8021},
},
timeout=30,
)
body = res.json()
if body["status"] is True:
# Redirect the customer to the hosted checkout:
print(body["data"]["authorization_url"])The response gives you the hosted-checkout URL:
{
"status": true,
"message": "Authorization URL created",
"data": {
"authorization_url": "https://ibejupay.com/pay/checkout/9f2c4a…",
"access_code": "9f2c4a…",
"reference": "order-8021",
"amount": 500000,
"currency": "NGN",
"mode": "test",
"expires_at": "2026-07-30T13:05:22+01:00"
}
}authorization_url. A reference that was
already paid (or used with a different amount) returns
409 duplicate_reference — use a fresh reference per transaction.
Step 2 — Redirect your customer
Send the customer to data.authorization_url. On the hosted checkout
they confirm their details, pick Fidelity or Access Bank, and get a one-time
virtual account to transfer the exact amount to — from any bank app, USSD or
bank branch. IbejuPay confirms the transfer automatically; the page updates
live and sends an SMS receipt.
Step 3 — Handle the callback
After payment, the customer is redirected to your callback_url
(per-transaction, or the default on your merchant profile) with the reference
appended:
https://yourapp.com/payment/complete?reference=order-8021&trxref=order-8021&status=successStep 4 — Verify before you fulfil
curl https://ibejupay.com/api/v1/transactions/verify/order-8021 \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxx"{
"status": true,
"message": "Verification successful",
"data": {
"reference": "order-8021",
"amount": 500000,
"currency": "NGN",
"status": "success",
"gateway_ref": "IBGW-260730121502-0042",
"channel": "fidelity",
"fee": 7500,
"net_amount": 492500,
"paid_amount": 500000,
"mode": "test",
"description": "Order #8021 — 2 bags of rice",
"customer": {
"name": "Adaeze Obi",
"email": "customer@example.com",
"phone": "2348012345678"
},
"metadata": {"order_id": 8021, "cart_items": 2},
"merchant_code": "IBM-9F3C2A1B",
"created_at": "2026-07-30T12:35:22+01:00",
"paid_at": "2026-07-30T12:38:41+01:00",
"expires_at": "2026-07-30T13:05:22+01:00"
}
}
Treat the payment as complete only when data.status is
"success" and data.amount matches what you
expected to be paid.
Webhooks
Set your webhook endpoint at onboarding (or ask the gateway team to update it). IbejuPay POSTs signed JSON events to it — this is how you learn about a payment even when the customer never returns to your site.
| Event | When |
|---|---|
charge.success | A transaction was confirmed paid (live: bank transfer confirmed; test: simulated). |
ping | A test event you can trigger any time via POST /webhooks/test to check your endpoint and signature code. |
POST https://yourapp.com/webhooks/ibejupay
Content-Type: application/json
X-IbejuPay-Event: charge.success
X-IbejuPay-Signature: 3f9a1c… (HMAC-SHA512 hex of the raw body)
{
"event": "charge.success",
"event_id": "evt_a1b2c3d4e5f60718",
"created_at": "2026-07-30T12:38:41+01:00",
"data": {
"reference": "order-8021",
"amount": 500000,
"currency": "NGN",
"status": "success",
"gateway_ref": "IBGW-260730121502-0042",
"channel": "fidelity",
"fee": 7500,
"net_amount": 492500,
"mode": "live",
"customer": {"name": "Adaeze Obi", "email": "customer@example.com", "phone": "2348012345678"},
"metadata": {"order_id": 8021},
"merchant_code": "IBM-9F3C2A1B",
"paid_at": "2026-07-30T12:38:41+01:00"
}
}Verify the signature
Every delivery carries X-IbejuPay-Signature: the hex HMAC-SHA512 of
the raw request body using your webhook signing secret
(whsec_…, issued at onboarding). Reject anything that doesn't match.
<?php
// webhook.php — your endpoint for POST /webhooks/ibejupay
$secret = getenv('IBEJUPAY_WEBHOOK_SECRET'); // whsec_…
$rawBody = file_get_contents('php://input');
$given = $_SERVER['HTTP_X_IBEJUPAY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha512', $rawBody, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401); // not from IbejuPay — reject
exit;
}
$event = json_decode($rawBody, true);
// Idempotency: you may receive the same event more than once.
// Store event_id and skip any you have already processed.
if (already_processed($event['event_id'])) {
http_response_code(200);
exit;
}
if ($event['event'] === 'charge.success') {
$data = $event['data'];
// Trust, then verify: confirm against the API before fulfilling.
// GET /transactions/verify/{$data['reference']} must say "success".
mark_order_paid($data['reference'], $data['amount']);
}
http_response_code(200); // acknowledge FAST; do heavy work asynchronouslyimport crypto from 'node:crypto';
import express from 'express';
const app = express();
// IMPORTANT: the signature covers the RAW body — capture it before JSON parsing.
app.post('/webhooks/ibejupay', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha512', process.env.IBEJUPAY_WEBHOOK_SECRET)
.update(req.body) // the raw Buffer
.digest('hex');
const given = req.get('X-IbejuPay-Signature') || '';
const a = Buffer.from(expected); const b = Buffer.from(given);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401); // not from IbejuPay — reject
}
const event = JSON.parse(req.body.toString());
if (event.event === 'charge.success') {
// Idempotency: skip event_ids you have already processed, then
// verify via GET /transactions/verify/{reference} before fulfilling.
}
res.sendStatus(200); // acknowledge fast
});Delivery rules
- Respond 2xx fast (within 15 seconds). Do heavy work asynchronously.
- Retries: failed deliveries are retried up to 5 times with exponential backoff. The gateway team can also re-send any delivery on request.
- Idempotency: the same
event_idmay arrive more than once — store processed ids and skip duplicates. - Reachability: the endpoint must be a public HTTPS URL (private/internal addresses are refused).
- Ordering is not guaranteed — always key off the event content, not arrival order.
Test mode
Everything you build works identically in test mode, with one difference: the
hosted checkout shows a “Simulate a successful payment” button instead of
a real bank account. Simulating runs the full settlement sequence — the
transaction becomes success, the signed charge.success
webhook fires, and the customer is redirected to your callback.
- Test transactions are visible only to
sk_test_…keys; live only tosk_live_…keys. - Test mode charges no fees and never touches the banks.
- Use
POST /webhooks/testto exercise your endpoint any time.
Errors & rate limits
Errors use conventional HTTP codes plus a stable machine-readable code:
{
"status": false,
"code": "invalid_amount",
"message": "amount must be a positive integer in KOBO (e.g. ₦5,000.00 = 500000)."
}
The most common: authorization_required, invalid_key,
key_revoked, merchant_suspended, live_not_enabled,
invalid_amount, invalid_reference, duplicate_reference,
transaction_not_found, rate_limited, service_unavailable.
The reference lists them all.
Rate limit: 120 requests per minute per merchant
(HTTP 429 rate_limited beyond it). Poll verify at a
sensible cadence — webhooks exist so you don't have to poll at all.
Go-live checklist
- Initialize uses a fresh unique reference per transaction and stores it with the order.
- Fulfilment happens only after verify (or a signature-checked webhook) says
successand the amount matches. - Your webhook endpoint verifies the HMAC-SHA512 signature over the raw body and rejects mismatches.
- Duplicate
event_ids are skipped (idempotent processing). - Secret keys live in server-side configuration, never in client code or repositories.
- You've handled the customer abandoning checkout (the transaction expires after 30 minutes →
expired). - A full test-mode payment (initialize → simulate → webhook → verify) passes end-to-end.
Ready? Email info@ibejulekki.lg.gov.ng to
request live approval. The gateway team reviews your integration, enables live
payments, and issues your sk_live_… key.
