Skip to main content
Payfonte sends webhook events to your configured endpoint when payment state changes. Use webhooks as your primary source of truth for asynchronous payment outcomes.

Why Webhooks Matter

Real-Time Updates

Get payment updates without polling every transaction.

Reliable Completion

Confirm final status (success or failed) before fulfilling orders.

Operational Safety

Handle retries with idempotent processing to avoid duplicate fulfillment.

Webhook Payload Fields

FieldDescription
eventEvent type (for example payment.completed, payment.failed)
clientIdYour Payfonte client identifier
data.statusTransaction status (success, failed, or pending)
data.referencePayfonte transaction reference
data.externalReferenceMerchant-provided reference (if supplied)
data.amountTransaction amount in minor units
data.chargeApplied transaction charge
data.providerProvider slug/name used for processing
data.channelPayment channel (for example card, mobile-money)
data.userCustomer metadata (name/email/phone when available)
Sample payload:
payment.completed
{
  "event": "payment.completed",
  "clientId": "payfonte",
  "data": {
    "clientId": "payfonte",
    "reference": "ORDER-1001",
    "externalReference": "ORDER-1001",
    "platformReference": "644ec09c4c2604002fac9d17",
    "amount": 10000,
    "provider": "mtn-momo-nigeria",
    "channel": "mobile-money",
    "status": "success",
    "charge": 0,
    "chargeFee": 0,
    "user": {
      "name": "John Doe",
      "email": "john@example.com"
    },
    "paidAt": 1682883430
  }
}

Signature Verification (Required)

Every webhook includes:
  • Header: x-webhook-signature
  • Value: sha512 HMAC digest of raw request body, signed with your client-secret
Use your client-secret from Settings -> API Keys/Webhooks.
const crypto = require("crypto");

app.post("/payfonte/webhook", express.json({ type: "*/*" }), (req, res) => {
  const secret = process.env.PAYFONTE_CLIENT_SECRET;
  const payload = JSON.stringify(req.body);
  const expected = crypto.createHmac("sha512", secret).update(payload).digest("hex");
  const received = req.headers["x-webhook-signature"];

  if (expected !== received) {
    return res.status(401).send("Invalid signature");
  }

  // Acknowledge quickly, then process asynchronously.
  res.status(200).send("OK");
  processWebhook(req.body);
});
If signature validation fails, do not process the event.

Idempotent Processing Pattern

1

Acknowledge fast

Return HTTP 200 quickly to prevent unnecessary retries.
2

Check duplicates

Use reference + status (or a delivery identifier if available) to detect already-processed events.
3

Verify transaction when needed

For critical flows, verify payment status from your backend before final fulfillment.
4

Apply state transition safely

Ensure business actions (for example order fulfillment) run once per final success.

Webhook URL Priority

Payfonte uses webhook URLs in this order:
  1. Webhook URL passed in the checkout/direct-charge request (webhook)
  2. Webhook URL configured on the provider integration
  3. Webhook URL configured in dashboard settings

Common Issues

IssueLikely CauseFix
Duplicate webhook processingNo idempotency guardTrack processed reference + status and skip repeats
Signature mismatchWrong secret or payload mutationUse correct client-secret and hash the exact received body
Missed updatesEndpoint timeout/non-200 responsesReturn 200 quickly and move heavy work to async workers
Wrong environment eventsMixed sandbox/production setupUse matching endpoint and credentials per environment