IbejuPaySelf-Service Portal
Guide

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.

FactValue
Base URLhttps://ibejupay.com/api/v1
FormatJSON over HTTPS. Responses use one envelope: {status, message, data} on success, {status:false, code, message} on error.
AmountsIntegers in kobo (₦1.00 = 100). ₦5,000.00 is 500000. Between 10,000 and 1,000,000,000 kobo per transaction.
CurrencyNGN only.
Request idEvery response carries an X-Request-Id header — quote it to support.
Checkout windowA 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
curl https://ibejupay.com/api/v1/merchant \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxx"
Keep secret keys secret. Call the API from your server only — never from a browser or mobile app. If a key leaks, revoke it from your merchant profile and switch to a fresh one; both can briefly coexist so you rotate without downtime.

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
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}
      }'
PHP
$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;
}
Node.js
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);
}
Python
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:

Response — 200
{
  "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"
  }
}
Idempotent by reference. Re-initializing the same open reference with the same amount returns the same 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:

Callback redirect
https://yourapp.com/payment/complete?reference=order-8021&trxref=order-8021&status=success
Never fulfil on the callback alone. Query-string parameters can be typed by anyone. The callback is a UX signal — the source of truth is the webhook and the verify endpoint.

Step 4 — Verify before you fulfil

cURL
curl https://ibejupay.com/api/v1/transactions/verify/order-8021 \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxx"
Response — 200
{
  "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.

EventWhen
charge.successA transaction was confirmed paid (live: bank transfer confirmed; test: simulated).
pingA test event you can trigger any time via POST /webhooks/test to check your endpoint and signature code.
Webhook request
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
<?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 asynchronously
Node.js
import 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_id may 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 to sk_live_… keys.
  • Test mode charges no fees and never touches the banks.
  • Use POST /webhooks/test to exercise your endpoint any time.

Errors & rate limits

Errors use conventional HTTP codes plus a stable machine-readable code:

Error envelope
{
  "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

  1. Initialize uses a fresh unique reference per transaction and stores it with the order.
  2. Fulfilment happens only after verify (or a signature-checked webhook) says success and the amount matches.
  3. Your webhook endpoint verifies the HMAC-SHA512 signature over the raw body and rejects mismatches.
  4. Duplicate event_ids are skipped (idempotent processing).
  5. Secret keys live in server-side configuration, never in client code or repositories.
  6. You've handled the customer abandoning checkout (the transaction expires after 30 minutes → expired).
  7. 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.