Commune Webhooks API
The events Commune pushes to a consumer, rather than the resources a consumer pulls. Commune publishes state changes on 23 topics, each delivered as one HTTPS POST to an endpoint the consumer registered. Every message shares one envelope, so a consumer can route on `type` and dedupe on `id` without knowing anything about the specific event, and the same values arrive as `Commune-Event-Type` and `Commune-Event-Id` headers so both can be read before the body is parsed. Delivery is at least once and unordered. A non-2xx response or a timeout is retried with backoff, so a consumer has to treat `id` as the dedupe key and tolerate replays. `occurred_at` is the ordering field, not arrival time. Two envelope fields say where a change came from rather than what changed. `actor` names the credential when the change was made through this API's write operations, and `idempotency_key` carries the key that write was made under. Both are `null` for a change made anywhere else, which is most of them. **If your consumer writes, read `idempotency_key` before you act.** A write through this API publishes an event, and that event is delivered to every endpoint registered for the newsletter, including yours. A consumer that reacts to events by writing therefore receives the echo of its own write, cannot tell it from a change somebody else made, and writes again. You already hold what breaks the loop: you generated the key you sent on the write, so keep it and skip any event whose `idempotency_key` is one of yours. `actor` is not the field for this. It names Commune's own id for your credential, and no operation here tells you what that id is. Registering an endpoint happens in the delivery portal, which `POST /newsletters/{newsletter}/portal-session` mints a link into. The endpoints already registered are readable at `GET /newsletters/{newsletter}/destinations`. What happened to a particular message is readable. `GET /newsletters/{newsletter}/delivery-attempts` lists every handover Commune made, filterable by the `event_id` a consumer reads off its own `Commune-Event-Id` header, so "did that event reach me" is answerable from both sides of the same identifier.