# Retrieve an article **GET /articles/{article}** Read one article, including its `content`. This is the only operation that returns a body. The same two gates apply as on the list. An article stamped with an audience answers `404` to a key that does not hold that audience, and a future dated article answers `404` until it goes live, including to the newsletter's own team, so that a preview link cannot be shared early. For a natively published issue, `content` is the email rendered to HTML with personalization placeholders resolved against an empty context, so a merge tag never leaks as raw text. For an imported issue it is the body as it arrived from the provider. `?expand=content` adds `content_markdown`, the same body as Markdown. Ask for it when a model is going to read the issue, and ask for `?fields=content_markdown` with it to leave the HTML behind entirely. ## Servers - Production. There is no separate sandbox host. : https://api.usecommune.com (Production. There is no separate sandbox host. ) ## Authentication methods - Api key ## Parameters ### Headers - **Commune-Version** (string(date)) The contract version this request is written against, as a release date (`YYYY-MM-DD`). Omitting the header pins the request to the version that was current when the API key was issued, so an integration keeps working when a newer version ships. An unknown value answers `400` with `invalid_version`. ### Path parameters - **article** (string) The article's `id` (a UUID) or its `short_id`, an eight character base62 string that is unique across Commune. The `slug` is not accepted here because it is unique only within a newsletter. ### Query parameters - **expand** (string) Comma separated list of relationship paths to inline in the response. Unexpanded relationships are returned as a reference object carrying only `id` and `object`. Each operation documents the paths it accepts, and an unknown path answers `400`. Nested paths use a dot, for example `article.newsletter`. One accepted path is not a relationship. `?expand=content` on `GET /articles/{article}` adds the Markdown rendition of the body beside the HTML one. It is the same trade the parameter always offers, a fuller response for a larger one, over a property that has more than one representation rather than over a reference. - **fields** (string) Comma separated allow list of top level properties to return on each object, so a client can trim a response it does not need in full. `id` and `object` are always returned. An unknown property name answers `400`. Properties omitted by an operation, such as `content` on any article list, cannot be brought back with `fields`. A trimmed body is a subset of the schema this operation declares, and a property that schema marks required is absent when it was not asked for. That is the point of the parameter, so a client that validates responses against the schema either sends no `fields` or relaxes `required`. ## Responses ### 200 The article, with `content`. #### Body: application/json (object | null) - **object** (string) Always `article`. - **id** (string(uuid)) Stable identifier. - **short_id** (string) Eight character base62 identifier, unique across Commune. Safe in a URL and accepted anywhere `{article}` is. - **slug** (string) URL segment under the newsletter, unique within it but not across Commune. The permalink is `/n/{handle}/a/{slug}`. Falls back to the `short_id` for an untitled issue. - **newsletter** (object | null) The newsletter this issue belongs to. A `Ref` unless `newsletter` is named in `?expand=`. - **title** (string | null) Subject line of the issue. Null for an untitled draft. - **preview_text** (string | null) The short line email clients show after the subject, and what Commune uses as the excerpt on a card. - **image_url** (string(uri) | null) Cover image. When the creator set none, Commune stamps the first image in the body at send time, so this is usually populated for a sent issue. - **external_url** (string(uri) | null) The issue's canonical URL on the newsletter's own provider, for an imported article. Null for a natively published one. - **status** (string) Where an issue is in its life. Only `sent` is ever visible to a public key. An imported article is always `sent`, since Commune sees it after the provider delivered it. - **is_imported** (boolean) `true` when the issue came in from the newsletter's provider, `false` when it was written and sent natively in Commune. - **posted_at** (string(date-time) | null) When the issue went out. An article dated in the future is not returned by any read operation until that moment passes, so this is never ahead of now in a response. - **authors** (array[object]) The byline, in order. Each entry is a `Ref` unless `authors` is named in `?expand=`. Empty when no Commune account is credited. - **thread** (object | null) The chat thread this issue opened, where its discussion lives. `null` when the newsletter does not open a thread per issue. A `Ref` unless `thread` is named in `?expand=`. - **stats** (object) Engagement counts for an article, computed at read time. These are Commune side counts, not provider side email metrics: opens, clicks and deliveries are not here. - **created_at** (string(date-time)) When the row was created in Commune. - **updated_at** (string(date-time)) When the issue was last edited. - **content** (string) The body as HTML. For a natively published issue this is the email rendered for the web, with personalization placeholders resolved against an empty context so no raw merge tag is ever served. For an imported issue it is what the provider published. Treat it as untrusted markup from a third party and render it in a sandboxed context. - **content_markdown** (string | null) The same body as Markdown, present only when `content` is named in `?expand=`. It is what a model should read: the HTML is mostly markup it will not use, and one issue body can fill a context window on its own. It is a conversion of the body rather than of the HTML above. For a natively published issue it comes from the document the author wrote, so a code block keeps its language and a table that declares a header becomes a Markdown table. For an imported issue it comes from the provider's HTML. Either way the words, the links, the images, the lists, the code and the quotes survive, and everything presentational does not. `null` means Commune holds no body it can convert faithfully. That happens when the only body it stored is a rendered email, whose words cannot be told apart from its layout. An empty string means the issue has no body, which is different. No merge tag ever appears here, resolved or not. ### 400 The request was malformed: an unknown query parameter, an unparseable cursor, an unknown `expand` path, or an unrecognised `Commune-Version`. #### Body: application/json (object) - **error** (object) ### 401 No API key was presented, or the key is unknown, revoked or expired. All four answer identically, down to the wording. Saying that a key was revoked rather than never issued confirms to whoever is holding the string that it was once real, which a legitimate caller does not need and a thief should not get. #### Headers - **WWW-Authenticate** (string) The authentication scheme this API accepts. Always `Bearer realm="Commune API"`; there is no second scheme and no query-parameter fallback, because a credential that can travel in a URL ends up in access logs and referer headers. #### Body: application/json (object) - **error** (object) ### 404 No such resource, or the key is not allowed to know that it exists. Commune answers `404` rather than `403` where distinguishing the two would leak the existence of private content. #### Body: application/json (object) - **error** (object) ### 429 Too many requests. Back off and retry after the interval named by the `Retry-After` response header. #### Headers - **Retry-After** (integer) Seconds to wait before retrying. #### Body: application/json (object) - **error** (object) ### 500 Something failed inside Commune. The request may be retried. #### Body: application/json (object) - **error** (object) [Powered by Bump.sh](https://bump.sh)