# Getting started with the API > Ten minutes from no credential to a response you can read. Mint a key, make a call, understand the envelope it comes back in, make a change safely, and pin the contract version before you write anything against it. Guide 2 of 2 in the Commune developer documentation at https://usecommune.dev. ## Mint a key A key belongs to you rather than to a newsletter, so you mint it in your own account, at [Settings, API keys](https://usecommune.com/settings/api-keys). You answer four questions when you create one. The first is a name, for telling four keys apart once their secrets are gone. The second is **which newsletters** it reaches. Pick them one by one, or take every newsletter you run today, or take every newsletter you will ever run. The last two differ only in the future, which is why they are separate choices: a credential that quietly gains a newsletter you start next year is convenient and worth deciding on purpose. The third is **what it may do**, on each of them. Six families, each `none`, `read` or `write`: - **content** is articles, threads, messages and highlights. `read` is the newsletter as published; `write` also reaches drafts and scheduled articles. - **audience** is subscribers, tags and the community roster. This is the family that carries email addresses. - **sending** is sends, senders and delivery attempts. `write` here puts real mail in front of real people. - **insights** is engagement events and metrics. - **settings** is the newsletter’s configuration, its website domains, its team and its credentials. - **webhooks** is where Commune sends events. There is no hierarchy across families. `content: write` is no claim at all on `audience`, which is the whole reason there are six of them rather than one switch. An operation that needs more than the key holds answers `403` and names the family and level it wanted, in the same words the form used. The fourth is **when it expires**, and it is required. Ninety days by default, any date up to a year away, or never, which the form offers and tells you why it would rather you did not choose. The secret is shown once, when the key is created, and is unrecoverable after that. It looks like `cmn_sk_` followed by a long random string. Put it wherever you keep secrets before closing the dialog. A key is for your own code. If you are building something other people will connect to their newsletters, they should never paste you a key: use OAuth, as [Build an integration](https://usecommune.dev/use-cases/build-an-integration) shows end to end. > **A key granted the audience reads email addresses** > > That is the point of that family, and it is also the reason to treat such a key like a password rather than like a configuration value. Every subscriber read a key makes is recorded against that key, the reads are charged to a tighter rate limit than everything else, and revoking a key takes effect on its next request. Do not commit one, and do not paste one into a page you might screenshot. > **What a key holds is not the last word** > > A grant is a ceiling, not an authority. What a key can actually do is what you granted it, narrowed by what *you* can do on each newsletter at the moment of the request, recomputed every time. Leave a newsletter’s team and every key of yours stops reaching it on its very next call, with nothing to revoke. An owner or admin of a newsletter can also cut a key off that newsletter without touching the key itself, and your settings page will say so beside the entry. ## Make the first call `GET /newsletters` is the call to start with, because this collection is reduced to what the key can see. So it returns the newsletters you granted it and nothing else, and it tells you the `handle` and `id` that every other operation wants. shell: ``` curl -i https://api.usecommune.com/newsletters \ -H "Authorization: Bearer cmn_sk_..." ``` `-i` is there on purpose. Three of the response headers are part of the contract and you will want to have seen them once before you need them. response: ``` HTTP/2 200 content-type: application/json; charset=utf-8 commune-version: 2026-08-26 commune-request-id: req_01j9c8h1q7m3n4p5r6s7t8u9v0 ratelimit-limit: 600 ratelimit-remaining: 599 ratelimit-reset: 60 { "object": "list", "data": [ { "object": "newsletter", "id": "9a4c1c6e-0f2b-4f47-9d3f-6d1b1a2c3d4e", "handle": "the-weekly", "name": "The Weekly", "esp": "commune", "created_at": "2026-02-11T09:14:03Z" } ], "pagination": { "has_more": false, "next_cursor": null } } ``` ## Reading the response Every collection comes back in the same envelope: `object` is always `"list"`, `data` is the page, and `pagination` holds the cursor state. Every object inside carries its own `object` field too, so a response is self describing and you can branch on a type without tracking which call produced it. Three response headers are worth wiring into your client now: - `Commune-Version` is the contract version this request actually resolved to. It is on every response, success and failure alike. - `Commune-Request-Id` identifies the request. It is the same value that appears as `request_id` in an error body. Log it, and quote it when you ask us about something. - `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` report whichever budget is closest to exhaustion, with `RateLimit-Reset` in seconds from now. ### When something fails Every non-2xx response from every operation has the same shape, so you can branch on `error.code` without knowing which call produced it. Treat an unrecognised code as a generic failure of its status class, because new ones can be added. Every error carries a `docs_url` pointing at the page for its code, which says what it means, why it happens and how to fix it: - [`bad_request`](https://usecommune.dev/errors/bad_request) 400. Something in the request itself is wrong: a query parameter, a header or the body. Change the request and send it again. - [`invalid_version`](https://usecommune.dev/errors/invalid_version) 400. The `Commune-Version` header names a contract version this API does not serve. Send one from `allowed_values`, or leave the header out. - [`unauthorized`](https://usecommune.dev/errors/unauthorized) 401. The request carried no credential the API accepts: none at all, a malformed one, or one that is unknown, revoked or expired. - [`forbidden`](https://usecommune.dev/errors/forbidden) 403. The credential is valid but cannot act on the newsletter you addressed, or on the key you tried to revoke. - [`insufficient_scope`](https://usecommune.dev/errors/insufficient_scope) 403. The credential reaches the newsletter but does not hold the permission this operation, filter or expansion needs. - [`payment_required`](https://usecommune.dev/errors/payment_required) 402. The credential may do this, but the newsletter's Commune plan does not include it. Reads outside insights stay free. - [`not_found`](https://usecommune.dev/errors/not_found) 404. Nothing this credential may see exists at that identifier. It may not exist at all, or your credential may not be allowed to know it does. - [`conflict`](https://usecommune.dev/errors/conflict) 409. The `Idempotency-Key` on this write was already used for a different request, or a request with the same key has not finished. - [`unprocessable`](https://usecommune.dev/errors/unprocessable) 422. The request is well formed, and the current state of what it addresses refuses it. The message says what is in the way. - [`not_commune_newsletter`](https://usecommune.dev/errors/not_commune_newsletter) 422. Articles can only be created or edited on a newsletter Commune publishes itself, one whose `esp` is `commune`. - [`rate_limited`](https://usecommune.dev/errors/rate_limited) 429. A rate limit budget or a daily send limit is spent. Wait `Retry-After` seconds; the message names which limit it was. - [`internal_error`](https://usecommune.dev/errors/internal_error) 500. Something failed inside Commune. Retry with backoff, reusing the same `Idempotency-Key` for a write, and quote the request id if it persists. - [`service_unavailable`](https://usecommune.dev/errors/service_unavailable) 503. A service this one operation depends on did not answer. The rest of the API is unaffected; retry this operation after `Retry-After`. When exactly one parameter is at fault it is named in `param`, and when that parameter accepts a finite set the whole set comes back in `allowed_values`. That array is deliberately redundant with the sentence in `message`: the array is what a program branches on and the sentence is what a model reads, and either way you can correct the request from the response without opening a reference page. Do not branch on `message`. It is written for a developer reading a log and it can change. ### Paging through a collection Cursors, never offsets. A response carries `pagination.next_cursor`; pass it back as `?cursor=` to get the following page, and stop when `has_more` is `false`. A cursor is opaque, is only valid for the same operation with the same filters, and is not a durable identifier, so do not store one and come back to it tomorrow. shell: ``` curl "https://api.usecommune.com/newsletters/the-weekly/articles?limit=100" \ -H "Authorization: Bearer cmn_sk_..." # then, while pagination.has_more is true: curl "https://api.usecommune.com/newsletters/the-weekly/articles?limit=100&cursor=" \ -H "Authorization: Bearer cmn_sk_..." ``` `?limit=` is a page size, between 1 and 100, defaulting to 20. Getting fewer items than you asked for does not mean the collection is exhausted. Only an absent `next_cursor` means that. ## Making a change An operation that changes something (a `POST`, a `PATCH` or a `DELETE`) needs `write` in its own family, and one more thing: an `Idempotency-Key` header. Choose one value per change you mean to make, and send the same value again if you have to retry. Commune makes the change once and answers the retry with the first answer, marked `Idempotent-Replay: true`, so a timeout is never a reason to wonder whether an article was drafted twice. shell: ``` curl -X POST https://api.usecommune.com/newsletters/the-weekly/articles \ -H "Authorization: Bearer cmn_sk_..." \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: 7c1e2a9d-4b3f-4e8a-9d61-2f5b0c3e8a14" \ -H "Content-Type: application/json" \ -d '{"title": "What we learned in March"}' ``` A key is remembered for 24 hours, per credential. Reusing one for a different request answers `409` rather than replaying the wrong answer. A request refused with a `4xx` changed nothing, so its key is free to use again once you have fixed it. Most changes can be taken back. Sending an article to the list cannot: it emails real people, and a key holding `sending: write` can do it. Send yourself a test copy first: `POST /articles/{article}/test-send` with no body sends one to you. ## Pin the contract version This is the part that costs people later, so it is worth five minutes now. There is no version segment in the URL. A request selects a contract version with the `Commune-Version` header, and the value is a release date: shell: ``` curl https://api.usecommune.com/newsletters \ -H "Authorization: Bearer cmn_sk_..." \ -H "Commune-Version: 2026-08-26" ``` **If you omit the header, you are still pinned.** A request with no `Commune-Version` is served the version that was current when its API key was issued. That is a deliberate default: an integration written last year keeps working when a new contract ships, instead of changing shape underneath it. But it means two keys in the same codebase can be answering from two different contracts, and nothing in your code would say so. So send the header. Explicitly, everywhere, from the first request. An unknown value answers `400` with `invalid_version`, rather than quietly serving the nearest version it does have. A failed build is cheaper than an integration that believes it is pinned and is not. 400 Bad Request: ``` { "error": { "code": "invalid_version", "message": "Unsupported value \"2025-01-01\" for the Commune-Version header. Supported versions: 2026-08-26. Omit the header to use the version your key was issued against.", "param": "Commune-Version", "allowed_values": ["2026-08-26"], "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0" } } ``` To find out whether there is a newer contract to move to, ask `GET /status`, which is unauthenticated and reports the newest version the service serves. Compare it against the `Commune-Version` you are getting back on your own responses. shell: ``` curl https://api.usecommune.com/status ``` When those two differ, the [changelog](https://api-reference.usecommune.dev/changes) says what moving would cost, change by change, and which of them break a caller. ## Rate limits Three budgets, counted against the key rather than against an address. The general budget counts every request. Two tighter ones count only some: the audience budget counts the operations that return subscriber or recipient email addresses, and the write budget counts the ones that change something. Those are charged to their own budget and to the general one, and have to pass both. Being refused by one of the narrow budgets still leaves the rest of the API callable, which is why they are separate counters rather than one smaller number. A `429` carries `Retry-After`, and its message names the budget that refused. The headers on an ordinary response can only describe one budget, so if you want the whole picture, ask `GET /rate-limit`, which reports every budget at once. ## Where to go next To see a whole job done end to end, read the [use cases](https://usecommune.dev/use-cases): winning back readers who are drifting away, publishing from wherever you write, reacting to events as they happen, and more. Each one is a sequence of real requests you can run or import into an API client. The [reference](https://api-reference.usecommune.dev) has all 63 operations with every field and every error, and the contract itself is published at [openapi.yaml](https://api.usecommune.com/openapi.yaml?profile=docs) if you would rather generate a client than read one. --- Next: [Use cases](https://usecommune.dev/use-cases.md) - Whole jobs done end to end, with an agent, with the API, or with both: win back readers, publish from anywhere, react in real time. - This guide as a page: https://usecommune.dev/guides/getting-started - Every guide on this origin has a Markdown twin at its own path with `.md` appended. - The whole API reference as one Markdown document: https://usecommune.dev/llms-full.txt - The index of everything here: https://usecommune.dev/llms.txt