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
| Event | When it fires |
|---|
deposit.received | An NGN transfer has been received; FX conversion starting |
deposit.pending_withdrawal | FX complete; USDT on its way to the destination |
deposit.settled | Deposit fully settled; USDT delivered |
deposit.failed | Deposit could not be completed |
deposit.flagged | Deposit held for compliance review |
withdrawal.completed | USDT withdrawal confirmed on-chain |
onramp.completed | A virtual bank account (onramp) became active |
offramp.completed | An offramp was completed |
transfer.completed | A bank transfer was completed |
payout.completed | An NGN payout was settled |
customer.created | A new customer was created |
customer.tier1_verified | Customer passed Tier 1 (BVN) verification |
customer.tier2_verified | Customer passed Tier 2 (KYC) verification |
customer.tier2_rejected | Customer failed Tier 2 verification |
customer.balance.credited | Customer’s internal balance was credited after a deposit |
customer.balance.refunded | Customer’s internal balance was refunded |
checkout.payment.completed | A payment was made through a checkout link |
virtual_account.created | A virtual bank account was provisioned |
Register a webhook endpoint
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.
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. 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.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.
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: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 -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.