PaymonetraDocs

Webhooks

How your server learns a customer paid. One event today: `collection.success`.

#Set it up

  1. Sign in to the dashboard and open Developers → Webhooks.
  2. Set your URL. It must be https, must not carry credentials, and must resolve to a public address. We check when you save it, we check again at send time, and we do not follow redirects.
  3. Copy the signing secret shown next to it. It is created the first time you open the screen, so there is always one to copy.
  4. Do it once per mode. The URL and the secret are separate for test and live, so a test integration can never post into a live system.

#What we post

Request we make to you
POST <your url>
Content-Type: application/json
X-Paymonetra-Signature: <hex HMAC-SHA512 of the raw body, keyed with your secret>
X-Paymonetra-Event: collection.success
X-Paymonetra-Delivery: 41
Body
{
  "event": "collection.success",
  "mode": "live",
  "created_at": "2026-08-28T18:02:11+01:00",
  "data": {
    "reference": "20260828140900236399951256",
    "amount": 12500,
    "fee": 175,
    "net": 12325,
    "currency": "NGN",
    "rail": "bank",
    "customer_reference": "cus_9f21",
    "customer_name": "Ngozi Nwosu",
    "account_number": "8029607081",
    "payer_name": "Chidera Okeke",
    "paid_at": "2026-08-28T18:02:11+01:00"
  }
}

customer_reference is the merchant's own key, which is the whole point: they credit their user from it without reconciling anything.

#Verify the signature first

The header is X-Paymonetra-Signature: HMAC-SHA512, hex, over the raw request body, keyed with your webhook secret. Check it over the bytes you received, not over an object you decoded and re-encoded, or the digest will not match.

PHP
$secret = getenv('PAYMONETRA_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$sent = $_SERVER['HTTP_X_PAYMONETRA_SIGNATURE'] ?? '';

$expected = hash_hmac('sha512', $raw, $secret);

// Compare in constant time, and never decode the body first.
if (!hash_equals($expected, $sent)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
// $event['data']['customer_reference'] is your own id for the customer.
http_response_code(200);

Answer any 2xx. We treat 200, 201 and 204 alike.

#Retries, and why you will not get a duplicate

Delivery is attempted once immediately, then retried on a backoff of roughly 30s, 2m, 10m, 30m, 1h, 3h, 12h. Eight attempts over about a day, then it is abandoned and flagged for ops. Each event is sent once per reference, so a our bank partner redelivery, which happens about once in twenty on this rail, never becomes a second copy for the merchant.

Every delivery and retry is in the dashboard under Developers → Delivery log, with a button to send one again by hand.

#Your server being down never costs a payment

A merchant's server being unreachable never affects the payment. The money is credited either way, and the ledger write does not wait on the delivery.


#Events

Only collection.success fires today. The rest are listed so you write your handler once:

eventlivewhat it is
collection.successyesA customer paid. Carries your customer_reference
payout.sentnot yetArrives with settlement
payout.failednot yetArrives with settlement
refund.sentnot yetArrives with refunds

A checkout payment carries checkout_reference on the same event. Null when the transfer went straight into a customer’s permanent account with no checkout behind it.