Thread published to the feed 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 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

  • 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 thread's visibility became public and it hit the global feed.

  • 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 thread.published.

  • 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
    • thread_id string(uuid) Required

      The thread. The same id thread.created carried, so a consumer that stored that event can match on it directly.

    • short_id string | null

      Short public identifier used in thread URLs.

    • url string(uri) | null
    • author object | null

      A pointer to a Commune account, enough to attribute and render something without a second call. Null when there is no account to point at, either because there never was one or because the account was deleted. Content outlives its author, so a null here is expected rather than a sign the reference was lost.

      Hide author attributes Show author attributes object | null
      • user_id string Required
      • username string | null
      • display_name string | null
      • avatar_url string(uri) | null
    • visibility string Required

      Always public. Present so a payload stays self-describing next to thread.created, which carries the same field and can carry other values.

      Value is public.

    • previous_visibility string | null Required

      What the thread was before. Null when it was created public, which is the case source also reports.

      Values are subscribers, paid, or null.

    • source string Required

      created_public for a thread a team member started public, which fires thread.created from the same write. visibility_changed for one that was featured later.

      Values are created_public or visibility_changed.

    • published_by object | null

      The team member who put it on the feed. Named published_by rather than actor because the envelope already has an actor, which means something different and is always null today. Null on the created_public path, where a thread was public from the start and nobody featured it.

      Hide published_by attributes Show published_by attributes object | null
      • user_id string Required
      • username string | null
      • display_name string | null
      • avatar_url string(uri) | null
    • article_id string(uuid) | null

      Set when the thread is an article's discussion rather than a standalone one.

    • published_at string(date-time) Required

      visibility_changed_at on the visibility_changed path, the thread's created_at on the created_public path.

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 thread.published
Request examples
{
  "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"
  }
}