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

# Receive real-time event notifications via webhooks

> Register HTTPS endpoints, verify HMAC signatures, and handle deposit, withdrawal, and checkout events from Dubu Pay in real time.

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

<Steps>
  <Step title="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.

    <CodeGroup>
      ```bash cURL theme={null}
      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"
          ]
        }'
      ```

      ```javascript Node.js theme={null}
      const response = await fetch(
        "https://api.dubupay.com/api/v1/webhooks/endpoints",
        {
          method: "POST",
          headers: {
            "X-Api-Key": "dubu_sk_live_your_api_key",
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            url: "https://yourserver.com/webhooks/dubu",
            events: [
              "deposit.settled",
              "deposit.failed",
              "withdrawal.completed",
              "checkout.payment.completed",
            ],
          }),
        }
      );
      const { data: endpoint } = await response.json();
      ```
    </CodeGroup>

    **Response:**

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

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="Store the secret securely">
    Save the `secret` value as an environment variable in your server:

    ```bash theme={null}
    DUBU_WEBHOOK_SECRET=whsec_AbCdEf1234567890AbCdEf1234567890Ab
    ```

    You'll use this value to verify that incoming requests actually come from Dubu Pay.
  </Step>

  <Step title="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.

    ```javascript Node.js theme={null}
    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);
      }
    );
    ```

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="Handle the event">
    Each event payload has the same envelope structure:

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

    ```javascript Node.js theme={null}
    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);
      }
    }
    ```
  </Step>
</Steps>

## Manage your endpoints

### List all endpoints

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.dubupay.com/api/v1/webhooks/endpoints \
    -H "X-Api-Key: dubu_sk_live_your_api_key"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.dubupay.com/api/v1/webhooks/endpoints",
    {
      headers: { "X-Api-Key": "dubu_sk_live_your_api_key" },
    }
  );
  const { data: endpoints } = await response.json();
  ```
</CodeGroup>

### Update an endpoint

Change the URL, event subscriptions, or active status:

<CodeGroup>
  ```bash cURL theme={null}
  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
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq",
    {
      method: "PATCH",
      headers: {
        "X-Api-Key": "dubu_sk_live_your_api_key",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        events: ["deposit.settled", "checkout.payment.completed"],
        is_active: true,
      }),
    }
  );
  ```
</CodeGroup>

### Delete an endpoint

```bash cURL theme={null}
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:

<CodeGroup>
  ```bash cURL theme={null}
  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"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq/rotate-secret",
    {
      method: "POST",
      headers: { "X-Api-Key": "dubu_sk_live_your_api_key" },
    }
  );
  const { data } = await response.json();
  // data.secret is the new secret — save it immediately
  ```
</CodeGroup>

```json theme={null}
{
  "success": true,
  "data": {
    "id": "wep_01hx9de2qr3st4uv5wx6pq",
    "secret": "whsec_NewSecretValue1234567890abcdef1234"
  }
}
```

<Warning>
  Rotating the secret immediately invalidates the previous one. Update your server's environment variable before rotating to avoid a gap in signature verification.
</Warning>

## Test an endpoint

Send a test event to confirm your endpoint is reachable and handling signatures correctly:

<CodeGroup>
  ```bash cURL theme={null}
  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" }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.dubupay.com/api/v1/webhooks/endpoints/wep_01hx9de2qr3st4uv5wx6pq/test",
    {
      method: "POST",
      headers: {
        "X-Api-Key": "dubu_sk_live_your_api_key",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ event: "deposit.settled" }),
    }
  );
  const { data } = await response.json();
  // data.success === true if your endpoint returned a 2xx response
  ```
</CodeGroup>

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:

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.dubupay.com/api/v1/webhooks/logs?limit=50" \
    -H "X-Api-Key: dubu_sk_live_your_api_key"
  ```

  ```javascript Node.js theme={null}
  const params = new URLSearchParams({ limit: "50" });
  const response = await fetch(
    `https://api.dubupay.com/api/v1/webhooks/logs?${params}`,
    {
      headers: { "X-Api-Key": "dubu_sk_live_your_api_key" },
    }
  );
  const { data } = await response.json();
  ```
</CodeGroup>

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:

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

<Tip>
  **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.
</Tip>

<Tip>
  **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.
</Tip>

<Note>
  **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`.
</Note>

<Warning>
  **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.
</Warning>
