# Publication webhooks

Receive signed publication lifecycle events and process delivery retries safely.

Beacon webhooks send publication lifecycle changes to your HTTPS endpoint.

Create an endpoint in **Settings > Developer > Webhooks**. Select only the events that your application needs. Beacon shows the signing secret one time. Store it in a secret manager.

## Supported events

| Event                     | Destination status    | Meaning                                                         |
| ------------------------- | --------------------- | --------------------------------------------------------------- |
| `post.scheduled`          | `scheduled`           | Beacon scheduled or safely requeued the destination.            |
| `post.published`          | `published`           | Beacon confirmed publication.                                   |
| `post.partial`            | `partially_published` | Beacon published at least one part, but work remains.           |
| `post.failed`             | `failed`              | Beacon has a safe failed result.                                |
| `post.delayed`            | `delayed`             | Beacon delayed a safe retry.                                    |
| `post.reconnect_required` | `reconnect_required`  | The channel owner must reconnect the provider.                  |
| `post.recovery_required`  | `checking`            | Beacon cannot prove the provider result. Do not retry the post. |
| `post.cancelled`          | `cancelled`           | Beacon cancelled the destination before dispatch.               |
| `post.expired`            | `expired`             | Beacon stopped unfinished work after its allowed time.          |

The event name `post.recovery_required` maps to the customer status `checking`.

## Event body

Beacon sends one JSON envelope for one post destination.

```json
{
  "createdAt": "2026-08-28T10:00:00.000Z",
  "data": {
    "destination": {
      "channelId": "c40a49a6-2f5b-4b03-8ca8-ddf3972d38b8",
      "provider": "linkedin",
      "publishAt": "2026-08-28T10:00:00.000Z",
      "releaseUrl": "https://www.linkedin.com/feed/update/example",
      "status": "published"
    },
    "post": {
      "postId": "8420cf47-6086-43f9-b37b-c6b26ef81a2d"
    }
  },
  "id": "647ef738-7bfc-467a-a7a5-3738f36f6dad",
  "type": "post.published"
}
```

A delayed event can include `checkAgainAt`. A failed or blocked event can include a safe `error` with `code`, `message`, `provider`, and `retryable`.

Beacon does not include internal target IDs, publication-unit IDs, provider external IDs, provider request IDs, raw provider responses, or credentials.

## Verify the signature

Verify the exact raw request body before you parse JSON. Do not reformat or reserialize the body first.

Beacon sends these headers:

```text
X-Beacon-Event-Id
X-Beacon-Event-Type
X-Beacon-Timestamp
X-Beacon-Signature: t=UNIX_SECONDS,v1=HEX_HMAC
```

Beacon calculates HMAC-SHA256 over this text:

```text
<timestamp>.<rawBody>
```

The TypeScript SDK verifies the signature and rejects old timestamps:

```ts
import {
  verifyBeaconWebhookSignature,
  type BeaconWebhookEvent,
} from "@beacon/sdk";

const rawBody = await request.text();
const verified = await verifyBeaconWebhookSignature({
  rawBody,
  secret: process.env.BEACON_WEBHOOK_SECRET!,
  signatureHeader: request.headers.get("x-beacon-signature"),
});

if (!verified) {
  return new Response("Invalid signature", { status: 401 });
}

const event = JSON.parse(rawBody) as BeaconWebhookEvent;
```

The default timestamp tolerance is five minutes. Keep the server clock accurate. Compare signatures in constant time. Never log the secret, signature, or full body.

## Acknowledge an event

Return an HTTP status from `200` through `299` after you store or process the event. Keep the handler fast. Move long work to your own queue.

Beacon does not follow redirects. Use a stable credential-free HTTPS address.

## Retry behavior

Beacon keeps the same event ID and exact body across automatic delivery attempts. It creates a new timestamp and signature for each attempt.

Beacon retries network errors, `408`, `425`, `429`, and `500` through `599`. Other `400` responses fail permanently.

Beacon makes at most five automatic attempts. The nominal waits after attempts are 1, 2, 4, and 8 seconds, plus worker scheduling time. Beacon honors `Retry-After` for a retryable response and limits that wait to 60 seconds.

## Ordering and duplicate handling

Beacon does not guarantee global event order. Separate workers or endpoints can observe events in different orders.

Store the event `id` with a unique constraint. If you receive the same ID again, return a successful response and do not repeat your side effect.

Use `createdAt` to understand when Beacon recorded the change. When event order affects a decision, read the current post through `GET /agent/v1/posts/{postId}` before you act.
