Home/Developers/Webhooks
Reseller API v1

Webhooks

The events we send to your server (purchases, checkouts, customer deposits, payouts) and how to verify that they are really from us.

Every code block, endpoint and section has a Copy button.

Set up

Set an https URL on your business's API & webhooks page. Each business has its own URL and signing secret, and every event carries business_id. The business page lists every webhook sent with your server's answer and a Resend button. Payouts are sent to the webhook URL of your oldest approved business that has one.

Delivery & retries

  • We POST JSON to your URL. Answer with any 2xx status within 10 seconds; anything else (or a timeout) counts as a failed attempt.
  • Failed attempts are retried with growing gaps (30 s, 1 min, 2 min, ... up to 1 hour apart) for up to 8 attempts.
  • Your URL must be https://, resolve to a public address, and must not redirect.
  • Payloads carry status only - pins and tokens are fetched through the authenticated API. Webhooks can arrive more than once or out of order: de-duplicate by id and confirm with the GET endpoint when it matters.

Every delivery has these headers:

HeaderValue
X-PowerWave-EventThe event name, e.g. checkout.paid.
X-PowerWave-DeliveryOur delivery ID (the same when a delivery is re-sent).
X-PowerWave-Signaturet=<unix time>,v1=<hex> - see below.

Events

EventWhenDocs
transaction.successful / transaction.failedAn API purchase finished, or failed and was refunded.Purchases
checkout.paid / checkout.expired / checkout.failedA checkout was paid, closed unpaid, or rejected after review.Payments
checkout.additional_paymentExtra money arrived on a checkout that was already paid or closed.Payments
collection.receivedA customer paid into their permanent account number.Payments
payout.successful / payout.failedA payout reached its final state.Payouts
pingThe "Send test event" button on your API page.-

Payloads

transaction.successful

A purchase finished (or was refunded: transaction.failed).

{
    "id": "evt_3f9a1c2b7d4e5f60a1b2c3d4",
    "event": "transaction.successful",
    "created_at": "2026-10-06T19:31:04+01:00",
    "business_id": 12,
    "data": {
        "reference": "ORDER-10293",
        "transaction_ref": "PW8K2J4L9QXA",
        "type": "airtime",
        "status": "successful",
        "amount": "970.00",
        "currency": "NGN"
    }
}

checkout.paid

A checkout was paid and credited to your wallet (also checkout.expired, checkout.failed). The data is the checkout object.

{
    "id": "evt_3f9a1c2b7d4e5f60a1b2c3d4",
    "event": "checkout.paid",
    "created_at": "2026-10-06T19:31:04+01:00",
    "business_id": 12,
    "data": {
        "reference": "INV-2026-0142",
        "checkout_reference": "PWCK7Q2M9XKD4RTB8WNA",
        "checkout_url": "https://checkout.powerwave.ng/PWCK7Q2M9XKD4RTB8WNA",
        "business_id": 12,
        "source": "api",
        "status": "paid",
        "paid_with": "bank_transfer",
        "amount": "45000.00",
        "fee": "675.00",
        "fee_bearer": "reseller",
        "customer_pays": "45000.00",
        "you_receive": "44325.00",
        "bank_name": "Paga",
        "account_number": "9946503468",
        "customer": {
            "name": "Adaeze Okafor",
            "email": "adaeze@example.com",
            "phone": null,
            "ref": null
        },
        "amount_paid": "45000.00",
        "note": null,
        "expires_at": "2026-10-06T20:01:00+01:00",
        "paid_at": "2026-10-06T19:40:12+01:00",
        "created_at": "2026-10-06T19:31:00+01:00"
    }
}

checkout.additional_payment

More money arrived on an already paid / closed checkout. The checkout object plus the extra amount.

{
    "id": "evt_3f9a1c2b7d4e5f60a1b2c3d4",
    "event": "checkout.additional_payment",
    "created_at": "2026-10-06T19:31:04+01:00",
    "business_id": 12,
    "data": {
        "reference": "INV-2026-0142",
        "checkout_reference": "PWCK7Q2M9XKD4RTB8WNA",
        "checkout_url": "https://checkout.powerwave.ng/PWCK7Q2M9XKD4RTB8WNA",
        "business_id": 12,
        "source": "api",
        "status": "paid",
        "paid_with": null,
        "amount": "45000.00",
        "fee": "675.00",
        "fee_bearer": "reseller",
        "customer_pays": "45000.00",
        "you_receive": "44325.00",
        "bank_name": "Paga",
        "account_number": "9946503468",
        "customer": {
            "name": "Adaeze Okafor",
            "email": "adaeze@example.com",
            "phone": null,
            "ref": null
        },
        "amount_paid": null,
        "note": null,
        "expires_at": "2026-10-06T20:01:00+01:00",
        "paid_at": null,
        "created_at": "2026-10-06T19:31:00+01:00",
        "additional_amount": "5000.00",
        "additional_fee": "75.00"
    }
}

collection.received

A customer paid into their permanent account number.

{
    "id": "evt_3f9a1c2b7d4e5f60a1b2c3d4",
    "event": "collection.received",
    "created_at": "2026-10-06T19:31:04+01:00",
    "business_id": 12,
    "data": {
        "customer_ref": "STU-0042",
        "account_number": "9812962181",
        "amount": "20000.00",
        "fee": "300.00",
        "you_receive": "19700.00",
        "transaction_ref": "PWC4D8K2L9QX",
        "currency": "NGN"
    }
}

payout.successful

A payout reached its final state (or payout.failed with a failure_reason).

{
    "id": "evt_3f9a1c2b7d4e5f60a1b2c3d4",
    "event": "payout.successful",
    "created_at": "2026-10-06T19:31:04+01:00",
    "business_id": 12,
    "data": {
        "reference": "PAYOUT-7781",
        "status": "successful",
        "amount": "25000.00",
        "fee": "100.00",
        "currency": "NGN",
        "failure_reason": null
    }
}

Verify the signature

Header X-PowerWave-Signature: t=1791297064,v1=<hex> where v1 = HMAC-SHA256(your_signing_secret, t + "." + raw_body). Use the raw request body (not a re-encoded copy). Reject the request if the signature does not match or t is older than 5 minutes.

<?php
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_POWERWAVE_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $raw, getenv('PW_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}
$event = json_decode($raw, true);
// de-duplicate by $event['id'], then handle $event['event'] ...
http_response_code(200);
// Node.js (Express: use express.raw({ type: 'application/json' }) for this route)
const crypto = require('crypto');
const parts = Object.fromEntries(req.get('X-PowerWave-Signature').split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.PW_WEBHOOK_SECRET)
  .update(parts.t + '.' + req.body.toString('utf8')).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''))) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// de-duplicate by event.id, then handle event.event ...
res.sendStatus(200);

Questions? Contact us.