Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dubupay.com/llms.txt

Use this file to discover all available pages before exploring further.

Dubu Pay sends webhook events to URLs you register whenever something significant happens — a deposit settles, a withdrawal completes, or a checkout link payment clears. This guide explains how to register an endpoint, verify signatures, handle the events you care about, and follow best practices for reliable delivery.

Events Dubu Pay sends

EventWhen it fires
deposit.receivedAn NGN transfer has been received; FX conversion starting
deposit.pending_withdrawalFX complete; USDT on its way to the destination
deposit.settledDeposit fully settled; USDT delivered
deposit.failedDeposit could not be completed
deposit.flaggedDeposit held for compliance review
withdrawal.completedUSDT withdrawal confirmed on-chain
onramp.completedA virtual bank account (onramp) became active
offramp.completedAn offramp was completed
transfer.completedA bank transfer was completed
payout.completedAn NGN payout was settled
customer.createdA new customer was created
customer.tier1_verifiedCustomer passed Tier 1 (BVN) verification
customer.tier2_verifiedCustomer passed Tier 2 (KYC) verification
customer.tier2_rejectedCustomer failed Tier 2 verification
customer.balance.creditedCustomer’s internal balance was credited after a deposit
customer.balance.refundedCustomer’s internal balance was refunded
checkout.payment.completedA payment was made through a checkout link
virtual_account.createdA virtual bank account was provisioned

Register a webhook endpoint

1

Create the endpoint

Send a POST /webhooks/endpoints request with your HTTPS URL and the list of events you want to receive. Pass an empty array to subscribe to all events.
curl -X POST https://api.dubupay.com/api/v1/webhooks/endpoints \
  -H "X-Api-Key: dubu_sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourserver.com/webhooks/dubu",
    "events": [
      "deposit.settled",
      "deposit.failed",
      "withdrawal.completed",
      "checkout.payment.completed"
    ]
  }'
Response:
{
  "success": true,
  "data": {
    "id": "wep_01hx9de2qr3st4uv5wx6pq",
    "url": "https://yourserver.com/webhooks/dubu",
    "events": [
      "deposit.settled",
      "deposit.failed",
      "withdrawal.completed",
      "checkout.payment.completed"
    ],
    "is_active": true,
    "secret": "whsec_AbCdEf1234567890AbCdEf1234567890Ab",
    "created_at": "2024-11-01T13:00:00.000Z"
  }
}
The secret is returned only once, at creation time. Save it securely (e.g. as an environment variable). You cannot retrieve it again — only rotate it.
2

Store the secret securely

Save the secret value as an environment variable in your server:
DUBU_WEBHOOK_SECRET=whsec_AbCdEf1234567890AbCdEf1234567890Ab
You’ll use this value to verify that incoming requests actually come from Dubu Pay.
3

Verify the signature on every request

Every request Dubu Pay sends to your endpoint includes an X-Dubu-Signature header containing an HMAC-SHA256 digest of the raw request body:
X-Dubu-Signature: sha256=<hex_digest>
Verify it by computing the expected HMAC using your endpoint secret and comparing it to the header value. Always use a constant-time comparison to prevent timing attacks.
Node.js
import crypto from "crypto";

function verifyDubuSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = signatureHeader.replace("sha256=", "");

  // Pad to equal length before comparing to avoid length-based timing leaks
  const expectedBuf = Buffer.from(expected, "hex");
  const receivedBuf = Buffer.from(received, "hex");

  if (expectedBuf.length !== receivedBuf.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}

// Express example
app.post(
  "/webhooks/dubu",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-dubu-signature"];
    const secret = process.env.DUBU_WEBHOOK_SECRET;

    if (!verifyDubuSignature(req.body, signature, secret)) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(req.body.toString());

    // Respond quickly — process asynchronously if needed
    res.status(200).json({ received: true });

    handleEvent(event);
  }
);
You must use the raw request body (before any JSON parsing) to compute the HMAC. If your framework parses the body first and you re-serialize it, the bytes may differ and verification will fail.
4

Handle the event

Each event payload has the same envelope structure:
{
  "event": "deposit.settled",
  "data": {
    "id": "dep_01hx9ef2qr3st4uv5wx6rs",
    "onramp_id": "onramp_01hx7kbp2qr3st4uv5wx6ab",
    "amount": "5000.00",
    "currency": "NGN",
    "status": "SETTLED",
    "settled_amount": "3.21",
    "asset": "USDT",
    "chain": "TRON",
    "tx_hash": "0xabc123...",
    "payment_reference": "TRF20241101090012"
  },
  "timestamp": "2024-11-01T09:04:32.000Z"
}
Route events by the event field:
Node.js
function handleEvent(event) {
  switch (event.event) {
    case "deposit.settled":
      fulfillOrder(event.data);
      break;
    case "deposit.failed":
      notifyCustomerOfFailure(event.data);
      break;
    case "checkout.payment.completed":
      activateSubscription(event.data.invoice_id);
      break;
    case "withdrawal.completed":
      updateWithdrawalRecord(event.data);
      break;
    default:
      console.log("Unhandled event:", event.event);
  }
}

Manage your endpoints

List all endpoints

curl https://api.dubupay.com/api/v1/webhooks/endpoints \
  -H "X-Api-Key: dubu_sk_live_your_api_key"

Update an endpoint

Change the URL, event subscriptions, or active status:
curl -X PATCH \
  https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq \
  -H "X-Api-Key: dubu_sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["deposit.settled", "checkout.payment.completed"],
    "is_active": true
  }'

Delete an endpoint

cURL
curl -X DELETE \
  https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq \
  -H "X-Api-Key: dubu_sk_live_your_api_key"

Rotate the webhook secret

If your secret is ever exposed, rotate it immediately. The new secret is returned once and replaces the old one:
curl -X POST \
  "https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq/rotate-secret" \
  -H "X-Api-Key: dubu_sk_live_your_api_key"
{
  "success": true,
  "data": {
    "id": "wep_01hx9de2qr3st4uv5wx6pq",
    "secret": "whsec_NewSecretValue1234567890abcdef1234"
  }
}
Rotating the secret immediately invalidates the previous one. Update your server’s environment variable before rotating to avoid a gap in signature verification.

Test an endpoint

Send a test event to confirm your endpoint is reachable and handling signatures correctly:
curl -X POST \
  "https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq/test" \
  -H "X-Api-Key: dubu_sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "event": "deposit.settled" }'
If you omit the event field, Dubu Pay sends a generic test.webhook event. The test request is signed with X-Dubu-Signature and marked with X-Dubu-Test-Mode: true so you can distinguish test events from live ones.

View webhook logs

Inspect the delivery history for your endpoints to debug failures:
curl "https://api.dubupay.com/api/v1/webhooks/logs?limit=50" \
  -H "X-Api-Key: dubu_sk_live_your_api_key"
You can filter logs by event, endpoint_id, and success (true or false). Results are paginated with page and limit (max 100 per page, default 50). Each log entry includes:
{
  "id": "wlog_01hx9fg2qr3st4uv5wx6tu",
  "endpoint_id": "wep_01hx9de2qr3st4uv5wx6pq",
  "endpoint_url": "https://yourserver.com/webhooks/dubu",
  "event": "deposit.settled",
  "payload": { "...": "..." },
  "status_code": 200,
  "success": true,
  "error": null,
  "created_at": "2024-11-01T09:04:35.000Z"
}

Best practices

Respond fast, process async. Your endpoint must return a 2xx status code within 10 seconds. If processing takes longer, acknowledge the request immediately and handle the event in a background job.
Use idempotency keys on your end. Dubu Pay deduplicates events internally, but network issues can still result in a delivery being retried. Store the event ID and skip processing if you’ve seen it before.
Only act on terminal statuses. For deposits, only fulfil orders when status is SETTLED. Do not take irreversible actions on PENDING_FX or PENDING_WITHDRAWAL.
Never expose your webhook secret in client-side code. The secret lives only on your server. If it is ever committed to version control or logged, rotate it immediately.