> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitliva.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive and verify Bitliva webhooks

> Get notified when a transaction finishes or a crypto deposit arrives, and verify the X-Bitliva-Signature header with HMAC-SHA256 before you trust it.

Bitliva sends a webhook to your endpoint when a transaction finishes or crypto arrives at one of your customers' addresses. Verify the signature on every request, then answer with any `2xx` status. Anything else is retried.

## Events

| Event | When it is sent | `data` contains |
| - | - | - |
| `transaction.successful` | A top-up, withdrawal, crypto buy, crypto sell or gift card sale finished successfully. | `transaction` |
| `transaction.failed` | One of those ended without completing. Any money held for it has been returned. | `transaction` |
| `crypto.deposit.received` | Crypto arrived at one of your customers' addresses. | `deposit` |
| `webhook.test` | You pressed **Send test** on an endpoint in the dashboard. | `message` |

## Request format

Every webhook is a `POST` with a JSON body and these headers:

| Header | Description |
| - | - |
| `X-Bitliva-Signature` | The signature, in the form `t=<unix time>,v1=<hex>`. |
| `X-Bitliva-Event` | The event name, for example `transaction.successful`. |
| `X-Bitliva-Delivery` | An id for this delivery. |

The body has the same envelope for every event:

```json crypto.deposit.received theme={null}
{
  "id": "evt_4f1c9a7e2b3d4a5c8e6f0a1b2c3d4e5f",
  "event": "crypto.deposit.received",
  "mode": "live",
  "created_at": "2026-10-18T10:15:02+01:00",
  "data": {
    "deposit": {
      "id": "5b8f0e2d-1c64-4a0f-8d77-3a9e2c1b6f40",
      "customer_id": "2f6d1c0a-8a3e-4d59-b8a1-71e0c4d5f9a3",
      "currency": "usdt",
      "network": "trc20",
      "amount": "25",
      "address": "TXyZ...exampleAddress",
      "txid": "9a1f4c0e7b2d58a3c6e1f0b9d4a7c2e58b3f6a1d0c9e8b7a6f5d4c3b2a1e0f9d",
      "explorer_url": "https://tronscan.org/#/transaction/9a1f4c0e7b2d58a3c6e1f0b9d4a7c2e58b3f6a1d0c9e8b7a6f5d4c3b2a1e0f9d",
      "created_at": "2026-10-18T10:15:02+01:00"
    }
  }
}
```

```json transaction.successful theme={null}
{
  "id": "evt_4f1c9a7e2b3d4a5c8e6f0a1b2c3d4e5f",
  "event": "transaction.successful",
  "mode": "live",
  "created_at": "2026-10-18T10:15:02+01:00",
  "data": {
    "transaction": {
      "id": "9d3c5a1e-5b7e-4c2a-9f43-0c6c1b7a2d10",
      "type": "crypto_sell",
      "status": "successful",
      "amount": "98010.00",
      "fee": "0.00",
      "currency": "NGN",
      "reference": "order-1042",
      "customer_id": "2f6d1c0a-8a3e-4d59-b8a1-71e0c4d5f9a3",
      "details": null,
      "crypto": {
        "currency": "btc",
        "amount": "0.001"
      },
      "created_at": "2026-10-18T10:15:00+01:00"
    }
  }
}
```

The `transaction` object is the same one described in [Wallet and transactions](https://docs.bitliva.com/guides/transactions.md).

## Verify the signature

The `v1` value is the HMAC-SHA256 of `t + "." + raw body`, using your endpoint's signing secret, written as hex. Compute it over the raw request body exactly as received, before any JSON parsing, and compare it in constant time.

```javascript Node.js theme={null}
import crypto from "node:crypto";

export function verifyBitlivaSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((part) => part.split("=", 2))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

```php PHP theme={null}
function verifyBitlivaSignature(string $rawBody, string $header, string $secret): bool
{
    $parts = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', $part, 2), 2, '');
        $parts[$key] = $value;
    }

    $expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $rawBody, $secret);

    return hash_equals($expected, $parts['v1'] ?? '');
}
```

```python Python theme={null}
import hashlib
import hmac


def verify_bitliva_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

> [!TIP]
>
> To guard against replayed requests, also reject a webhook whose `t` timestamp is more than a few minutes old.

## Handle deliveries safely

* Respond with a `2xx` status as soon as you have verified and stored the event. Any other response is retried, so do slow work after you respond.
* Retries mean you can receive the same event more than once. Use the event `id` to ignore duplicates.
* Use the `mode` field to keep test events apart from live events.
* Send a `webhook.test` event from the dashboard to check your endpoint before you rely on it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.