---
title: API · conventions
status: current
phase: 3
order: 1
---

# API · conventions

Everything you do in the dashboard can be done by API. This page explains what applies to every endpoint.

## Base

```
https://pluma.so/api/v1
```

Start at the root. It tells you what it is, what your key can do and where the docs are:

`GET /api/v1`

```sh
curl https://pluma.so/api/v1 -H "Authorization: Bearer $PLUMA_KEY"
```

```json
{
  "name": "Pluma",
  "docs": "https://pluma.so/llms.txt",
  "key": { "kind": "delivery", "space": "acton-estero", "can": ["read_published"] },
  "next": "GET /api/v1/spaces/acton-estero"
}
```

The machine contract (OpenAPI 3.1) is at [`/openapi.json`](/openapi.json): use it to generate clients or to let your agent see every endpoint with its parameters.

## Authentication

Every request carries a key in the header:

```
Authorization: Bearer pluma_dlv_…
```

Keys are created in the site dashboard, under **Keys**. The full value is shown **only once**; keep it in a secrets manager. You can revoke a key in one click.

| Kind | Prefix | What it can do | What for |
| --- | --- | --- | --- |
| `delivery` | `pluma_dlv_` | Read what is **published** | Your site's build. Safe to use on the build server |
| `preview` | `pluma_prv_` | Also read **drafts** | Preview |
| `management` | `pluma_mgt_` | Read everything, **write**, publish and change the model | Scripts, migrations. Never in the browser |

An invited **agent** has its own token (`pluma_agt_`) with the same permissions as `management`, plus creating `delivery` and `preview` keys. See [Agents](agents.md).

**Delivery and Preview are the same API:** same routes, same responses. The only thing that changes is the key: with a `preview` key you see the latest version of each entry; with a `delivery` key, the published one.

## Responses

- JSON, UTF-8, `snake_case` in the API's names; your content's fields use their API ID as is.
- Every object has `sys` (metadata: `id`, `type`, dates, version) and `fields` (your content).
- The `sys` dates (`created_at`, `updated_at`, `published_at`) are ISO 8601, UTC. Date fields in your content come back exactly as you saved them, with their time zone.
- `id`s are opaque and unique across all of Pluma, not per site: don't assume they start at 1 or are consecutive.

## Collections and pagination

```json
{ "sys": { "type": "Array" }, "total": 57, "skip": 0, "limit": 100, "items": [ … ] }
```

- `limit`: up to 1000. Default 100.
- `skip`: how many to skip.
- For the next page: `skip = skip + limit` while `skip < total`.

## Locales

- Without `locale`, fields come in the site's default language.
- `locale=en`: in that language; if a field has no value, it falls back to the fallback language. Fields that aren't localized always come with the default language's value.
- `locale=*`: every language, as `{ "title": { "es": "…", "en": "…" } }`.
- Filters (`fields.slug=…`), sorting and `sys.url` use the language you asked for.
- Writing takes the same `locale`. See [Languages](../guides/languages.md).

## Errors

Every error says what happened, **how to fix it** and links to its page:

```json
{
  "error": {
    "code": "invalid_key",
    "message": "The key does not exist or was revoked.",
    "fix": "Check that you copied all of it (it starts with pluma_). If it was revoked, create a new one in Keys.",
    "doc_url": "https://pluma.so/docs/errors/invalid_key"
  }
}
```

The full list is in [Errors](../errors/index.md).

## Cache

- With a `delivery` key: `Cache-Control: public, max-age=60, s-maxage=300` and `ETag`. Send `If-None-Match` and you get `304` if nothing changed.
- With `preview` or `management`: `Cache-Control: private, no-store`.

## Limits

- **Per token:** up to **600 requests per minute** (API and MCP together). Every response has `X-RateLimit-Limit` and `X-RateLimit-Remaining`. If you go over, you get [`rate_limited`](../errors/rate_limited.md) with `Retry-After` in seconds.
- **Per site:** 1 million API calls per month. Today they are counted; the warning and the limit arrive with billing.
