# Subscriber insight status changed **POST subscriber.status_changed** A reader crossed a boundary in the newsletter's engagement ladder: `reader` to `superfan`, or `engaged` to `dormant`. This is the topic that makes Insights actionable. The scores themselves are a number a dashboard draws; a crossing is the moment a CRM or a re-engagement automation has something to do. Fired by a scheduled scoring pass that recomputes the insight record of every scored reader. One event per reader whose `status` actually differs from the one already stored, so a pass that changes only the scores is silent. Because the pass is scheduled, `occurred_at` is when the crossing was computed and not when the reader acted. `data.last_action_at` is the reader's own clock and sits earlier than it by up to a full scoring interval. A consumer that wants to react while the moment is still warm should read that field rather than `occurred_at`. Status is a rank inside this newsletter, not an absolute score, so a reader can be moved by other readers. Someone who did nothing at all can still be demoted because the audience around them got busier. Two events that look like opposite movements can therefore arrive from one pass without either reader having changed their behaviour. Only readers with a Commune account are scored, since engagement is attributed to an account, so a subscriber the newsletter knows only as an address never produces this event. Not the same thing as `subscriber.unsubscribed`. That reports the subscription itself ending. This reports how engaged someone is while they are still subscribed, and `dormant` is a reader who went quiet, not a reader who left. Delivered as a single HTTPS POST to the consumer's registered endpoint, with the message as the JSON request body. Respond 2xx to acknowledge. Anything else, or a timeout, is retried with backoff, so acknowledge fast and do the work afterwards. ## Servers - Production. There is no separate sandbox host. : https://api.usecommune.com (Production. There is no separate sandbox host. ) ## Authentication methods - Webhook signature ## Parameters ### Headers - **Commune-Version** (string) API version the payload conforms to. Same value as `api_version` in the envelope. - **Commune-Event-Id** (string(uuid)) Same value as `id` in the envelope. The dedupe key. - **Commune-Event-Type** (string) Same value as `type` in the envelope. The topic name. - **Commune-Signature** (string) HMAC over the raw request body. Verify before parsing. - **Commune-Timestamp** (string) Unix seconds the signature was produced at. Reject anything outside your tolerance window. - **Commune-Delivery-Attempt** (integer) 1 on the first attempt, incremented on each retry. Informational only. A consumer must be idempotent regardless of its value. ### Body: application/json (object) - **id** (string(uuid)) Unique id for this event. Stable across redeliveries, so it is the dedupe key. It is the primary key of the row the publisher wrote, which means it is assigned when the state change is recorded, not when the message is dispatched. - **type** (string) The topic name, identical to the channel address. Route on this. - **api_version** (string) The `Commune-Version` value this payload conforms to. Present on the message itself, not only on the request header, so an event persisted to a consumer's own store stays self-describing. - **occurred_at** (string(date-time)) RFC 3339 timestamp of the state change, not of the delivery attempt. Use it to order events, since delivery order is not guaranteed. - **newsletter_id** (string(uuid) | null) The newsletter the change belongs to. This is the tenant boundary: a consumer only ever receives events for newsletters its credential can read. Null only for events that are genuinely not newsletter-scoped, which none of the 21 topics currently are. - **actor** (object | null) Who caused the change. ALWAYS `null` today and consumers must treat it that way: the public API is currently read-only, so nothing that reaches this catalog was triggered through it, and Commune does not backfill an actor for changes made in the product UI or by a cron. The field exists now so that populating it, once writes exist, is an additive change rather than a new envelope version. - **idempotency_key** (string | null) ALWAYS `null` today. Once the public API accepts writes this carries the `Idempotency-Key` header of the write that caused the change, so a consumer can tie an event back to its own request and collapse the duplicates a retried write would otherwise produce. Null stays a legitimate value even then: a change made in the product UI or by a cron has no originating request to key on. - **data** (object) Event-specific body. Narrowed by each concrete event schema below. ## Responses ### 200 The consumer accepted the delivery. Any 2xx acknowledges the message and it is not sent again. ### 4XX The consumer rejected the delivery. Handled exactly like a 5xx: the message is retried with backoff, because a rejection cannot be told apart from a consumer that is briefly misconfigured. ### 5XX The consumer failed to handle the delivery. Retried with backoff. A timeout is the same case and is retried too. [Powered by Bump.sh](https://bump.sh)