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 uses webhooks to push real-time notifications to your server whenever something meaningful happens in your account. When an event fires, Dubu sends an HTTP POST request to every active endpoint subscribed to that event type. Your server should respond with a 2xx status code within 5 seconds to acknowledge receipt.

How delivery works

Each webhook request contains:
  • A JSON body with the event type, data payload, and a timestamp.
  • An X-Dubu-Signature header containing an HMAC-SHA256 signature you can use to confirm the request came from Dubu.
  • An X-Dubu-Timestamp header with the ISO 8601 time the event was dispatched.
Test deliveries also include X-Dubu-Test-Mode: true so you can distinguish test events from live ones.

Payload shape

Every event payload follows this structure:
{
  "event": "deposit.settled",
  "data": {
    "id": "dep_abc123",
    "onramp_id": "onramp_xyz",
    "amount": "5000.00",
    "currency": "NGN",
    "status": "SETTLED",
    "settled_amount": "3.18",
    "asset": "USDC",
    "chain": "BASE",
    "tx_hash": "0xabc123..."
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}
event
string
The event type string, e.g. deposit.settled.
data
object
Event-specific data. The shape varies by event type; see the event catalogue below.
timestamp
string
ISO 8601 timestamp of when the event was dispatched.

Verifying signatures

Every request from Dubu includes an X-Dubu-Signature header in the format sha256=<hex_digest>. You should always verify this signature before processing an event to ensure it was not tampered with in transit. To verify, compute an HMAC-SHA256 of the raw request body using your endpoint’s signing secret, then compare it to the value in the header. Use a constant-time comparison to prevent timing attacks.

Node.js example

const crypto = require('crypto');

function verifyDubuSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody) // rawBody must be the raw Buffer or string, not parsed JSON
    .digest('hex');

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

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex')
  );
}

// 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).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log('Received event:', event.event);

  // Handle the event...
  res.status(200).send('OK');
});
Important: Parse the request body as a raw Buffer before reading it as JSON. If your framework parses JSON first, the raw body is no longer available and signature verification will fail.

Retry policy

If your endpoint returns a non-2xx response, times out, or is unreachable, Dubu retries delivery with exponential back-off. Check your delivery logs to see failed attempts and their error details. To avoid duplicate processing, make your event handlers idempotent — use the event type and the id in the data payload as a deduplication key.

Event catalogue

Deposit events

These events fire as a deposit progresses through its lifecycle.
EventDescription
deposit.receivedA deposit was detected and is pending FX conversion.
deposit.pending_withdrawalFX conversion is complete; the crypto is being sent out.
deposit.settledThe deposit has fully settled. The customer’s balance is credited.
deposit.failedThe deposit could not be processed.
deposit.flaggedThe deposit was flagged for review.
deposit.completedAlias for deposit.settled in some integration flows.
deposit.settled payload
{
  "event": "deposit.settled",
  "data": {
    "id": "dep_abc123",
    "onramp_id": "onramp_xyz",
    "amount": "5000.00",
    "currency": "NGN",
    "status": "SETTLED",
    "settled_amount": "3.18",
    "asset": "USDC",
    "chain": "BASE",
    "tx_hash": "0xabc123...",
    "payment_reference": "ref_001"
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}
deposit.failed payload
{
  "event": "deposit.failed",
  "data": {
    "id": "dep_abc123",
    "onramp_id": "onramp_xyz",
    "amount": "5000.00",
    "currency": "NGN",
    "status": "FAILED",
    "failure_reason": "Insufficient funds"
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Withdrawal events

EventDescription
withdrawal.completedA crypto withdrawal has been submitted and settled on-chain.
withdrawal.completed payload
{
  "event": "withdrawal.completed",
  "data": {
    "id": "wdraw_abc123",
    "amount_usd": "10.00",
    "token": "USDT",
    "chain": "TRX",
    "status": "SETTLED",
    "tx_hash": "0xdef456..."
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Customer events

EventDescription
customer.createdA new customer was created under your account.
customer.verifiedA customer passed basic verification.
customer.tier1_verifiedA customer’s Tier 1 KYC was approved.
customer.tier2_verifiedA customer’s Tier 2 KYC was approved.
customer.tier2_rejectedA customer’s Tier 2 KYC application was rejected.
customer.balance.creditedA customer’s internal balance was credited (e.g. after a deposit).
customer.balance.refundedA customer’s balance was refunded (e.g. after a failed transfer).
customer.balance.credited payload
{
  "event": "customer.balance.credited",
  "data": {
    "customer_id": "cus_abc123",
    "daya_customer_id": "daya_cus_xyz",
    "amount_usd": "3.1800",
    "source_type": "NGN_DEPOSIT",
    "source_id": "dep_abc123",
    "payment_reference": "ref_001",
    "amount_ngn": "5000.00",
    "asset": null,
    "chain": null,
    "tx_hash": null,
    "payment_rail": null
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Checkout events

EventDescription
checkout.payment.completedA customer completed payment on a checkout link. The linked invoice (if any) is marked as paid.
checkout.payment.completed payload
{
  "event": "checkout.payment.completed",
  "data": {
    "invoice_id": "inv_abc123",
    "invoice_number": "INV-0001",
    "amount": 75000,
    "currency": "NGN",
    "payment_method": "bank_transfer",
    "paid_at": "2024-01-15T10:30:00.000Z"
  },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Onramp and transfer events

EventDescription
onramp.completedA virtual account (onramp) became active.
offramp.completedAn offramp transaction completed.
transfer.completedAn internal transfer completed.
payout.completedA payout was processed.
virtual_account.createdA new virtual bank account was provisioned for a customer.

Best practices

Respond quickly. Your endpoint must return a response within 5 seconds. If you need to do heavy processing, return 200 OK immediately and handle the event asynchronously (e.g. push to a queue). Use idempotency keys. The same event can be delivered more than once due to retries. Use the id field inside data (or the combination of event + id) as a deduplication key in your database. Verify every signature. Always check the X-Dubu-Signature header before trusting an incoming payload. Reject requests that fail verification with a 401 status. Use HTTPS. Dubu only delivers events to HTTPS endpoints. Plain HTTP URLs are rejected at registration time. Monitor your logs. Check the delivery logs endpoint regularly to catch failed deliveries early.