A thread's visibility became public, which is what puts it on the
global feed. Since posts were retired into chat, this is the single
deliberate path by which community content leaves one newsletter's space
and reaches everyone, so it is the topic to watch for anything that
mirrors, syndicates or moderates the feed.
Two code paths reach public and both publish here, distinguished by
data.source.
visibility_changed: a team member featured a thread that already
existed. Who featured it and when are recorded by the same change, and
reach this payload as published_by and published_at.
created_public: the thread was born public, because a team member may
set visibility: 'public' when starting a top-level thread. That thread
fires thread.created and this topic from the same write, and has no
published_by.
Only team members (owner, admin or editor) can reach public on either
path, and only a top-level thread can: featuring anything with a
non-zero thread_level is refused, and replies inherit their thread's
placement. Going the other way, back to subscribers, is a real change
but is not modelled: a consumer mirroring the feed should reconcile
against the REST resource rather than assume a thread it saw here is
still public.
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
-
API version the payload conforms to. Same value as
api_versionin the envelope. -
Same value as
idin the envelope. The dedupe key. -
Same value as
typein the envelope. The topic name. -
HMAC over the raw request body. Verify before parsing.
-
Unix seconds the signature was produced at. Reject anything outside your tolerance window.
-
1 on the first attempt, incremented on each retry. Informational only. A consumer must be idempotent regardless of its value.
Minimum value is
1.
Body
Required
A thread's visibility became public and it hit the global feed.
-
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.
-
The topic name, identical to the channel address. Route on this.
Value is
thread.published. -
The
Commune-Versionvalue 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. -
RFC 3339 timestamp of the state change, not of the delivery attempt. Use it to order events, since delivery order is not guaranteed.
-
Who caused the change. ALWAYS
nulltoday 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.
-
ALWAYS
nulltoday. Once the public API accepts writes this carries theIdempotency-Keyheader 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. -
Event-specific body. Narrowed by each concrete event schema below.
Responses
-
The consumer accepted the delivery. Any 2xx acknowledges the message and it is not sent again.
-
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.
-
The consumer failed to handle the delivery. Retried with backoff. A timeout is the same case and is retried too.
{
"id": "018f2a92-3030-7000-8000-000000000030",
"type": "thread.published",
"api_version": "2026-08-26",
"occurred_at": "2026-08-26T18:02:19Z",
"newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
"actor": null,
"idempotency_key": null,
"data": {
"thread_id": "b1c2d3e4-f506-4718-8293-a4b5c6d7e8f9",
"short_id": "k3n8qz",
"url": "https://example.com/t/k3n8qz",
"author": {
"user_id": "usr_2Nf8Kq1pWc",
"username": "mira",
"display_name": "Mira Okafor",
"avatar_url": "https://cdn.example.com/avatars/mira.png"
},
"visibility": "public",
"previous_visibility": "subscribers",
"source": "visibility_changed",
"published_by": {
"user_id": "usr_5Qw8Hn2vFd",
"username": "sam",
"display_name": "Sam Ortega",
"avatar_url": null
},
"article_id": "4c9e2f81-0b7a-4d13-8e55-1a2b3c4d5e6f",
"published_at": "2026-08-26T18:02:19Z"
}
}
{
"id": "018f2a92-3131-7000-8000-000000000031",
"type": "thread.published",
"api_version": "2026-08-26",
"occurred_at": "2026-08-26T18:40:03Z",
"newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
"actor": null,
"idempotency_key": null,
"data": {
"thread_id": "c3d4e5f6-0718-4920-a1b2-c3d4e5f60718",
"short_id": "t9m4hx",
"url": "https://example.com/t/t9m4hx",
"author": {
"user_id": "usr_5Qw8Hn2vFd",
"username": "sam",
"display_name": "Sam Ortega",
"avatar_url": null
},
"visibility": "public",
"previous_visibility": null,
"source": "created_public",
"published_by": null,
"article_id": null,
"published_at": "2026-08-26T18:40:03Z"
}
}