> For the complete documentation index, see [llms.txt](https://ivorypay.gitbook.io/ivorypay-api-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ivorypay.gitbook.io/ivorypay-api-documentation/transactions/webhooks.md).

# Webhooks

Webhooks let IvoryPay push real-time notifications to your server whenever a transaction changes state. Instead of polling the API, your server receives an HTTP POST request the moment something happens.

**Delivery method:** `POST` to your registered URL\
**Signature header:** `x-ivorypay-signature`

***

## Setup

Webhook configuration is done through your **IvoryPay Merchant Dashboard** under **Settings → Webhooks**. From there you can:

* Register a webhook URL for your TEST or LIVE environment
* Choose which events to subscribe to
* Update or remove your webhook at any time

> You can have one webhook per environment — one for `TEST` and one for `LIVE`.

***

## How It Works

```
1. Set your webhook URL in the Merchant Dashboard
2. A transaction status changes
3. IvoryPay POSTs to your URL  →  { event, data } + signature header
4. Your server responds with HTTP 200  →  Delivery confirmed
```

If your server does not return `200`, IvoryPay will retry delivery automatically.

***

## Receiving Webhooks

IvoryPay sends a `POST` request to your registered URL with the following structure:

### Request Headers

| Header                 | Value                                |
| ---------------------- | ------------------------------------ |
| `Content-Type`         | `application/json`                   |
| `x-ivorypay-signature` | HMAC-SHA512 hash of the request body |

### Request Body

```json
{
  "event": "cryptoCollection.success",
  "data": {
    "reference": "550e8400-e29b-41d4-a716-446655440000",
    "status": "SUCCESS",
    "token": "USDT",
    "address": "0xYourCollectionWalletAddress",
    "expectedAmountInCrypto": "10.5",
    "receivedAmountInCrypto": "10.5",
    "settledAmountInCrypto": "10.2",
    "cryptoTransactionHash": "0xabc123...",
    "environment": "LIVE",
    "expiresAt": "2026-02-19T10:10:00.000Z",
    "completedAt": "2026-02-19T10:05:32.000Z",
    "failureReason": null,
    "metadata": {}
  }
}
```

### Payload Fields

| Field                         | Type     | Description                                                              |
| ----------------------------- | -------- | ------------------------------------------------------------------------ |
| `event`                       | `string` | The event that triggered this webhook (e.g. `cryptoCollection.success`). |
| `data.reference`              | `string` | Your unique transaction reference (UUID).                                |
| `data.status`                 | `string` | Current status: `PENDING`, `PROCESSING`, `SUCCESS`, or `FAILED`.         |
| `data.token`                  | `string` | Crypto token involved (e.g. `USDT`, `USDC`).                             |
| `data.address`                | `string` | Wallet address for crypto transactions.                                  |
| `data.expectedAmountInCrypto` | `string` | Amount expected to be received.                                          |
| `data.receivedAmountInCrypto` | `string` | Amount actually received on-chain.                                       |
| `data.settledAmountInCrypto`  | `string` | Amount settled after fees.                                               |
| `data.cryptoTransactionHash`  | `string` | On-chain transaction hash. `null` until confirmed.                       |
| `data.environment`            | `string` | `TEST` or `LIVE`.                                                        |
| `data.expiresAt`              | `string` | ISO timestamp when the payment window closes.                            |
| `data.completedAt`            | `string` | ISO timestamp when the transaction was finalised. `null` if pending.     |
| `data.failureReason`          | `string` | Human-readable reason for failure. `null` if not failed.                 |
| `data.metadata`               | `object` | Custom metadata you passed when creating the transaction.                |

***

## Verifying the Signature

Every webhook request includes an `x-ivorypay-signature` header. This is an HMAC-SHA512 hash of the body.data, signed with your **secret API key**.

Always verify this signature before processing a webhook to confirm it came from IvoryPay.

### Node.js

```javascript
const crypto = require('crypto');

function verifyWebhook(req, secretKey) {
  const signature = req.headers['x-ivorypay-signature'];
  const body = JSON.stringify(req.body.data);

  const expected = crypto
    .createHmac('sha512', secretKey)
    .update(body)
    .digest('hex');

  return signature === expected;
}

// Express example
app.post('/webhooks/ivorypay', express.json(), (req, res) => {
  if (!verifyWebhook(req, process.env.IVORYPAY_SECRET_KEY)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = req.body;

  switch (event) {
    case 'cryptoCollection.success':
      // Fulfil the order
      break;
    case 'cryptoCollection.failed':
      // Notify the customer
      break;
    case 'fiatPayout.success':
      // Mark withdrawal as complete
      break;
  }

  res.sendStatus(200); // Always respond with 200
});
```

### Python

```python
import hmac
import hashlib

def verify_webhook(body: bytes, signature: str, secret_key: str) -> bool:
    expected = hmac.new(
        secret_key.encode(),
        body,
        hashlib.sha512
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example
@app.route('/webhooks/ivorypay', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('x-ivorypay-signature')
    if not verify_webhook(request.data, signature, SECRET_KEY):
        return 'Invalid signature', 401

    payload = request.json
    event = payload['event']
    data = payload['data']

    if event == 'cryptoCollection.success':
        pass  # Fulfil the order

    return '', 200
```

***

## Event Types

Events follow the pattern `<category>.<status>`.

### Collection Events

Triggered when a customer makes a payment to your business.

| Event                         | Description                                                       |
| ----------------------------- | ----------------------------------------------------------------- |
| `cryptoCollection.pending`    | A crypto payment request was created and is awaiting payment.     |
| `cryptoCollection.processing` | Crypto payment detected on-chain, awaiting confirmations.         |
| `cryptoCollection.success`    | Crypto payment fully confirmed and received.                      |
| `cryptoCollection.failed`     | Crypto payment expired or failed.                                 |
| `fiatCollection.pending`      | A fiat payment request was created and is awaiting bank transfer. |
| `fiatCollection.processing`   | Fiat payment received, being verified.                            |
| `fiatCollection.success`      | Fiat payment confirmed and received.                              |
| `fiatCollection.failed`       | Fiat payment failed or was not received in time.                  |

### Payout Events

Triggered when you send funds to a recipient.

| Event                     | Description                                                      |
| ------------------------- | ---------------------------------------------------------------- |
| `cryptoPayout.pending`    | A crypto transfer was initiated and is queued.                   |
| `cryptoPayout.processing` | Crypto transfer has been broadcast to the blockchain.            |
| `cryptoPayout.success`    | Crypto transfer confirmed on-chain. Funds delivered.             |
| `cryptoPayout.failed`     | Crypto transfer failed. No funds were deducted.                  |
| `fiatPayout.pending`      | A fiat transfer was initiated and is queued.                     |
| `fiatPayout.processing`   | Fiat transfer submitted to the bank for processing.              |
| `fiatPayout.success`      | Fiat transfer completed. Funds delivered to recipient's account. |
| `fiatPayout.failed`       | Fiat transfer failed. No funds were deducted.                    |

### Settlement Events

Triggered when collected funds are settled to your wallet or bank account.

| Event                         | Description                                            |
| ----------------------------- | ------------------------------------------------------ |
| `cryptoSettlement.pending`    | Crypto settlement initiated, waiting to be processed.  |
| `cryptoSettlement.processing` | Crypto settlement is being sent to your wallet.        |
| `cryptoSettlement.success`    | Crypto settlement completed. Funds in your wallet.     |
| `cryptoSettlement.failed`     | Crypto settlement failed.                              |
| `fiatSettlement.pending`      | Fiat settlement initiated, waiting to be processed.    |
| `fiatSettlement.processing`   | Fiat settlement is being sent to your bank account.    |
| `fiatSettlement.success`      | Fiat settlement completed. Funds in your bank account. |
| `fiatSettlement.failed`       | Fiat settlement failed.                                |

### Swap Events

Triggered when a token swap is performed on collected funds.

| Event             | Description                               |
| ----------------- | ----------------------------------------- |
| `swap.pending`    | A token swap was initiated and is queued. |
| `swap.processing` | Token swap is being executed.             |
| `swap.success`    | Token swap completed successfully.        |
| `swap.failed`     | Token swap failed.                        |

### Refund Events

Triggered when a collected payment is refunded to the customer.

| Event                               | Description                                                |
| ----------------------------------- | ---------------------------------------------------------- |
| `cryptoCollectionRefund.pending`    | Crypto refund initiated, waiting to be processed.          |
| `cryptoCollectionRefund.processing` | Crypto refund is being sent back to the customer's wallet. |
| `cryptoCollectionRefund.success`    | Crypto refund completed. Funds returned to customer.       |
| `cryptoCollectionRefund.failed`     | Crypto refund failed.                                      |
| `fiatCollectionRefund.pending`      | Fiat refund initiated, waiting to be processed.            |
| `fiatCollectionRefund.processing`   | Fiat refund is being sent to the customer's bank account.  |
| `fiatCollectionRefund.success`      | Fiat refund completed. Funds returned to customer's bank.  |
| `fiatCollectionRefund.failed`       | Fiat refund failed.                                        |

### Permanent Wallet Deposit Events

Triggered when a deposit is received on a customer's permanent blockchain address (generated via `POST /v1/blockchain-accounts/create`).

| Event                               | Description                                                           |
| ----------------------------------- | --------------------------------------------------------------------- |
| `permanentWalletDeposit.pending`    | A deposit was detected on the permanent wallet address and is queued. |
| `permanentWalletDeposit.processing` | Deposit is being confirmed on-chain.                                  |
| `permanentWalletDeposit.success`    | Deposit fully confirmed and credited.                                 |
| `permanentWalletDeposit.failed`     | Deposit failed or could not be processed.                             |

***

## Best Practices

**Always return HTTP 200.** IvoryPay considers any non-200 response a delivery failure and will retry. Return `200` immediately, then process the event asynchronously.

**Verify the signature.** Reject any request where the `x-ivorypay-signature` doesn't match. This protects against spoofed requests.

**Handle duplicates.** Webhooks may be delivered more than once. Use the `reference` field as an idempotency key to avoid processing the same event twice.

**Subscribe only to what you need.** Only subscribe to events relevant to your integration. This keeps your handler logic focused and your logs clean.

**Use TEST environment first.** Configure a webhook in the TEST environment and validate all your event handlers before switching to LIVE.
