DelwaPay Docs

DelwaPay API · v1

Accept payments with DelwaPay

DelwaPay lets your customers pay by bank transfer or card through a secure checkout. Your server starts a payment, sends the customer to the checkout, then confirms the payment before delivering. The API follows the conventions of the major Nigerian gateways, so moving over is quick.

  1. 1Start a payment from your server
  2. 2Send the customer to authorization_url
  3. 3Verify, then deliver

Base URL: https://api.delwapay.com. All amounts are integers in kobo (₦2,500 is 250000). Responses are JSON: {"status": true, "message": "…", "data": …}.

Keys and modes

Every business has two pairs of keys, one for test and one for live, in API keys & webhooks on the dashboard. Send a key as a bearer token:

Authorization: Bearer pri_test_…
  • pub_… public keys can only start payments. They are safe in a website or app.
  • pri_… secret keys do everything else and sign your webhooks. Keep them on your server only.
  • Test keys only touch test data and never move money. The mode is in every response as data.domain.

Start a payment

POST /transaction/initialize with your public or secret key. Returns the checkout URL to send your customer to.

curl https://api.delwapay.com/transaction/initialize \
  -H "Authorization: Bearer pub_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ada@example.com",
    "amount": 1850000,
    "reference": "FEES-2026-T1-0042",
    "callback_url": "https://yourschool.com/fees/paid",
    "metadata": { "student_id": 412 }
  }'
$response = Http::withToken(config('services.delwapay.secret'))
    ->post('https://api.delwapay.com/transaction/initialize', [
        'email' => $student->guardian_email,
        'amount' => 1850000, // kobo
        'reference' => $invoice->reference,
        'callback_url' => route('fees.paid'),
    ])->throw();

return redirect($response->json('data.authorization_url'));
const response = await fetch('https://api.delwapay.com/transaction/initialize', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.DELWAPAY_SECRET_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'ada@example.com', amount: 1850000, reference: 'order_1001', callback_url: 'https://yourapp.com/paid' }),
});
const { data } = await response.json();
res.redirect(data.authorization_url);

Fields

emailrequiredThe customer's email.
amountrequiredInteger kobo, at least 5,000.
referenceoptionalYour unique reference (letters, numbers, . _ = -). Starting the same reference again returns the same open checkout.
callback_urloptionalWhere the customer returns after paying, with ?reference= added.
webhook_urloptionalOverrides your webhook URL for this payment.
channelsoptional["bank_transfer", "card"] to limit how the customer can pay.
metadataoptionalAny JSON object, returned to you in verify and webhooks.
first_name, last_name, phoneoptionalSaved on the customer.
{
  "status": true,
  "message": "Authorization URL created",
  "data": {
    "authorization_url": "https://checkout.delwapay.com/k3v9…",
    "access_code": "k3v9…",
    "reference": "FEES-2026-T1-0042"
  }
}

Who pays the fee. By default DelwaPay's fee comes out of what you receive. Turn on "Customers pay the DelwaPay fee" in Business settings to add it at checkout instead: amount stays what you asked for (and what you receive), amount_charged is what the customer paid and fees_paid_by is customer.

Verify a payment

When the customer comes back to your callback_url, or a webhook arrives, confirm with GET /transaction/verify/{reference} using your secret key. Deliver only when data.status is success and the amount is what you expected.

{
  "status": true,
  "message": "Verification successful",
  "data": {
    "id": "DPX8H2K4…", "domain": "live", "reference": "FEES-2026-T1-0042",
    "amount": 1850000, "currency": "NGN", "status": "success",
    "channel": "bank_transfer", "fees": 27750, "paid_at": "2026-09-28T10:04:31+01:00",
    "metadata": { "student_id": 412 },
    "customer": { "email": "ada@example.com", "customer_code": "CUS_…" }
  }
}

status is one of pending, success, failed, abandoned (the checkout expired) or reversed (fully refunded).

List and fetch

GET /merchant, with either key, says which business and mode the key belongs to: {"code", "name", "domain", "key", "live_enabled"}. Use it to show what an integration is connected to, and to check that a public and a secret key belong together.

GET /transaction lists transactions newest first, filtered by status, from and to, paged with perPage (up to 100) and page. GET /transaction/{id} fetches one by its DelwaPay id.

Refunds

POST /refund with transaction (your reference or the DelwaPay id), an optional amount in kobo for a partial refund, and an optional merchant_note. Refunds are paid from your balance; DelwaPay's fee on the payment is not returned. A fully refunded transaction becomes reversed.

curl https://api.delwapay.com/refund \
  -H "Authorization: Bearer pri_live_…" -H "Idempotency-Key: refund-order-1001" \
  -d transaction=order_1001 -d amount=500000

GET /refund lists refunds (filter by transaction); GET /refund/{id} fetches one.

Webhooks

DelwaPay POSTs events to your webhook URL (set per mode in the dashboard) as JSON {"event": "…", "data": {…}}. Answer with any 2xx quickly; anything else is retried after 1, 5 and 30 minutes, then 2, 6 and 24 hours.

charge.successA payment succeeded. data is the transaction.
charge.failedA card was declined. The checkout stays open for another try.
refund.processedA refund completed. data is the refund.
invoice.paidAn invoice was paid. data is the invoice, with its transaction.
dispute.createdA payer disputed a payment; the amount is held.
dispute.resolvedA dispute was decided (won, lost or accepted).

Check the signature

Every request carries x-delwapay-signature: HMAC-SHA512 of the raw body with your secret key for that mode. Reject anything that doesn't match, then verify the transaction before acting.

// PHP
$expected = hash_hmac('sha512', $request->getContent(), config('services.delwapay.secret'));
abort_unless(hash_equals($expected, (string) $request->header('x-delwapay-signature')), 401);

// Node.js (use the raw body, not re-serialised JSON)
const expected = crypto.createHmac('sha512', process.env.DELWAPAY_SECRET_KEY).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-delwapay-signature'] || ''))) return res.sendStatus(401);

Rolling your secret key. In API keys & webhooks you can roll either key and keep the old one working for up to 7 days. During that time webhooks carry two signatures: x-delwapay-signature with the new key and x-delwapay-signature-previous with the old one. Accept either, and your server keeps verifying before and after you switch; the libraries do this for you.

Invoices

Bill a customer with line items and a due date. DelwaPay emails them the invoice with a link to pay by bank transfer or card, sends reminders 3 days before, on, and 7 days after the due date, and a receipt when they pay. You can also create and send invoices from the dashboard.

curl https://api.delwapay.com/invoice \
  -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{
    "customer": { "name": "Mrs Adaeze Obi", "email": "adaeze@example.com" },
    "items": [
      { "description": "First-term tuition", "amount": 15000000 },
      { "description": "Textbooks", "quantity": 3, "amount": 450000 }
    ],
    "due_date": "2026-10-15",
    "send": true
  }'

GET /invoice lists invoices (filter by status: draft, sent, paid, void); GET /invoice/{id} fetches one by its id or number; POST /invoice/{id}/send sends (or re-sends) it; POST /invoice/{id}/void cancels it. The response includes the customer's url. When it is paid you receive charge.success (with metadata.invoice) and invoice.paid.

Subscriptions

Bill a customer on a schedule. A plan sets the amount and how often (weekly, monthly, quarterly, termly or yearly), optionally for a fixed number of cycles. On each billing date DelwaPay creates that cycle's invoice and emails it, with reminders until it is paid. A cycle unpaid a week after its due date makes the subscription past_due; paying it makes it active again.

curl https://api.delwapay.com/plan -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "name": "JSS 1 termly fees", "amount": 15000000, "interval": "termly", "cycles": 3 }'

curl https://api.delwapay.com/subscription -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "plan": "PLN…", "customer": { "name": "Mrs Obi", "email": "obi@example.com" }, "start_date": "2026-10-01" }'

GET /plan lists plans; GET /subscription and GET /subscription/{code} return subscriptions with their invoices; POST /subscription/{code}/cancel stops billing and voids unpaid invoices. Each paid cycle sends invoice.paid with the subscription's code. Automatic card charging arrives with card payments; until then, customers pay each cycle from the email.

Disputes

When a payer contests a payment (a chargeback from their bank, or a complaint to DelwaPay), the disputed amount is held from your balance and you are emailed. Respond with evidence within 5 days in the dashboard, or through the API; DelwaPay then decides. Won, the amount returns to your balance; lost, accepted or unanswered, it is returned to the payer.

GET /dispute and GET /dispute/{id} list and fetch disputes; POST /dispute/{id}/respond with {"response": "…"} submits your answer (add files in the dashboard); POST /dispute/{id}/accept concedes it. Webhooks: dispute.created and dispute.resolved.

Dedicated accounts

Give a customer their own permanent account number. Whatever they transfer into it, from any bank and at any time, arrives as a successful payment: a charge.success webhook with dedicated_account set, the usual fee, and your settlement the next working day. Useful for school fees, rent and anything paid in instalments. Payments into it can be shared with a subaccount or a split_code.

curl https://api.delwapay.com/dedicated_account -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "customer": "obi@example.com", "first_name": "Adaeze", "last_name": "Obi" }'

customer is a customer code or an email (a new email adds the customer). Assigning again returns the same account. GET /dedicated_account and GET /dedicated_account/{id} fetch accounts (by id or account number); DELETE /dedicated_account/{id} closes one, after which transfers into it are refused.

Test mode: accounts are at the made-up DelwaPay Test Bank. POST /dedicated_account/{id}/simulate with {"amount": 5000000} pretends the customer sent money, so you can test your webhook. Live dedicated accounts arrive with DelwaPay's bank partner.

Split payments

Share a payment with others: a campus, a vendor, a partner. Each subaccount has its own bank account, and its share of every payment is settled to it the next working day, separately from yours. Shares are a percentage (in basis points: 2000 is 20%) or a flat amount in kobo; you keep the rest.

curl https://api.delwapay.com/subaccount -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "business_name": "Lekki campus", "bank_name": "GTBank", "account_number": "0123456789",
        "account_name": "Heritage College Lekki", "share_type": "percentage", "share_value": 7000 }'

curl https://api.delwapay.com/transaction/initialize -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "email": "parent@example.com", "amount": 5000000, "subaccount": "SUB_…", "bearer": "account" }'

Choosing the split on a payment

Send one of these with /transaction/initialize:

  • subaccount: that subaccount gets its default share. Add transaction_charge (kobo) to keep that amount yourself and give it everything else.
  • split_code: a saved split of several subaccounts, created in the dashboard or with POST /split: {"name", "type": "percentage"|"flat", "bearer", "subaccounts": [{"subaccount": "SUB_…", "share": 3000}]}.

bearer says who pays DelwaPay's fee: account (you, the default), subaccount (the named one; for a split, set bearer_subaccount), or all (a split only: everyone in proportion to their share). A share too small to cover its fee passes the rest to you. The division is fixed when the payment starts. Once paid, the transaction's split.allocation shows what each party received after fees.

GET /subaccount, GET /subaccount/{code} (with its balances) and PUT /subaccount/{code} manage subaccounts; GET /split, GET /split/{code} and PUT /split/{code} manage splits. GET /settlement?subaccount=SUB_… lists a subaccount's settlements. Refunds and disputes on a split payment come out of your balance: you own the relationship with the payer.

Transfers

Send money from your DelwaPay balance to any Nigerian bank account: suppliers, staff, a refund outside the original payment. Save the account once as a recipient, then transfer to its code. The amount and a small fee leave your balance at once, so what you send comes out of your next settlement.

curl https://api.delwapay.com/balance -H "Authorization: Bearer pri_test_…"

curl https://api.delwapay.com/transferrecipient -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "type": "nuban", "name": "Chuks Stationery", "bank_name": "GTBank", "account_number": "0123456789", "account_name": "Chuks Stationery Ltd" }'

curl https://api.delwapay.com/transfer -H "Authorization: Bearer pri_test_…" -H "Content-Type: application/json" \
  -d '{ "source": "balance", "amount": 2500000, "recipient": "RCP_…", "reason": "October stationery", "reference": "stationery-oct" }'

Fees: ₦10.00 up to ₦5,000.00, ₦25.00 up to ₦50,000.00, ₦50.00 above. The smallest transfer is ₦100.00. Transfers of ₦1,000,000.00 or more wait for DelwaPay's approval first (pending_approval).

status is pending, success, failed or reversed (stopped before it was sent); a failed or reversed transfer returns the amount and fee to your balance. Webhooks: transfer.success, transfer.failed and transfer.reversed. POST /transfer/bulk with {"transfers": [ … ]} sends up to 100 at once: all are checked against your balance together, and either all are made or none. GET /transfer, GET /transfer/{id} and GET /transfer/verify/{reference} fetch them; GET /transferrecipient lists recipients and DELETE /transferrecipient/{code} removes one.

Test mode: transfers are paid at once; send to account 0000000000 to see one fail. Live transfers are switched on for your business by DelwaPay.

Settlements

Each working day, what reached your balance before midnight (Lagos time) is settled: payments less DelwaPay's fees and any refunds, paid to the settlement account in your Business settings. Weekends and public holidays roll to the next working day. Every settlement has a statement on the dashboard (with a CSV download) and in the API.

GET /settlement lists settlements (filter by status, from, to); GET /settlement/{id} returns one with its payout and every line. Amounts are in kobo: total_amount collected, total_fees, total_refunds, reserve_held and effective_amount paid out. status is awaiting_approval, queued, processing, paid, failed or cancelled. A failed payout's money stays in your balance and goes out with the next settlement.

In test mode, settlements run on your test balance and are "paid" at once, so you can see the whole cycle. Use account number 0000000000 to see a payout fail.

Inline checkout

Open the checkout over your own page instead of redirecting. Start the payment on your server, then open it with the access code:

<script src="https://checkout.delwapay.com/v1/inline.js"></script>
<script>
  DelwaPay.open({
    accessCode: '{{ access_code }}', // from your server
    onSuccess: ({ reference }) => fetch('/orders/confirm?reference=' + reference), // verify on your server
    onClose: () => console.log('Checkout closed'),
  });
</script>

Or start it from the browser with your public key: DelwaPay.checkout({ key: 'pub_live_…', email, amount, onSuccess, onClose }). Either way, verify on your server before delivering.

Libraries

Official clients for PHP (8.1+) and Node.js (18+), with no dependencies. They send your secret key, unwrap data, raise errors with the status and field messages, take idempotency keys, and verify webhook signatures.

// PHP: composer require delwapay/delwapay-php
$delwapay = new DelwaPay\DelwaPay('pri_test_…');
$checkout = $delwapay->transactions->initialize(['email' => 'ada@example.com', 'amount' => 500000]);
$event = DelwaPay\Webhook::verify($rawBody, $signatureHeader, 'pri_test_…');

// Node.js: npm install @delwapay/node
import DelwaPay, { verifyWebhook } from '@delwapay/node'
const delwapay = new DelwaPay('pri_test_…')
const { authorization_url } = await delwapay.transactions.initialize({ email: 'ada@example.com', amount: 500000 })

Both cover every endpoint on this page. Any HTTP client works too: the API is plain JSON over HTTPS. For code generators, Postman or Insomnia, import the OpenAPI 3.1 description.

API versions

The API has dated versions. Your business is pinned to the version current when it signed up, so changes that would break an integration never reach yours until you choose. New fields and endpoints are added without a new version. Every response has a DelwaPay-Version header saying which version answered; send the same header to try another version on one request. Webhooks use your pinned version. Upgrade under API keys & webhooks.

  • 2026-09-29 The first version of the API.

Idempotency

Send an Idempotency-Key header (any unique string up to 255 characters) on POST requests. If a network error makes you unsure whether a request went through, retry with the same key: you get the first response back, with Idempotent-Replayed: true, and the action happens only once. Keys last 24 hours; reusing one for a different request is refused with 422.

Errors

Errors return {"status": false, "message": "…"} with a message you can show or log. Validation errors also include errors by field.

400The request is not valid (for example, an amount below the minimum).
401The key is missing, wrong, or a public key was used where a secret key is needed.
403Live payments are not enabled yet, or the business is suspended.
404Not found.
409An idempotent request with this key is still running.
422An idempotency key was reused for a different request.
429Too many requests: slow down.
5xxA problem on our side. Safe to retry with the same Idempotency-Key.

Testing

With test keys, the checkout offers a one-off transfer account and a Simulate the transfer arriving button, and accepts only these cards (any future expiry and any CVV):

4084 0840 8408 4081Visa · success
5078 5000 0000 0000Verve · success
5555 5555 5555 4444Mastercard · success
4000 0000 0000 0002Visa · declined
4000 0000 0000 9995Visa · insufficient funds

Going live

  1. Create an account and confirm your email. You start in test mode with your own keys.
  2. Complete Verification in the dashboard. We review within about a working day.
  3. Add your settlement account in Business settings.
  4. Set your live webhook URL (https) and switch your server to your live keys.
  5. Verify every payment on your server before delivering.

Limits by level. Live payments are limited by your verification level: Starter up to ₦500,000.00 a payment and ₦5,000,000.00 a month; Registered business up to ₦5,000,000.00 a payment and ₦100,000,000.00 a month. A payment above your limit is refused at /transaction/initialize with a 403 saying so. Your level and this month's total are on the Verification page; DelwaPay raises limits after further checks.