# Send an article to the list **POST /articles/{article}/send** Sends this article to the newsletter's subscribers. Cannot be undone. **It answers before the article has gone out.** The article is queued for immediate dispatch and the answer is `202` with the article as it now stands: `status` is `scheduled` and `scheduled_for` is the moment it was queued. Commune begins sending within a few minutes. Watch `send.completed` for the outcome and the counts, or `send.failed` if the dispatch broke. `article.published` fires when the article goes live on the web. All three carry the credential that asked for the send and the idempotency key it was made under, so a consumer can tie them back to this call. **Six gates.** Four are simple: there has to be an article, a verified sending address, something in the body, and a status that can be sent from. An article that is already sending or sent answers `422`. The other two are worth knowing about before you call this: * **`missing_footer`** (`422`) when the body no longer carries an unsubscribe link or a mailing address. Both are seeded into a draft as ordinary content and can be edited away, and commercial email is required to carry them. The error names which half is gone. * **`broken_images`** (`422`) when an image definitively will not load, naming the URLs and why each one failed. An inbox fetches images when the reader opens the article, so a rotted image is broken for everybody and cannot be repaired after the send. Only a definite answer refuses: a merely slow host is reported and never blocks. Set `acknowledge_broken_images` to send anyway. That acknowledgement covers the body as it currently reads and any edit clears it. An article addressed to a tag goes only to the subscribers holding it. An article with no subscribers to send to is still published: it goes live on the web and reports zero recipients. **Failed deliveries are Commune's to follow up.** A recipient the sending provider turns away for a moment is retried automatically during the send. One that still fails is followed up by Commune's team rather than re-sent blindly, because some of those may already have been delivered and a second copy is worse than a late one. Publishes `article.scheduled` when the article is queued, then `article.published` and `send.completed` or `send.failed` when the dispatch runs. ## Servers - Production. There is no separate sandbox host. : https://api.usecommune.com (Production. There is no separate sandbox host. ) ## Authentication methods - Api key - Oauth2 ## Parameters ### Headers - **Commune-Version** (string) The contract version this request is written against. Every version published so far is a release date (`YYYY-MM-DD`), which is why the examples look like one, but the value is an opaque identifier: match it against the versions this API publishes rather than parsing it, because a future one may not be only a date. An unknown value answers `400` with `invalid_version`. 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. - **Idempotency-Key** (string) A value of your choosing naming the change this request is making. Send the same value again to retry the same request. Commune replays the answer the first attempt gave instead of making the change twice, and marks the replay with an `Idempotent-Replay: true` response header. Send a different value for a different change: a key reused for a request that differs in any way answers `409`, because replaying an answer to a question you did not ask is a wrong answer you could not detect. A UUID per change is the usual choice. Remembered for 24 hours, per credential, so two credentials choosing the same value never see each other's answers. Required, not optional. ### 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. ### Body: application/json (object) - **acknowledge_broken_images** (boolean) Send even though an image in the body will not load for a reader. Without this, an image Commune could definitively not fetch refuses the send. With it, the send proceeds and the acknowledgement is recorded against the article as it currently reads, so the dispatch that happens a few minutes later honours the same decision. Any edit to the body clears it, which keeps it scoped to the images that were actually looked at. It does not suppress anything else. An unreadable image is still reported on the article, and the footer gate is not escapable at all. ## Responses ### 202 The article is queued. `status` is `scheduled` and `scheduled_for` is the moment it was queued; sending begins within a few minutes. #### 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 article. - **newsletter** (object | null) The newsletter this article belongs to. A `Ref` unless `newsletter` is named in `?expand=`. - **title** (string | null) Subject line of the article. 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 article. - **external_url** (string(uri) | null) The article's canonical URL on the newsletter's own provider, for an imported article. Null for one written in Commune. - **status** (string) Where an article is in its life. A credential holding only `read` permissions ever sees `sent` and nothing else. An imported article is always `sent`, since Commune sees it after the provider delivered it. - **is_imported** (boolean) `true` when the article came in from the newsletter's provider, `false` when it was written and sent in Commune. - **posted_at** (string(date-time) | null) When the article 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. - **scheduled_for** (string(date-time) | null) When a queued article may go out. Set while `status` is `scheduled` and null otherwise. This is not `posted_at` and the difference matters: a queued article has no publication date yet, which is why it stays invisible on every reader surface until it really goes out. Commune dispatches in passes, so this is the moment from which the article may go rather than the moment it will. - **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 article opened, where its discussion lives. `null` when the newsletter does not open a thread per article. 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 article was last edited. ### 400 The request was malformed, and the same request will fail the same way until it is changed. `param` names the parameter or header at fault when there is exactly one, and `allowed_values` lists what it accepts when that is a finite set. The code is `bad_request` for every case below except the last. * **A query parameter**: one the operation does not have, a value outside its set, range or format (an unparseable cursor, an unknown `expand` path or `fields` name, an identifier that is not a UUID), or a required one left out, such as `q` on a search or `newsletter` when the credential reaches more than one. * **The request body**: not JSON, not the shape the operation reads, a property it does not write, or a value of the wrong type, length or format. `param` is absent here, since the body is not a parameter, and the message names the property. * **The `Idempotency-Key` header**, on an operation that changes something: missing, or a value this API will not store. * **An unrecognised `Commune-Version`**, which answers with its own code, `invalid_version`, because it is never fixed by changing the body. #### Body: application/json (object) - **error** (object) ### 401 No credential was presented, or it is malformed, unknown, revoked or expired, or it is an access token minted for a different audience. Every one of these answers identically, down to the wording and the headers, so a refusal never confirms that a string was once real. #### Headers - **WWW-Authenticate** (string) The authentication scheme this API accepts, and where to find out how to get a credential for it. Always `Bearer realm="Commune API", resource_metadata="https://api.usecommune.com/.well-known/oauth-protected-resource"`. `resource_metadata` is the RFC 9728 pointer to this API's protected resource metadata, which names the authorization server an OAuth client should send its user to. A client holding an API key can ignore it. The header carries no `error` parameter, not even `error="invalid_token"`, because it describes what this API accepts rather than what was wrong with the credential sent, and the reasons above are deliberately indistinguishable. 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) ### 402 The credential is allowed to do this but the newsletter's plan does not include it. Two surfaces can answer it: **insights**, the engagement and metrics operations, which are the only reads Commune reserves the right to meter, and **writing**, every operation that changes something. Every other read stays free on every plan, so a credential refused at one of these can still read everything else. The body names the plan the newsletter is on and the plans that would work. **This status is predictable and should not be how you discover it.** `GET /newsletters/{newsletter}/entitlements` answers the same question in advance, carrying the same plan list this puts in `allowed_values` and the same sentence it puts in `message`. Read it once at the start of a run rather than finding out in the middle of one. #### Body: application/json (object) - **error** (object) ### 403 The credential is valid but is not allowed to do this. Two codes answer with this status, and `error.code` says which. **`insufficient_scope`: it does not hold the permission.** The operation needs, say, `audience: read` on the newsletter addressed, and this credential holds less than that there. `allowed_values` carries the permission that was needed, and the message says what the credential does hold on that newsletter, because a credential granted the wrong family and a credential belonging to somebody whose standing on the team has narrowed look identical without it. The answer can differ per newsletter: the same credential may be allowed here and refused on the next one it reaches. The same code answers an operation that needs the **account permission** from a credential that does not carry it. That permission is about the person a credential belongs to rather than about any newsletter, so nothing granted on a newsletter adds up to it. It is granted on the credential itself, when a key is minted or when an authorization asks for `account:read`. And it answers a parameter the credential may send, but not with the value it sent: a filter a credential holding only `read` permissions may not use, or an `expand` path whose rows need a permission the operation does not. `param` names the parameter, and `allowed_values` carries what this credential may send instead, or is absent when it may send nothing there at all. **`forbidden`: it may not act here at all.** Either the credential does not reach the newsletter addressed, because it was never granted it or because the person it belongs to can no longer act on it, or it reaches no newsletter at all; `param` is `newsletter`, and `GET /newsletters` lists the ones it does reach. Or, on `DELETE /api-keys/{key}`, the credential named belongs to somebody else. Neither carries `allowed_values`, because there is no value to send instead. #### 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) ### 409 The request collided with something. On a write this is always the `Idempotency-Key`, in one of two ways, and the message says which. Either the key was already used for a **different** request, which is refused rather than answered with the earlier request's result. Or an earlier request using the same key has not finished, or never reported an outcome, in which case this one was not run and the key becomes usable again shortly. Nothing was changed by a request that answers this. #### Headers - **Retry-After** (integer) Seconds to wait before retrying, on the second case only. #### Body: application/json (object) - **error** (object) ### 422 The request is well formed and every value in it is legal, and the state of what it addresses refuses it anyway. The message says what about that state is in the way. #### Body: application/json (object) - **error** (object) ### 429 Too many requests. Back off and retry after the interval named by the `Retry-After` response header. Usually one of the budgets in `RateLimit-Policy` ran out, and the `RateLimit-*` headers on this response say which and when it resets. This operation can also reach a **daily send limit**, which is counted apart from those budgets and is not reported in them: how many times an article may be dispatched to a newsletter's whole list in a day, or how many test copies a credential may send to addresses it names. Neither counts recipients, so the size of a send is never what refuses it. When one of these is what answered, the message says so by name and `Retry-After` is measured in hours rather than seconds, which is how to tell the two apart. #### 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)