Webhooks
Receive real-time event notifications for transaction status changes, payment events, invoice updates, and more.
Overview
Danipa sends webhook events to your configured endpoint whenever a significant event occurs — such as a payment completing, an invoice being paid, a remittance settling, or a wallet being funded.
All webhooks are:
- Delivered via HTTP POST with a JSON body
- Signed with HMAC-SHA256 for verification
- Retried with exponential backoff (up to 5 attempts)
- Deduplicated using the
X-Danipa-Deliveryheader
Configuration
Configure your webhook endpoint in the merchant dashboard:
- Navigate to Settings → Webhooks
- Click Add Endpoint
- Enter your endpoint URL (must be HTTPS)
- Select the events you want to receive
- Copy the Webhook Secret (starts with
whsec_) for signature verification
Event Types
Merchant / Payment Events
| Event | Description |
|---|---|
payment.completed | Payment link payment completed successfully |
payment.failed | Payment link payment failed |
payment.refunded | Payment refunded to payer |
invoice.created | New invoice created |
invoice.sent | Invoice sent to customer |
invoice.paid | Invoice paid in full |
invoice.overdue | Invoice past due date |
invoice.cancelled | Invoice cancelled |
Remittance Events
| Event | Description |
|---|---|
remittance.pending | Remittance created, awaiting processing |
remittance.processing | Funds deducted, payout initiated |
remittance.completed | Recipient received funds |
remittance.failed | Transaction failed |
remittance.refunded | Funds returned to sender |
Wallet Events
| Event | Description |
|---|---|
wallet.funded | Wallet balance increased |
wallet.debited | Wallet balance decreased |
System Events
| Event | Description |
|---|---|
rate.updated | Exchange rate changed significantly |
test.ping | Test event from the webhook tester |
Webhook Payload
{
"id": "evt_x8k2n4p1",
"type": "payment.completed",
"created_at": "2026-03-11T14:22:08Z",
"data": {
"id": "txn_r7kj2m9x",
"status": "COMPLETED",
"amount": 50.00,
"currency": "GHS",
"payment_link_id": "pl-1",
"provider": "MTN_MOMO",
"completed_at": "2026-03-11T14:22:08Z"
}
}
Webhook Headers
Every webhook request includes the following headers:
| Header | Description |
|---|---|
X-Danipa-Signature | sha256=<hex> — HMAC-SHA256 signature of the request |
X-Danipa-Timestamp | Unix epoch seconds when the webhook was sent |
X-Danipa-Event | Event type (e.g. payment.completed) |
X-Danipa-Delivery | Unique delivery ID (UUID) for deduplication |
Content-Type | Always application/json |
Signature Verification
Every webhook request includes two headers for verification:
X-Danipa-Signature: sha256=a1b2c3d4e5f6...
X-Danipa-Timestamp: 1710165008
Verification Steps
- Extract the
X-Danipa-TimestampandX-Danipa-Signatureheaders - Concatenate:
timestamp + "." + raw_body - Compute HMAC-SHA256 using your webhook secret
- Prepend
sha256=to the hex-encoded result - Compare with the signature from the header (use constant-time comparison)
- Reject if the timestamp is older than 300 seconds (replay protection)
Node.js Example
const crypto = require('crypto');
function verifyWebhook(req, webhookSecret) {
const signature = req.headers['x-danipa-signature'];
const timestamp = req.headers['x-danipa-timestamp'];
const body = req.rawBody;
// Check replay window (5 minutes)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
throw new Error('Webhook timestamp too old');
}
// Compute expected signature
const payload = `${timestamp}.${body}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(body);
}
Java Example
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HexFormat;
public boolean verifyWebhook(String signature, String timestamp,
String body, String secret) {
long age = Instant.now().getEpochSecond() - Long.parseLong(timestamp);
if (age > 300) return false;
String payload = timestamp + "." + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = "sha256=" + HexFormat.of().formatHex(
mac.doFinal(payload.getBytes(StandardCharsets.UTF_8))
);
return MessageDigest.isEqual(
signature.getBytes(StandardCharsets.UTF_8),
expected.getBytes(StandardCharsets.UTF_8)
);
}
PHP Example
function verifyWebhook(string $signature, string $timestamp,
string $body, string $secret): bool {
// Check replay window (5 minutes)
if (abs(time() - (int)$timestamp) > 300) {
return false;
}
$payload = $timestamp . '.' . $body;
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($signature, $expected);
}
// Usage in a controller
$signature = $_SERVER['HTTP_X_DANIPA_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_DANIPA_TIMESTAMP'] ?? '';
$body = file_get_contents('php://input');
if (!verifyWebhook($signature, $timestamp, $body, $webhookSecret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($body, true);
// Process event...
Retry Policy
If your endpoint returns a non-2xx status code or times out, we retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 seconds |
| 3 | 30 seconds |
| 4 | 5 minutes |
| 5 | 30 minutes |
After 5 failed attempts, the delivery is marked as failed. You can view failed deliveries and trigger manual retries in the merchant dashboard under Settings → Webhooks → Deliveries.
Testing Webhooks
You can send a test event from the merchant dashboard:
- Go to Settings → Webhooks
- Click the Test button on any endpoint
- A
test.pingevent is sent to your URL - The delivery result (status code, response) appears immediately
Best Practices
- Return 200 quickly — Process webhooks asynchronously. Return
200 OKimmediately, then handle the event in a background job. - Handle duplicates — Use the
X-Danipa-Deliveryheader or eventidto deduplicate. We guarantee at-least-once delivery. - Verify signatures — Always verify the HMAC signature before processing. Never trust unverified webhooks.
- Use HTTPS — Webhook endpoints must use HTTPS with a valid TLS certificate.
- Monitor deliveries — Check the webhook delivery log in your dashboard to catch failures early.