Commune API
2026-08-26
The Commune API exposes a Commune community: newsletters, the articles they publish, the chat threads those articles start, and the people who write and read them.
Most of it is reading. A small set of operations changes something, and those behave differently in three ways described under Writes below.
Fetching this document
This contract is served by the API itself, in two syntaxes carrying the same
content: https://api.usecommune.com/openapi.yaml and
https://api.usecommune.com/openapi.json. Generate a client from whichever
your toolchain prefers.
?version= selects a contract version, the same way the Commune-Version
header does for a request, and answers 404 for a version that was never
released. ?profile=docs returns the variant the published reference is
rendered from; it differs only in presentation metadata, so the operations,
webhooks and schemas are identical either way.
Versioning
The base URL carries no version segment. A request selects a contract
version with the Commune-Version header, whose value is the release date
of the contract (for example 2026-08-26). Omitting the header pins the
request to the version that was current when the API key was issued.
Every response echoes the version it resolved to in a Commune-Version
response header, on success and on failure alike. A client that never sets
the header can read which contract it has been getting, and compare it
against version in GET /status to find out whether a newer one is
available to move to.
Every response also carries a Commune-Request-Id, which is the value that
appears as request_id in an error body. Quote it in support requests.
Authentication
Every request is authenticated with a credential sent as a bearer token. There are two ways to obtain one and one permission model behind both. An API key is minted by a creator in Commune's settings. An OAuth access token is issued when a person completes the authorization code flow and clicks allow; the walkthrough is at usecommune.dev/use-cases/build-an-integration.
A credential is granted one or more of the newsletters its holder can act
on. On each of those it carries six independent permissions, one per family,
each none, read or write:
| Family | Covers |
|---|---|
content |
articles, the passages readers marked in them, threads, messages |
audience |
subscribers, segments, the community roster |
sending |
sending an article, senders, delivery attempts |
insights |
engagement events and the metrics over them |
settings |
the newsletter's configuration, its website domains, its team, its credentials |
webhooks |
event destinations and the portal that edits them |
Each operation names the family and the level it needs, as an OAuth scope
such as content:read. write implies read within its own family and
nowhere else: there is no hierarchy across families, so a credential that
may send your articles has no claim at all on your subscribers. An operation
a credential does not hold the family for answers 403 naming what it
needed and what the credential holds on that newsletter.
Permissions are granted per newsletter, so the same credential can hold
content: read on one and audience: write on another. They are also
bounded by their holder: what a credential can do is what it was granted
intersected with what the person it belongs to can do on that newsletter at
the moment of the request. Remove them from the team and the credential
reaches nothing there on its very next call; demote them from admin to
editor and it loses settings: write. There is nothing to revoke and no
delay.
Unpublished rows follow one extra rule. A draft, an article whose send
time has not arrived, and a thread addressed to a segment are not secret,
they are unpublished, and the credentials that may see them are the ones
that may change the newsletter: those holding write in any family on
it. A credential holding only read permissions sees the newsletter as it
has been published, and this document says so on each operation where it
makes a difference.
A credential can also carry account: read, which reads the account it
belongs to: the profile behind it, and the teams, lists, saved articles and
liked articles that belong to the person rather than to a newsletter. It is
a separate axis, not a seventh family. No newsletter grant implies it
and it implies no newsletter grant, so a credential that reads a
newsletter's subscribers still cannot read its owner's own reading list. It
is granted on the credential itself, so either kind can carry it, and one
issued without it answers 403 at an operation that needs it however many
newsletters it reaches.
An OAuth authorization that asks only for account:read is granted no
newsletter, so it answers 403 at every operation that addresses one.
Rate limits
Every request is counted against the credential that made it, never against an address. Three budgets apply:
generalcounts every request.audience, which is tighter, counts only the operations that return subscriber or recipient email addresses.write, equally tight, counts only the operations that change something.
An operation covered by one of the narrow budgets is charged to it and to
general, and has to pass both.
From the moment a credential resolves, every response carries
RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for whichever
budget is closest to exhaustion, and RateLimit-Policy listing every budget
that applied. RateLimit-Reset is in seconds from now. A 429 additionally
carries Retry-After, and its message names the budget that refused:
being refused by the audience budget still leaves the rest of the API
callable.
GET /rate-limit reports every budget at once, which is what to read rather
than inferring the whole picture from the one budget the headers describe.
Writes
An operation that changes something is a POST, a PATCH or a DELETE,
needs write in its own family, and differs from a read in three ways.
It requires an Idempotency-Key request header. Choose one value per
change you intend to make, and send that same value again if you have to
retry. Commune records the answer your first attempt produced and replays
it rather than making the change a second time; a replayed response carries
Idempotent-Replay: true and is otherwise identical to the original. A key
is remembered for 24 hours, per credential.
Reusing a key for a different request answers 409 rather than replaying
the wrong answer. Two requests count as the same request when the operation,
the path, the query string, the body and the contract version all match.
It is counted against the write rate limit budget. See Rate limits
above.
It publishes an event, carrying your credential in the envelope's
actor and your Idempotency-Key in idempotency_key. That lets a
consumer tell a change your integration made from one a creator made in
Commune, and collapse the events one retried write produced. If the same
integration also consumes events, the event your write publishes is
delivered back to you: skip the ones whose idempotency_key you issued, or
your integration will answer itself. See Webhooks.
Pagination
Collections are cursor paginated. A response carries data plus a
pagination object holding an opaque next_cursor. Pass it back as
?cursor= to fetch the following page. There is no offset, limit-offset or
page number, and a cursor is not a durable identifier.
Identifiers
Resources that have a page in Commune carry both a UUID id and a short,
URL friendly short_id. Either value is accepted wherever a path parameter
names that resource.
This is version 2026-08-26 of this API documentation. Last update on Oct 6, 2026.
Waiting for a request to be sent.