# Outbound Event Catalog

LabNote emits HMAC-signed webhook deliveries (see `webhooks-dispatch` /
`X-LabNote-Signature: t=<ts>,v1=<hex>`) whenever a configured endpoint is
subscribed to a matching event type.

Endpoints subscribe via `webhook_endpoints.events text[]`. Both exact event
names and wildcards are honored:

- exact: `experiment.signed`
- per-resource wildcard: `experiment.*`, `sample.*`, `stock_movement.*`, `equipment.*`
- global wildcard: `*`

## Event types

| Event | Trigger | Payload fields |
| --- | --- | --- |
| `experiment.created` | INSERT on `experiment_entries` | `id, display_id, title, status, project_id, author_id, created_at` |
| `experiment.status_changed` | UPDATE of `status` | `id, display_id, old_status, new_status, updated_at` |
| `experiment.signed` | First time `signed_at` becomes non-null | `id, display_id, signed_by, signed_at` |
| `sample.created` | INSERT on `samples` | `id, name, type, status, storage_location_id, created_at` |
| `sample.status_changed` | UPDATE of `status` | `id, name, old_status, new_status, updated_at` |
| `stock_movement.created` | INSERT on `reagent_stock_movements` | `id, stock_item_id, movement_type, quantity_delta, quantity_after, unit, reason, storage_location_id, from_storage_location_id, to_storage_location_id, performed_by, performed_at` |
| `equipment.*` | existing equipment lifecycle hooks | see equipment module docs |

## Envelope

The body sent to your endpoint is:

```json
{
  "id": "<delivery uuid>",
  "event": "experiment.signed",
  "org_id": "<uuid>",
  "delivered_at": "2026-06-19T19:00:00Z",
  "data": { /* payload from the table above */ }
}
```

Signed payload for HMAC: `<timestamp>.<raw body bytes>`. Use the secret stored
when you created the endpoint (or its predecessor during a rotation grace
window — both `v1=` values in the header are valid).

## Receiver checklist

1. Read `X-LabNote-Timestamp` and reject deliveries older than 5 minutes.
2. Recompute `HMAC_SHA256(secret, "<timestamp>.<raw_body>")` and compare in
   constant time against any `v1=` value in `X-LabNote-Signature`.
3. Respond 2xx within 15 seconds. Non-2xx triggers retry with exponential
   backoff; after the retry limit the delivery moves to the DLQ.
4. Make handling idempotent on `id` (delivery id) — duplicates can occur after
   network errors on the 2xx response.
