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

# Webhooks

> Configure webhooks on search creation and parse delivery payloads.

Osintly can push a lightweight search completion notification to your server.

You configure the webhook when creating a search (`POST /search`) with the `delivery.webhook` object:

```json theme={null}
{
  "query": {
    "type": "Email Address",
    "value": "target@example.com"
  },
  "delivery": {
    "webhook": {
      "url": "https://client.example/webhooks/osintly",
      "secret": "super-secret-webhook-key"
    }
  }
}
```

## When a webhook is sent

A webhook is attempted after the search reaches a terminal status:

* `finished`
* `error`

If no `delivery.webhook.url` is provided at search creation, no callback is sent.

## What your webhook endpoint receives

Your endpoint receives a JSON payload with the following shape:

```json theme={null}
{
  "searchId": "4a8cbb96-70df-4bd6-8f4a-9c0bf5ffde1f",
  "status": "finished",
  "finishedAt": "2026-03-03T14:21:30.120Z",
  "search": {
    "id": "4a8cbb96-70df-4bd6-8f4a-9c0bf5ffde1f",
    "value": "target@example.com",
    "type": "Email Address",
    "options": {
      "include_registered_accounts": true
    },
    "created_at": "2026-03-03T14:20:10.010Z"
  },
  "summary": {
    "module_results": 24,
    "leak_sources": 2,
    "breached_accounts": 1,
    "registered_accounts": 7
  },
  "links": {
    "state": "https://api.osint.ly/search/4a8cbb96-70df-4bd6-8f4a-9c0bf5ffde1f",
    "results": "https://api.osint.ly/search/4a8cbb96-70df-4bd6-8f4a-9c0bf5ffde1f/results",
    "byok": "https://api.osint.ly/search/4a8cbb96-70df-4bd6-8f4a-9c0bf5ffde1f/byok",
    "leaks": "https://api.osint.ly/search/4a8cbb96-70df-4bd6-8f4a-9c0bf5ffde1f/results/leaks"
  }
}
```

### Field reference

| Field                         | Type                    | Description                                             |
| ----------------------------- | ----------------------- | ------------------------------------------------------- |
| `searchId`                    | `string (uuid)`         | Search identifier                                       |
| `status`                      | `"finished" \| "error"` | Final normalized status sent by webhook delivery        |
| `finishedAt`                  | `string (date-time)`    | Delivery timestamp                                      |
| `search.id`                   | `string (uuid)`         | Original search id                                      |
| `search.value`                | `string`                | Original search value                                   |
| `search.type`                 | `string`                | Original search type                                    |
| `search.options`              | `object \| null`        | Original request options                                |
| `search.created_at`           | `string (date-time)`    | Original search creation timestamp                      |
| `summary.module_results`      | `integer`               | Count of stored module and normalized BYOK result cards |
| `summary.leak_sources`        | `integer`               | Count of successful persisted leak providers            |
| `summary.breached_accounts`   | `integer`               | Count of breached account entries                       |
| `summary.registered_accounts` | `integer`               | Count of registered account entries                     |
| `links.state`                 | `string (uri)`          | Retrieve the search state                               |
| `links.results`               | `string (uri)`          | Retrieve normalized search results                      |
| `links.byok`                  | `string (uri)`          | Retrieve BYOK provider details                          |
| `links.leaks`                 | `string (uri)`          | Retrieve leak source overview                           |

## Verify authenticity (recommended)

When you define `delivery.webhook.secret`, verify a signature header on your endpoint.

Use this signing format:

* Algorithm: `HMAC-SHA256`
* Signed input: `timestamp + raw_request_body`
* Output format: `sha256=<hex_digest>`

### Example verifier (Node.js)

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

function sign(secret, timestamp, rawBody) {
  const digest = crypto
    .createHmac("sha256", secret)
    .update(timestamp + rawBody)
    .digest("hex");
  return `sha256=${digest}`;
}

function verifySignature({ secret, timestamp, rawBody, receivedSignature }) {
  const expected = sign(secret, timestamp, rawBody);
  if (expected.length !== receivedSignature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(receivedSignature, "utf8")
  );
}
```

## Receiver checklist

* Return `2xx` quickly after validation
* Process heavy work asynchronously
* Keep handlers idempotent by deduplicating on `searchId`
* Log and monitor non-2xx responses from your endpoint

<Tip>
  Use the webhook as a completion signal, then fetch the full data you need from the links included in the payload.
</Tip>
