Skip to main content
Danipa

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-Delivery header

Configuration

Configure your webhook endpoint in the merchant dashboard:

  1. Navigate to Settings → Webhooks
  2. Click Add Endpoint
  3. Enter your endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. Copy the Webhook Secret (starts with whsec_) for signature verification

Event Types

Merchant / Payment Events

EventDescription
payment.completedPayment link payment completed successfully
payment.failedPayment link payment failed
payment.refundedPayment refunded to payer
invoice.createdNew invoice created
invoice.sentInvoice sent to customer
invoice.paidInvoice paid in full
invoice.overdueInvoice past due date
invoice.cancelledInvoice cancelled

Remittance Events

EventDescription
remittance.pendingRemittance created, awaiting processing
remittance.processingFunds deducted, payout initiated
remittance.completedRecipient received funds
remittance.failedTransaction failed
remittance.refundedFunds returned to sender

Wallet Events

EventDescription
wallet.fundedWallet balance increased
wallet.debitedWallet balance decreased

System Events

EventDescription
rate.updatedExchange rate changed significantly
test.pingTest 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:

HeaderDescription
X-Danipa-Signaturesha256=<hex> — HMAC-SHA256 signature of the request
X-Danipa-TimestampUnix epoch seconds when the webhook was sent
X-Danipa-EventEvent type (e.g. payment.completed)
X-Danipa-DeliveryUnique delivery ID (UUID) for deduplication
Content-TypeAlways application/json

Signature Verification

Every webhook request includes two headers for verification:

X-Danipa-Signature: sha256=a1b2c3d4e5f6...
X-Danipa-Timestamp: 1710165008

Verification Steps

  1. Extract the X-Danipa-Timestamp and X-Danipa-Signature headers
  2. Concatenate: timestamp + "." + raw_body
  3. Compute HMAC-SHA256 using your webhook secret
  4. Prepend sha256= to the hex-encoded result
  5. Compare with the signature from the header (use constant-time comparison)
  6. 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:

AttemptDelay
1Immediate
25 seconds
330 seconds
45 minutes
530 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:

  1. Go to Settings → Webhooks
  2. Click the Test button on any endpoint
  3. A test.ping event is sent to your URL
  4. The delivery result (status code, response) appears immediately

Best Practices

  • Return 200 quickly — Process webhooks asynchronously. Return 200 OK immediately, then handle the event in a background job.
  • Handle duplicates — Use the X-Danipa-Delivery header or event id to 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.