# Webhooks

DISIFY can send your server a signed JSON notification when a [bulk validation](https://docs.disify.com/api/bulk.md) completes. Delivery is best-effort, so retain polling as a fallback.

Webhooks are configured per account, so they require an [API account](https://docs.disify.com/guide/authentication.md) — they aren't available for anonymous usage.

## Setup

1. Add a public **HTTPS** URL in [Webhooks](https://disify.com/account/webhooks). Local/private addresses are not accepted, and redirects are not followed.
2. Save the generated signing secret in your server configuration. The full secret is shown once; regenerating it replaces the old secret immediately.
3. Select the events you want. An empty selection subscribes to all events; remove the URL to disable delivery.

## Events

| Event | When it fires |
| --- | --- |
| `test.ping` | Sent from [Webhooks](https://disify.com/account/webhooks) to verify the endpoint works. |
| `bulk.completed` | A synchronous or asynchronous [bulk validation](https://docs.disify.com/api/bulk.md) has completed. |

## Payload

Every delivery is an HTTP `POST` with a JSON body in this envelope:

```json
{
  "event": "bulk.completed",
  "timestamp": "2026-07-13T12:00:00+00:00",
  "data": {
    "session": "d117271ce938bf91bc718f6cfb7954de",
    "stats": {
      "total": 8000,
      "unique": 8000,
      "invalid_format": 12,
      "invalid_dns": 40,
      "disposable": 156,
      "valid": 7792,
      "session": "d117271ce938bf91bc718f6cfb7954de"
    }
  }
}
```

- `event` — the event name (also sent as the `X-Disify-Event` header).
- `timestamp` — ISO 8601 time the event was dispatched.
- `data` — event-specific payload. `bulk.completed` includes the `session` and [summary statistics](https://docs.disify.com/api/bulk.md#bulk-response). The address list is not embedded; retrieve it through `GET /api/view/{session}` within the 15-minute session lifetime, using the submitting backend or account.

## Request headers

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `X-Disify-Event` | The event name, e.g. `bulk.completed` |
| `X-Disify-Signature` | HMAC-SHA256 signature of the raw request body (see below) |
| `User-Agent` | `Disify-Webhook/1.0` |

## Verifying signatures

`X-Disify-Signature` is a lowercase hexadecimal **HMAC-SHA256** of the raw request body, keyed with your signing secret. Pass the exact bytes, the header value (or an empty string if absent), and the server-configured secret to a verifier:


**JavaScript**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verifySignature(rawBody, signature, secret) {
  if (!secret || typeof signature !== "string" ||
      signature.length !== 64 || !/^[0-9a-f]{64}$/.test(signature)) {
    return false;
  }
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}
```

**PHP**

```php
function verifySignature(string $rawBody, string $signature, string $secret): bool
{
    if ($secret === '' || !preg_match('/\A[0-9a-f]{64}\z/', $signature)) {
        return false;
    }
    return hash_equals(hash_hmac('sha256', $rawBody, $secret), $signature);
}
```

**Python**

```python
import hashlib
import hmac
import re

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    if not secret or not re.fullmatch(r"[0-9a-f]{64}", signature):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```


Reject a failed signature before processing the event. Configure your framework to preserve the raw body; JSON re-encoding changes the signed bytes. After verification, parse and validate the payload. Use its signed timestamp to enforce an appropriate freshness window, and deduplicate `bulk.completed` by event name and session.

## Delivery behavior

- The HTTP delivery timeout is **10 seconds**. Verify the signature, durably enqueue the event, then return `2xx`; process longer work asynchronously.
- There are **no automatic retries**. Check the delivery log in [Webhooks](https://disify.com/account/webhooks) for status and errors; entries older than 7 days are pruned daily.
- If a notification is missed, poll the session returned by the original bulk request. Temporary results still expire after 15 minutes.

## Testing

Send a `test.ping` from [Webhooks](https://disify.com/account/webhooks) and check the delivery log. Ensure your event selection includes `test.ping` (or all events). In automated tests, cover valid, tampered, missing-signature, and duplicate events with local fixtures.

Source: https://docs.disify.com/api/webhooks.html
