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

# Webhooks

Instead of polling, subscribe an HTTPS endpoint in the console at **Developer → Webhooks** to the lifecycle events `dataquality.<usecase>.sent`, `.done` and `.error` (batch jobs use the `<usecase>-batch` slug). Events are **notify-only** — they carry the `job_id`, the `usecase` and the outcome, never the result itself; fetch the result from the poll URL. Every delivery is signed: `Delpha-Signature: t=<unix-seconds>,v1=<hex>` where `v1` is HMAC-SHA256 of `"<t>.<raw request body>"` under the endpoint's `whsec_…` signing secret. Verify the signature, reject stale timestamps, and deduplicate on the event `id` — it is derived from the job and the event type, so a redelivery repeats the same id. A timeout, a 429 or a 5xx from your endpoint is retried up to 3 attempts with exponential backoff (0.5 s, then 1 s); any other 4xx is not retried. The full event shape is the `WebhookEvent` schema and the three entries under `webhooks`.

#### Event families

There are **16 event families** — one per operation that admits work, each publishing the 3 lifecycle events `sent`, `done`, `error`. 16 of them are current. The 0 marked deprecated below belong to the deprecated generic LinkedIn operations: they keep delivering for endpoints already subscribed to them, and a new subscription should use the profile and company-page families instead.

| Use case slug                 | Events                                                                                                                                          | State   |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `email`                       | `dataquality.email.sent`, `dataquality.email.done`, `dataquality.email.error`                                                                   | current |
| `website`                     | `dataquality.website.sent`, `dataquality.website.done`, `dataquality.website.error`                                                             | current |
| `phone`                       | `dataquality.phone.sent`, `dataquality.phone.done`, `dataquality.phone.error`                                                                   | current |
| `name`                        | `dataquality.name.sent`, `dataquality.name.done`, `dataquality.name.error`                                                                      | current |
| `legal-id`                    | `dataquality.legal-id.sent`, `dataquality.legal-id.done`, `dataquality.legal-id.error`                                                          | current |
| `linkedin-profile`            | `dataquality.linkedin-profile.sent`, `dataquality.linkedin-profile.done`, `dataquality.linkedin-profile.error`                                  | current |
| `linkedin-company-page`       | `dataquality.linkedin-company-page.sent`, `dataquality.linkedin-company-page.done`, `dataquality.linkedin-company-page.error`                   | current |
| `ultimate`                    | `dataquality.ultimate.sent`, `dataquality.ultimate.done`, `dataquality.ultimate.error`                                                          | current |
| `email-batch`                 | `dataquality.email-batch.sent`, `dataquality.email-batch.done`, `dataquality.email-batch.error`                                                 | current |
| `website-batch`               | `dataquality.website-batch.sent`, `dataquality.website-batch.done`, `dataquality.website-batch.error`                                           | current |
| `phone-batch`                 | `dataquality.phone-batch.sent`, `dataquality.phone-batch.done`, `dataquality.phone-batch.error`                                                 | current |
| `name-batch`                  | `dataquality.name-batch.sent`, `dataquality.name-batch.done`, `dataquality.name-batch.error`                                                    | current |
| `legal-id-batch`              | `dataquality.legal-id-batch.sent`, `dataquality.legal-id-batch.done`, `dataquality.legal-id-batch.error`                                        | current |
| `linkedin-profile-batch`      | `dataquality.linkedin-profile-batch.sent`, `dataquality.linkedin-profile-batch.done`, `dataquality.linkedin-profile-batch.error`                | current |
| `linkedin-company-page-batch` | `dataquality.linkedin-company-page-batch.sent`, `dataquality.linkedin-company-page-batch.done`, `dataquality.linkedin-company-page-batch.error` | current |
| `ultimate-batch`              | `dataquality.ultimate-batch.sent`, `dataquality.ultimate-batch.done`, `dataquality.ultimate-batch.error`                                        | current |

#### Event shape

A lifecycle event delivered to a subscribed endpoint. Notify-only: it names the job and its outcome, never the result — fetch that from the job's poll URL. Deduplicate on `id`, which is derived from the job and the event type and therefore repeats on redelivery.

| Field      | Type      | Always present | Meaning                                                                                                                                                                                           |
| ---------- | --------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`       | `string`  | yes            | Stable event id — identical on every redelivery of the same event.                                                                                                                                |
| `type`     | `string`  | yes            | The event scope: `dataquality.<usecase>.<sent\|done\|error>`, where `<usecase>` is the route slug (`email`, `legal-id`, `linkedin-profile`, …) or the slug with a `-batch` suffix for batch jobs. |
| `created`  | `integer` | yes            | Unix time (seconds) the delivery was signed — the `t` of `Delpha-Signature`.                                                                                                                      |
| `data`     | `object`  | yes            | What happened, and to which job.                                                                                                                                                                  |
| `metadata` | `object`  | no             | The free-form metadata configured on the endpoint, echoed verbatim.                                                                                                                               |

A delivery of `dataquality.{usecase}.sent`:

```json
{
  "id": "evt_e704df05-b620-5efb-87c9-a2169e755cbc",
  "type": "dataquality.email.sent",
  "created": 1788870000,
  "data": {
    "job_id": "0c8f0d6e-5b1a-4d4e-9f0e-1234567890ab",
    "usecase": "email",
    "status": "sent"
  },
  "metadata": {}
}
```

#### Verifying a delivery

Every delivery carries `Delpha-Signature`. `t=<unix-seconds>,v1=<hex>` — `v1` is HMAC-SHA256 of `"<t>.<raw request body>"` under the endpoint's `whsec_…` signing secret (shown once at creation in the console). Recompute it over the raw bytes, compare in constant time, and reject a `t` older than a few minutes.

Every delivery also sends `User-Agent: Delpha-Webhooks/1.0`.

Both snippets below reject a delivery whose `t` is more than five minutes old. That tolerance is a receiver-side choice, not a value Delpha enforces — widen or narrow it to suit your own clock skew.

Python:

```python
import hashlib
import hmac
import time

# 5 minutes - a receiver-side choice, not a value Delpha enforces.
TOLERANCE_SECONDS = 300


def verify(secret: str, header: str, body: bytes) -> bool:
    """True when `header` is a fresh, valid Delpha-Signature for `body`."""
    parts = dict(piece.split("=", 1) for piece in header.split(",") if "=" in piece)
    timestamp, signature = parts.get("t"), parts.get("v1")
    if not timestamp or not signature:
        return False
    try:
        age = abs(time.time() - int(timestamp))
    except ValueError:
        return False
    if age > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}.".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Node.js:

```javascript
const crypto = require("node:crypto");

// 5 minutes - a receiver-side choice, not a value Delpha enforces.
const TOLERANCE_SECONDS = 300;

/** True when `header` is a fresh, valid Delpha-Signature for `body` (a Buffer). */
function verify(secret, header, body) {
  const parts = new Map(
    header
      .split(",")
      .filter((piece) => piece.includes("="))
      .map((piece) => [piece.slice(0, piece.indexOf("=")), piece.slice(piece.indexOf("=") + 1)]),
  );
  const timestamp = parts.get("t");
  const signature = parts.get("v1");
  if (!timestamp || !signature) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(body)
    .digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

#### Acknowledging

**Acknowledge with any 2xx within 10 seconds** — that is the delivery timeout, and a slower handler is recorded as a timeout however it ends. Do the work asynchronously and answer first. A redirect is **not** followed: a 3xx counts as a failed delivery. A timeout, a 429 or a 5xx is retried up to 3 attempts in total, with exponential backoff (0.5 s, then 1 s) — so a failing endpoint is given up on within about two seconds, not minutes; any other 4xx is not retried at all. Delivery is therefore **at-least-once with no server-side deduplication**: the same event can arrive more than once and your handler must be idempotent on `id`. Each subscribed endpoint is delivered to independently. Endpoints are managed in the console at <https://app.delpha.ai/developer/webhooks>.

| Your response | Delpha's behaviour                                |
| ------------- | ------------------------------------------------- |
| `2XX`         | Acknowledged — the event is not delivered again.  |
| anything else | Not acknowledged — see the delivery policy above. |

Endpoints, their signing secrets and their delivery history are managed at [**Developer → Webhooks**](https://app.delpha.ai/developer/webhooks).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://help.delpha.io/delpha-api-and-mcp/api/webhooks.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
