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

# Set Up and Handle Diameter Webhooks

> Diameter sends real-time event notifications to your endpoint for payments, end users, accounts, and counterparties. Learn how to subscribe and verify.

Diameter uses webhooks to push real-time notifications to your application when important events occur — a payment completes, an end user's status changes, a new account is created, and more. Rather than polling the API repeatedly, you register an HTTPS endpoint and Diameter delivers a signed `POST` request to it every time a subscribed event fires. This guide covers subscription setup, signature verification, payload handling, and recovery from missed events.

### Create a webhook subscription

Send a `POST` to `/v1/webhook-subscriptions` with a name, your HTTPS callback URL, and the list of event types you want to receive.

```bash cURL theme={null}
curl -X POST https://api.sandbox.diameterpay.com/v1/webhook-subscriptions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "x-idempotency-key: 550e8400-e29b-41d4-a716-446655440030" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Payment Notifications",
    "url": "https://api.example.com/webhooks",
    "eventTypes": [
      "PAYMENT_WIRE_OUTBOUND_COMPLETED",
      "PAYMENT_WIRE_OUTBOUND_FAILED",
      "END_USER_UPDATED"
    ]
  }'
```

```json Response theme={null}
{
  "id": "sub-3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Payment Notifications",
  "url": "https://api.example.com/webhooks",
  "eventTypes": [
    "PAYMENT_WIRE_OUTBOUND_COMPLETED",
    "PAYMENT_WIRE_OUTBOUND_FAILED",
    "END_USER_UPDATED"
  ],
  "secret": "whsec_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
  "createdAt": "2024-01-15T09:00:00Z"
}
```

> [!WARNING]
>
> The `secret` value is returned **only once** at subscription creation time. Copy it immediately and store it securely (for example, as an environment variable). You cannot retrieve it again — if you lose it, you must delete the subscription and create a new one.

**Endpoint requirements:**

* Must use `HTTPS` — plain `HTTP` URLs are rejected
* Must not redirect — point directly to your final handler URL
* Must respond within **3 seconds** with a `2xx` status code

### Verify webhook signatures

Every inbound webhook request includes an `X-Diameter-Signature` header. Always verify this signature before processing the payload to confirm the request genuinely originated from Diameter and hasn't been tampered with.

The signature format is: `t=TIMESTAMP,v1=HMAC_HEX`

Diameter computes the HMAC as `HMAC-SHA256(secret, "{timestamp}.{rawBody}")`.

```javascript Node.js theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(payload, signatureHeader, secret, tolerance = 300) {
  const components = {};
  signatureHeader.split(',').forEach((part) => {
    const [key, value] = part.split('=');
    if (key && value) components[key] = value;
  });

  const { t: timestamp, v1: signature } = components;
  if (!timestamp || !signature || !secret) return false;

  // Reject events older than `tolerance` seconds (default: 5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > tolerance) return false;

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex')
    .toLowerCase();

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js usage example
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const signatureHeader = req.headers['x-diameter-signature'];
  const isValid = verifyWebhookSignature(
    req.body.toString(),
    signatureHeader,
    process.env.DIAMETER_WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // Process event asynchronously...
  res.status(200).send('OK');
});
```

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

def verify_webhook_signature(payload: str, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    components = {}
    for part in signature_header.split(','):
        key, _, value = part.partition('=')
        if key and value:
            components[key] = value

    timestamp = components.get('t')
    signature = components.get('v1')

    if not timestamp or not signature or not secret:
        return False

    now = int(time.time())
    if abs(now - int(timestamp)) > tolerance:
        return False

    expected = hmac.new(
        secret.encode('utf-8'),
        f'{timestamp}.{payload}'.encode('utf-8'),
        hashlib.sha256
    ).hexdigest().lower()

    return hmac.compare_digest(signature, expected)
```

> [!NOTE]
>
> Use your language's constant-time comparison function (e.g. `crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python) when comparing signatures. Standard string equality is vulnerable to timing attacks.

### Handle the webhook payload

Every Diameter webhook follows the same envelope structure. Parse the `eventType` to determine how to handle the `data` object.

```json Webhook payload structure theme={null}
{
  "eventId": "evt-7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "eventType": "PAYMENT_WIRE_OUTBOUND_COMPLETED",
  "contextId": "pay-9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "contextType": "PAYMENT",
  "subscriptionId": "sub-3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "paymentId": "pay-9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "endToEndId": "WIRE-2024-001",
    "amount": "500.00",
    "currency": "USD"
  },
  "timestamp": "2024-01-15T10:30:00Z"
}
```

| Field | Description |
| - | - |
| `eventId` | Unique ID for this delivery — use for deduplication |
| `eventType` | The event that occurred (see table below) |
| `contextId` | UUID of the resource that changed (payment, end user, etc.) |
| `contextType` | Resource type: `PAYMENT`, `END_USER`, `ACCOUNT`, `COUNTERPARTY` |
| `subscriptionId` | The subscription that matched this event |
| `data` | Event-specific payload — varies by `eventType` |
| `timestamp` | ISO 8601 time the event was emitted |

> [!TIP]
>
> Store the `eventId` in your database when you process an event. If Diameter retries the delivery, check for the `eventId` first and skip processing if you've already handled it.

### Respond promptly and process asynchronously

Your endpoint must return a `2xx` response within **3 seconds** of receiving the request. If Diameter does not receive a `2xx` within this window, it treats the delivery as failed and schedules a retry.

To meet this requirement, immediately acknowledge the request and hand the payload off to a background queue for processing:

```javascript Node.js — async processing pattern theme={null}
app.post('/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  // 1. Verify signature synchronously
  const isValid = verifyWebhookSignature(
    req.body.toString(),
    req.headers['x-diameter-signature'],
    process.env.DIAMETER_WEBHOOK_SECRET
  );
  if (!isValid) return res.status(401).send('Unauthorized');

  // 2. Respond immediately
  res.status(200).send('OK');

  // 3. Enqueue for async processing
  const event = JSON.parse(req.body);
  await eventQueue.push(event);
});
```

***

## Supported event types

| Event | Trigger |
| - | - |
| `PAYMENT_WIRE_OUTBOUND_SCREENING_REQUESTED` | Payment is under compliance review |
| `PAYMENT_WIRE_OUTBOUND_SCREENING_PASSED` | Compliance review approved the payment |
| `PAYMENT_WIRE_OUTBOUND_COMPLETED` | Outbound wire settled successfully |
| `PAYMENT_WIRE_OUTBOUND_FAILED` | Outbound wire could not be processed |
| `PAYMENT_WIRE_INBOUND_POSTED` | An inbound wire was credited to an account |
| `END_USER_CREATED` | A new end user record was created |
| `END_USER_UPDATED` | An end user's status or data changed |
| `ACCOUNT_CREATED` | A new virtual account was provisioned |
| `COUNTERPARTY_CREATED` | A new counterparty was created or became active at a bank |

***

## Retry policy

If your endpoint returns a non-`2xx` response or times out, Diameter retries the delivery using exponential backoff — up to **8 retries over approximately 6 hours**. Each retry uses the same `eventId`, so your idempotency check will correctly skip duplicate processing.

***

## Recover missed events

If your endpoint was down for an extended period, use the events search API to find undelivered events and replay them.

### Find undelivered events

```bash cURL theme={null}
curl -X GET "https://api.sandbox.diameterpay.com/v1/webhooks/events/search?deliveryStatus=NOT_DELIVERED" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

### Process and acknowledge each event

After processing each event in your system, mark the batch as delivered so Diameter stops retrying:

```bash cURL theme={null}
curl -X POST https://api.sandbox.diameterpay.com/v1/webhooks/events/bulk-mark-delivered \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "eventIds": [
      "evt-7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "evt-9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
    ]
  }'
```

***

## Security best practices

Follow these practices to keep your webhook endpoint secure:

* **Always verify the signature** — reject any request that fails signature validation with a `401` response
* **Check timestamp freshness** — reject events older than 5 minutes to prevent replay attacks
* **Implement idempotency** — deduplicate on `eventId` before updating your database
* **Use HTTPS only** — never accept webhook deliveries over plain HTTP
* **Keep your secret confidential** — store it as an environment variable, never hardcode it or commit it to source control


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