# Webhooks

Get a signed HTTPS request when a post is published or fails, and verify it.


Instead of polling `GET /posts/:id/results`, register an endpoint and CreatorHyve calls it when something happens. Add endpoints under **Settings, then Developer, then Webhooks**, or with `POST /webhooks`.

## Events

| Event | When |
|---|---|
| `post.published` | A post went live on one platform. `data` has `post_id`, `platform_post_id`, `platform`, `url`, `published_at`. |
| `post.failed` | A post could not be published and will not be retried automatically. `data` has `post_id`, `platform_post_id`, `platform` and `error` (`code`, `message`, `action`). |
| `account.needs_reconnect` | A connected account lost access and must be reconnected. `data` has `account_id`, `platform`, `username`. Sent once per failure. |

Every delivery is a `POST` with a JSON body:

```json
{
  "id": "evt_7f0c3a52-0c93-4f4f-9a2e-0b1f5f6d2a10",
  "type": "post.published",
  "created_at": "2030-01-15T09:00:04.512Z",
  "data": {
    "post_id": "c8f1e4b2-...",
    "platform_post_id": "a3d2...",
    "platform": "bluesky",
    "url": "https://bsky.app/profile/you.bsky.social/post/3k...",
    "published_at": "2030-01-15T09:00:04.100Z"
  }
}
```

## Requirements for your endpoint

- A public `https` URL (no IP addresses, no custom port, no credentials in the URL).
- Answer with any `2xx` status within about 30 seconds. Do the heavy work afterwards.
- Treat deliveries as at-least-once: use the event `id` to ignore repeats.

## Verifying the signature

Each delivery carries `X-CreatorHyve-Signature: t=<unix seconds>,v1=<hex>`. The `v1` value is the HMAC-SHA256 of `"<t>.<raw body>"` with your endpoint's signing secret (`whsec_...`, shown once when you create the endpoint). Reject deliveries whose timestamp is more than five minutes old.

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

export function verify(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

```python
import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

Verify against the **raw** request body, before any JSON parsing.

## Retries

If your endpoint does not answer `2xx`, CreatorHyve retries after 1 minute, 5 minutes and 30 minutes, then gives up on that delivery. An endpoint that keeps failing is paused after 10 failed deliveries; resume it in the dashboard once it is fixed. Use the **Test** button (or `POST /webhooks/:id/test`) to send a `webhook.test` event.

