> ## 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.

# Webhook event types and payload reference

> Learn how Dubu delivers webhook events, verify request signatures with HMAC-SHA256, and browse all event types with their payload shapes and retry behavior.

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:

```json theme={null}
{
  "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"
}
```

<ResponseField name="event" type="string">
  The event type string, e.g. `deposit.settled`.
</ResponseField>

<ResponseField name="data" type="object">
  Event-specific data. The shape varies by event type; see the event catalogue below.
</ResponseField>

<ResponseField name="timestamp" type="string">
  ISO 8601 timestamp of when the event was dispatched.
</ResponseField>

***

## 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

```js theme={null}
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](/api-reference/webhooks/endpoints#view-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.

| Event                        | Description                                                        |
| ---------------------------- | ------------------------------------------------------------------ |
| `deposit.received`           | A deposit was detected and is pending FX conversion.               |
| `deposit.pending_withdrawal` | FX conversion is complete; the crypto is being sent out.           |
| `deposit.settled`            | The deposit has fully settled. The customer's balance is credited. |
| `deposit.failed`             | The deposit could not be processed.                                |
| `deposit.flagged`            | The deposit was flagged for review.                                |
| `deposit.completed`          | Alias for `deposit.settled` in some integration flows.             |

**`deposit.settled` payload**

```json theme={null}
{
  "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**

```json theme={null}
{
  "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

| Event                  | Description                                                  |
| ---------------------- | ------------------------------------------------------------ |
| `withdrawal.completed` | A crypto withdrawal has been submitted and settled on-chain. |

**`withdrawal.completed` payload**

```json theme={null}
{
  "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

| Event                       | Description                                                        |
| --------------------------- | ------------------------------------------------------------------ |
| `customer.created`          | A new customer was created under your account.                     |
| `customer.verified`         | A customer passed basic verification.                              |
| `customer.tier1_verified`   | A customer's Tier 1 KYC was approved.                              |
| `customer.tier2_verified`   | A customer's Tier 2 KYC was approved.                              |
| `customer.tier2_rejected`   | A customer's Tier 2 KYC application was rejected.                  |
| `customer.balance.credited` | A customer's internal balance was credited (e.g. after a deposit). |
| `customer.balance.refunded` | A customer's balance was refunded (e.g. after a failed transfer).  |

**`customer.balance.credited` payload**

```json theme={null}
{
  "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

| Event                        | Description                                                                                     |
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
| `checkout.payment.completed` | A customer completed payment on a checkout link. The linked invoice (if any) is marked as paid. |

**`checkout.payment.completed` payload**

```json theme={null}
{
  "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

| Event                     | Description                                                |
| ------------------------- | ---------------------------------------------------------- |
| `onramp.completed`        | A virtual account (onramp) became active.                  |
| `offramp.completed`       | An offramp transaction completed.                          |
| `transfer.completed`      | An internal transfer completed.                            |
| `payout.completed`        | A payout was processed.                                    |
| `virtual_account.created` | A 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](/api-reference/webhooks/endpoints#view-delivery-logs) regularly to catch failed deliveries early.
