Subscriber insight status changed Webhook

Add MCP server to your AI tool

Allow AI tools and LLMs to interact with the API documentation portal through MCP.

MCP server URL

https://api-reference.usecommune.dev/mcp

Standard setup for AI tools providing an mcp.json file

mcp.json
{
  "REST API MCP server": {
    "url": "https://api-reference.usecommune.dev/mcp"
  }
}

Close
POST https://webhook.example.com

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.

Headers

  • Commune-Version string Required

    API version the payload conforms to. Same value as api_version in the envelope.

  • Commune-Event-Id string(uuid) Required

    Same value as id in the envelope. The dedupe key.

  • Commune-Event-Type string Required

    Same value as type in the envelope. The topic name.

  • Commune-Signature string Required

    HMAC over the raw request body. Verify before parsing.

  • Commune-Timestamp string Required

    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.

    Minimum value is 1.

application/json

Body Required

A reader crossed a boundary in the newsletter's engagement ladder.

  • id string(uuid) Required

    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 Required

    The topic name, identical to the channel address. Route on this.

    Value is subscriber.status_changed.

  • api_version string Required

    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) Required

    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 Required

    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 Required

    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.

    Additional properties are NOT allowed.

    Hide actor attributes Show actor attributes object | null
    • type string Required

      user for a change a person made through an authenticated session, api_key for a public API write, system for a cron or webhook ingest with no human behind it.

      Values are user, api_key, or system.

    • id string Required

      Identifier of the actor within its type. A user id for user, a key id (never the secret) for api_key, a stable job name for system.

    • label string | null

      Human-readable name for display. Best effort, may be null.

  • idempotency_key string | null Required

    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.

    Maximum length is 255.

  • data object Required

    Event-specific body. Narrowed by each concrete event schema below.

    Hide data attributes Show data attributes object
    • subscriber_id string(uuid) Required

      The subscriber this score belongs to, the same identifier the other subscriber.* topics carry.

    • user_id string Required

      The Commune account the score is attributed to. Insights are keyed on the account rather than on the subscription, because half the signal is community activity that belongs to a person and not to a mailing list entry. Always present: an unscored subscriber cannot cross a boundary.

    • email string(email) Required

      Where the newsletter reaches this reader, so a consumer can match the event against its own records without a second call. Resolved from the Commune account when the subscription row itself carries no address.

    • previous_status string | null

      Where a reader sits in the newsletter's engagement ladder, from dormant at the bottom to superfan at the top.

      Assigned by rank inside the newsletter rather than against an absolute score, so it is a statement about this audience and never comparable between two newsletters. It also means a reader can move without doing anything, because the people around them moved.

      Values are superfan, engaged, reader, or dormant.

    • status string Required

      Where a reader sits in the newsletter's engagement ladder, from dormant at the bottom to superfan at the top.

      Assigned by rank inside the newsletter rather than against an absolute score, so it is a statement about this audience and never comparable between two newsletters. It also means a reader can move without doing anything, because the people around them moved.

      Values are superfan, engaged, reader, or dormant.

    • direction string Required

      Which way along the ladder the reader moved, derived from the two statuses so a consumer does not have to hardcode their order. promoted the first time a reader is scored, because there is no earlier position they could have fallen from.

      Values are promoted or demoted.

    • total_score integer Required

      The blended score at the moment of the crossing, summed across the community and the newsletter's email provider. It has no unit and no ceiling, and is meaningful only ranked against the other readers of the same newsletter.

    • velocity string Required

      The last fourteen days of points against the fourteen before them. Read it with direction: a demotion while rising means the audience around this reader moved faster, not that the reader slowed down.

      Values are rising, cooling, or steady.

    • last_action_at string(date-time) | null

      When the reader last did anything that earned points. The reader's own clock, not the scoring pass's, so it sits earlier than occurred_at by up to a full scoring interval. Null for a reader who has never acted.

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.

POST subscriber.status_changed
Request examples
{
  "id": "018f2a91-dddd-7000-8000-00000000000d",
  "type": "subscriber.status_changed",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-27T03:15:00Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": null,
  "idempotency_key": null,
  "data": {
    "subscriber_id": "33445566-7788-4990-a1b2-c3d4e5f60718",
    "user_id": "usr_2Nf8Kq1pWc",
    "email": "reader@example.com",
    "previous_status": "reader",
    "status": "superfan",
    "direction": "promoted",
    "total_score": 412,
    "velocity": "rising",
    "last_action_at": "2026-08-26T21:04:11Z"
  }
}
{
  "id": "018f2a91-eeee-7000-8000-00000000000e",
  "type": "subscriber.status_changed",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-27T03:15:00Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": null,
  "idempotency_key": null,
  "data": {
    "subscriber_id": "44556677-8899-4aa1-b2c3-d4e5f6071829",
    "user_id": "usr_7Zx3Lm9qRt",
    "email": "quiet@example.com",
    "previous_status": "engaged",
    "status": "dormant",
    "direction": "demoted",
    "total_score": 88,
    "velocity": "cooling",
    "last_action_at": "2026-07-02T09:12:40Z"
  }
}