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

# Operator: webhooks

> Verifying signed deliveries, answering duplicates correctly, and keeping your endpoint out of the auto-disable path.

Webhook deliveries carry three headers:

```text theme={null}
X-Omega-Signature: t=<unix-seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
X-Omega-Event:     operator.task.succeeded.v1
X-Omega-Delivery:  <uuid>
```

Endpoint secrets are returned once, in the `POST /v1/webhook_endpoints`
response. There is no reveal route; a lost secret is replaced by creating a
new endpoint. Subscribe with `event_types` from the published catalogue; an
unknown type is `400 unknown_event_type` rather than a subscription that
silently receives nothing.

## Four verification rules

The TypeScript client's `WebhookVerifier` enforces all four. If you verify by
hand, so must you.

1. **Verify the raw bytes.** The signed string is `"<t>.<raw body>"`. A
   framework that parsed the JSON and re-serialized it has changed the bytes
   and the MAC will not match. Use `express.raw`, `fastify` with `rawBody`,
   `await request.text()` in Workers, or the request stream in `node:http`.
2. **Compare in constant time.** A `===` on hex strings leaks the correct MAC
   one byte at a time.
3. **Bound the timestamp in both directions** (default plus or minus 300
   seconds). `t` is inside the MAC so it cannot be re-stamped, but a captured
   delivery can be re-sent, and a one-sided check would let a single forged
   far-future stamp replay forever.
4. **Dedupe on the signed envelope's `id`, never on a header.** The signature
   covers the body and no header, so `X-Omega-Delivery` is attacker-controlled
   on a captured delivery. The verifier returns the signed `eventId` for
   exactly this; `deliveryId` is echoed for logging and is explicitly not
   authenticated.

```ts theme={null}
import { WebhookVerificationError, WebhookVerifier } from "omegas-operator";

const verifier = new WebhookVerifier({ secret: process.env.OMEGAS_WEBHOOK_SECRET! });

app.post("/hooks", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    const { event } = await verifier.verify({ body: req.body, headers: req.headers });
    await handle(event);          // 200 fast; do the slow part out of band
    res.sendStatus(200);
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      res.sendStatus(error.reason === "replayed_delivery" ? 200 : 400);
    } else {
      res.sendStatus(500);        // our bug, not theirs
    }
  }
});
```

## What you answer matters

The sender treats every non-2xx as a delivery failure and **disables an
endpoint after 20 consecutive failures**. Re-enabling is an explicit
`PATCH /v1/webhook_endpoints/{id}` with `{"enabled": true}`; nothing turns an
endpoint back on for you.

| Situation                      | Answer | Why                                                                                                                                                 |
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Verified and handled           | `200`  |                                                                                                                                                     |
| A replayed delivery            | `200`  | A duplicate is expected traffic under at-least-once delivery, not an error. Answering `400` burns your endpoint's failure budget on normal traffic. |
| Any other verification failure | `400`  | It will never verify; a retry cannot help.                                                                                                          |
| Your handler threw             | `500`  | You want the retry.                                                                                                                                 |

## Receipt is not processing

By default the verifier records an event as seen the moment it verifies. If
your handler then throws, the retry is refused as a replay and that event is
gone from your system's point of view. That default makes the common case, an
idempotent handler, effectively-once. For the other trade:

```ts theme={null}
const verifier = new WebhookVerifier({ secret, manualCommit: true });

const delivery = await verifier.verify({ body, headers });
await handle(delivery.event);   // throws: nothing recorded, the retry is accepted
await delivery.commit();        // only now is a redelivery treated as a replay
```

The cost of `manualCommit` is that two deliveries of the same event arriving
concurrently can both be processed. Neither default is universally right; pick
the one your handler's failure mode deserves. The default replay store is
in-memory and correct for one process; several instances need a shared store,
such as Redis or a unique index on the event id.

## Delivery semantics

Delivery is at-least-once and unordered. Order on the envelope's
`created_at`, never on arrival. `operator.webhook.*` events are audit-only:
they appear in `GET /v1/audit` and are never delivered, because delivering
them would loop a failing endpoint against itself.
