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

# Webhook events and signature verification

> Event payloads Orbit delivers to your webhook endpoints, and how to verify the HMAC-SHA256 signature on every delivery.

Every delivery is an HTTPS `POST` with a JSON body and three headers:

| Header                      | Contents                                                                                                 |
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
| `x-orbit-webhook-signature` | `sha256=<hex>` — HMAC-SHA256 of `` `${timestamp}.${rawBody}` `` using your endpoint's `whsec_...` secret |
| `x-orbit-webhook-timestamp` | Unix timestamp used in the signature                                                                     |
| `x-orbit-webhook-event-id`  | Unique delivery ID — deduplicate on this; retries reuse it                                               |

Failed deliveries are retried up to 6 times with backoff (10-second timeout per attempt). Respond with any `2xx` quickly and process asynchronously. An endpoint that fails 20 consecutive deliveries is automatically disabled.

## Verifying signatures

Compute the signature from the raw request body, compare in constant time, and reject deliveries with a timestamp older than five minutes. This protects you from replayed deliveries:

```javascript JavaScript theme={"dark"}
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

function verifyOrbitWebhook(secret, headers, rawBody) {
  const timestamp = headers["x-orbit-webhook-timestamp"];
  const signature = headers["x-orbit-webhook-signature"];
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
  const expected = `sha256=${createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex")}`;
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

```python Python theme={"dark"}
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300

def verify_orbit_webhook(secret: str, headers: dict, raw_body: bytes) -> bool:
    timestamp = headers["x-orbit-webhook-timestamp"]
    signature = headers.get("x-orbit-webhook-signature", "")
    try:
        if abs(time.time() - float(timestamp)) > TOLERANCE_SECONDS:
            return False
    except ValueError:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

## `profile.updated`

Sent when a [watcher](/concepts/watchers) run adds new events to a profile. The delivery carries the events themselves: `events` holds the new timeline entries in the exact shape [profile reads](/api/enrich/read-profile#profile-sections) return in `sections.eventsTimeline` — title, description, `date` with `datePrecision` and `dateBasis`, and the `sources` behind the event — so your handler has everything without a follow-up read, and a later profile read returns identical objects. `newEventCount` is the number of new events; `changes` lists the source-level deltas. `runsCovered` names how many completed runs the delivery folds together (`runId` is the newest), and `updatedSections` lists `events_timeline` when the delivery carries events.

```json theme={"dark"}
{
  "orbitId": "83f1b564-3607-4618-b8c9-a886410ceb32",
  "watcherId": "6f2f1f37-8f9f-4a51-9a2f-2f1cbb1a9d20",
  "runId": "deep-search-v3-refresh-83f1b564-...-20260716",
  "runsCovered": 1,
  "newEventCount": 1,
  "events": [
    {
      "kind": "event",
      "title": "Example announces new venture",
      "description": "Example Person announced a new venture in July 2026...",
      "date": "2026-07",
      "datePrecision": "month",
      "dateBasis": "stated",
      "eventKey": "example-announces-new-venture",
      "updateCount": 1,
      "lastUpdatedAt": "2026-07-16T21:07:46.000Z",
      "sources": [{ "link": "https://news.example.com/story", "name": "Example News" }],
      "changes": [
        {
          "sourceLink": "https://news.example.com/story",
          "sourceName": "Example News",
          "changeKind": "new_source",
          "summary": "Coverage of the announcement..."
        }
      ]
    }
  ],
  "updatedSections": ["events_timeline"],
  "changes": [
    {
      "link": "https://x.com/example",
      "platform": "twitter",
      "change_kind": "social_post",
      "new_posts": 3,
      "summary": "3 new twitter post(s)"
    },
    {
      "link": "https://news.example.com/story",
      "change_kind": "new_source",
      "title": "Example announces new venture",
      "summary": "Coverage of the announcement..."
    }
  ]
}
```

`events` carries at most 10 entries and `changes` at most 25; the full timeline lives on [profile reads](/api/enrich/read-profile#profile-sections) and the full change set on [watcher runs](/api/watchers/runs).

<Note>
  Slack incoming-webhook URLs receive a formatted Slack message (new-event
  count and a short change list) instead of this JSON. Use one for a
  notifications channel.
</Note>

## Other events

`company.thesis.changed`, `company.alert`, and `portfolio.changed` are delivered to endpoints subscribed to them and follow the same signing scheme. Their payloads are documented with the company intelligence APIs.
