Understanding Payment Webhooks: A Developer's Guide
DGateway is a unified payment and commerce platform for Africa that lets developers accept mobile money (MTN, Airtel) and card payments through a single API. Webhooks are central to how DGateway communicates transaction updates to your application in real time.
If you have ever integrated a payment system, you have encountered webhooks. They are the backbone of asynchronous payment processing — the mechanism that tells your application when something important happens. But webhooks are also one of the most common sources of bugs, data inconsistencies, and late-night debugging sessions.
This guide explains how webhooks work, how DGateway's webhook system is designed, and the best practices that will keep your integration rock-solid.
What Are Webhooks?
A webhook is an HTTP POST request sent from one system to another when an event occurs. Instead of your application repeatedly asking "has the payment been completed yet?" (polling), the payment gateway proactively notifies your application the moment something changes.
In the context of payments, the most common webhook events are:
- Payment completed — The customer successfully paid.
- Payment failed — The payment attempt was unsuccessful.
- Payment expired — The customer did not complete the payment within the allowed time window.
- Refund processed — A refund was issued for a previous payment.
- Subscription renewed — A recurring payment was successfully charged.
Without webhooks, you would need to poll the payment API continuously, which is inefficient, slow, and unreliable. Webhooks give you near-real-time notifications with minimal overhead.
How DGateway's Webhook System Works
When you create a collection, disbursement, or subscription through the DGateway API, you specify a callback_url. This is the URL where DGateway will send webhook notifications for that transaction.
Here is what happens behind the scenes:
- Event occurs — A transaction changes status (for example, from
pendingtosuccessful). - Payload is constructed — DGateway creates a JSON payload containing the event type and the full transaction data.
- Signature is generated — The payload is signed using HMAC-SHA256 with your webhook secret key. This signature is included in the
X-DGateway-Signatureheader. - HTTP POST is sent — DGateway sends the signed payload to your callback URL.
- Response is evaluated — If your endpoint returns a 2xx status code, the webhook is considered delivered. If it returns anything else (or times out), DGateway schedules a retry.
Webhook Payload Structure
Every webhook from DGateway follows a consistent structure:
{
"event": "collection.completed",
"timestamp": "2026-03-24T14:30:00Z",
"data": {
"id": "txn_abc123",
"status": "successful",
"amount": 50000,
"currency": "UGX",
"provider": "mtn",
"phone_number": "256771234567",
"description": "Order #1234",
"metadata": {
"order_id": "order_1234",
"customer_email": "customer@example.com"
},
"created_at": "2026-03-24T14:29:50Z",
"completed_at": "2026-03-24T14:30:00Z"
}
}The event field tells you what happened. The data field contains the full transaction record, including any metadata you attached when creating the transaction.
Event Types
DGateway sends webhooks for the following events:
| Event | Description |
|---|---|
collection.completed | A collection was successfully completed |
collection.failed | A collection attempt failed |
collection.expired | A collection request expired without payment |
disbursement.completed | A disbursement was sent successfully |
disbursement.failed | A disbursement attempt failed |
subscription.renewed | A subscription payment was charged |
subscription.cancelled | A subscription was cancelled |
refund.processed | A refund was issued |
Retry Logic
Networks are unreliable. Servers go down. Deployments cause brief outages. DGateway's retry system ensures you do not miss critical events.
If your webhook endpoint fails to return a 2xx response, DGateway retries the delivery using an exponential backoff schedule:
- Retry 1 — 1 minute after the first attempt
- Retry 2 — 5 minutes after retry 1
- Retry 3 — 30 minutes after retry 2
- Retry 4 — 2 hours after retry 3
- Retry 5 — 12 hours after retry 4
After five failed retries, the webhook is marked as failed. You can view failed webhooks in your dashboard and manually trigger a re-delivery.
This schedule means that even if your server is down for a few hours, you will still receive the webhook when it comes back online.
Best Practices for Handling Webhooks
Always Verify Signatures
Never process a webhook without verifying its signature. The signature ensures that the webhook actually came from DGateway and has not been tampered with in transit.
const crypto = require("crypto");
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Use timingSafeEqual to prevent timing attacks. A simple string comparison (===) leaks information about how many characters matched, which an attacker could exploit.
Make Your Handler Idempotent
Webhooks can be delivered more than once. Network issues, retries, or edge cases in distributed systems can result in duplicate deliveries. Your handler must be able to process the same webhook multiple times without causing problems.
The simplest approach is to track which transaction IDs you have already processed:
async function handleWebhook(event, data) {
const alreadyProcessed = await db.webhookLog.findUnique({
where: { transactionId: data.id },
});
if (alreadyProcessed) return; // Skip duplicate
await db.webhookLog.create({ data: { transactionId: data.id } });
// Process the event
}Respond Quickly
Your webhook endpoint should return a 200 response as fast as possible. Do not perform heavy processing — like sending emails, updating external systems, or running long database queries — inside the webhook handler.
Instead, receive the webhook, validate it, store the raw event, and return 200. Then process the event asynchronously using a background job or queue.
Use HTTPS
Always use an HTTPS endpoint for your webhook URL. This encrypts the webhook payload in transit and prevents man-in-the-middle attacks. DGateway will not send webhooks to HTTP endpoints in production.
Log Everything
Store the raw webhook payload alongside your processed data. If something goes wrong, having the original payload makes debugging dramatically easier. Include the timestamp, event type, and the full data object.
Monitoring and Debugging
DGateway provides several tools for monitoring your webhook health:
- Webhook logs — View every webhook sent to your endpoint, including the payload, response code, and delivery time.
- Retry history — See which webhooks were retried and whether they eventually succeeded.
- Manual re-delivery — Trigger a webhook re-delivery from the dashboard for any past event.
If you are experiencing webhook issues, start by checking the webhook logs in your dashboard. Nine times out of ten, the problem is a misconfigured endpoint URL, a signature verification bug, or a server that returned a 500 error.
Conclusion
Webhooks are the foundation of reliable payment integration. By understanding how DGateway's webhook system works — and following the best practices outlined here — you can build integrations that handle payments accurately, gracefully recover from failures, and never miss a transaction.
Get the fundamentals right, and your payment integration will be one of the most reliable parts of your entire system.