# API Conventions

Behaviors that hold across every Playbook API endpoint. If you are building an integration, or an AI agent, against Playbook, treat this page as ground truth.

## Reading these docs as an LLM or agent[​](#reading-these-docs-as-an-llm-or-agent "Direct link to Reading these docs as an LLM or agent")

Every documentation page is available as clean Markdown — **append `.md` to any doc URL**. For example, the MCP guide at `/docs/guides/mcp` is also served at `/docs/guides/mcp.md`. There is also an `/llms.txt` index of every page and an `/llms-full.txt` with the entire docs corpus in one file.

## Base URL[​](#base-url "Direct link to Base URL")

All REST endpoints are served under:

```
https://api.playbook.com/v1
```

Most endpoints are scoped to an organization (workspace) slug, for example `/v1/{org}/assets`.

## Authentication[​](#authentication "Direct link to Authentication")

Every request authenticates with a **Bearer token** in the `Authorization` header:

```
Authorization: Bearer YOUR_TOKEN
```

Create and manage tokens yourself in the Playbook app, with no approval step. See [Getting Started](/docs/getting_started.md).

### Scopes[​](#scopes "Direct link to Scopes")

Tokens carry scopes, enforced at the API layer:

* **`read`** lists and fetches boards, assets, comments, and search results.
* **`write`** creates, updates, moves, deletes, uploads, comments, shares, and publishes.

A write call made with a read-only token returns `403`. Never ship an API token in client-side code or paste it into a chat with an AI model. Configure it once as a request header. Code that runs in a browser uses a board access token instead (below).

### Tokens for browser code[​](#tokens-for-browser-code "Direct link to Tokens for browser code")

An API token reads and writes everything its owner can, and anything in a web page can be read by its visitors. So for code that runs in a browser — the [gallery SDK](/docs/guides/sdk.md) or your own — keep the API token on your server and exchange it there for a **board access token**:

```
curl -X POST https://api.playbook.com/v1/your-org/access_tokens \

  -H "Authorization: Bearer $PLAYBOOK_TOKEN" \

  -H "Content-Type: application/json" \

  -d '{"board_token": "BOARD_TOKEN", "expires_in": 900}'
```

```
{

  "data": {

    "access_token": "…",

    "expires_in": 900,

    "expires_at": "2026-10-02T13:15:00Z",

    "board_token": "BOARD_TOKEN"

  }

}
```

Hand `access_token` to the browser. That token:

* is read-only and expires after `expires_in` seconds (60 to 3600; 3600 if you omit it);
* reads one board and its sub-boards, through `GET /boards`, `GET /boards/{token}`, `GET /boards/{token}/children`, `GET /boards/{token}/assets`, `GET /assets`, `GET /search` and `GET /ai_search`;
* gets `403` with the code `board_scoped_token` on every other endpoint, and cannot create further tokens;
* shows what the API token's owner can see on that board, unapproved assets included, so point it at a board whose whole contents may be shown;
* keeps working until it expires, even if the API token it came from is revoked.

Only an API token created on the Developer page can be exchanged. Each exchange counts as one API request and each board access token has its own rate limit, so reuse one for a visitor's whole session rather than creating one per page view. Full reference: [Create Board Access Token](/docs/api/create-organization-board-access-token.md).

## Response envelope[​](#response-envelope "Direct link to Response envelope")

Successful responses wrap the payload in a top-level `data` key:

```
{ "data": { "token": "abc123", "title": "Hero shot" } }
```

List endpoints return `data` as an array. Always read from `data`.

## Status codes and errors[​](#status-codes-and-errors "Direct link to Status codes and errors")

| Code  | Meaning                                                                                                                                                          |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | Success.                                                                                                                                                         |
| `401` | Authentication failed. Check your Bearer token.                                                                                                                  |
| `403` | Permission denied. Check the token's `read` / `write` scopes. A board access token gets this, with the code `board_scoped_token`, outside its board's endpoints. |
| `404` | Not found. Check the organization slug, board token, or asset token.                                                                                             |
| `406` | Invalid input (used by batch URL ingest validation).                                                                                                             |
| `422` | Validation error. The response body explains which field failed.                                                                                                 |
| `402` | Monthly request quota exhausted (see below).                                                                                                                     |
| `429` | Rate limited. Back off and retry (see below).                                                                                                                    |
| `5xx` | Playbook API error. Retry with backoff.                                                                                                                          |

Error responses include a human-readable message. Tokens are never echoed back in error bodies.

Every error body uses the same envelope, with a machine-readable `code`:

```
{

  "errors": [

    {

      "message": "API quota exhausted: 20000/20000 requests this period",

      "extensions": { "code": "api_quota_exceeded" }

    }

  ]

}
```

## Rate limiting[​](#rate-limiting "Direct link to Rate limiting")

Requests are throttled per token at **120 requests per minute**, the same for every plan. Exceeding it returns `429 Too Many Requests`. Every API response, not only a `429`, carries the `RateLimit-*` headers, so a bulk job can pace itself from `RateLimit-Remaining` instead of discovering the limit by hitting it:

| Header                | Meaning                                                    |
| --------------------- | ---------------------------------------------------------- |
| `RateLimit-Limit`     | Requests allowed per window (`120`).                       |
| `RateLimit-Remaining` | Requests left in this window; `0` on a throttled response. |
| `RateLimit-Reset`     | Seconds until the window resets.                           |
| `Retry-After`         | Seconds to wait before retrying. `429` only.               |

The `429` JSON body also carries `extensions.code: "rate_limit_exceeded"` and a message naming the limit and the retry delay, e.g. `Rate limit of 120 requests per 60s exceeded. Retry in 23s.` Wait at least `Retry-After` seconds before retrying; use exponential backoff only as additional protection for repeated `429` or `5xx` responses.

Rate limiting is short-term and independent of the monthly quota below. A `429` clears within a minute; a `402` does not.

## Monthly request quota[​](#monthly-request-quota "Direct link to Monthly request quota")

Every request to an organization-scoped endpoint (any path carrying a `{slug}`) counts against that organization's monthly allowance. Requests that fail validation or authorization **still count** — a client burning its allowance on malformed requests is generating real load.

| Plan       | Included requests / month | Ceiling with usage billing |
| ---------- | ------------------------- | -------------------------- |
| Free       | 1,000                     | —                          |
| Pro        | 20,000                    | 200,000                    |
| Team       | 200,000                   | 1,000,000                  |
| Business   | 5,000,000                 | —                          |
| Enterprise | Custom                    | Custom                     |

See [pricing](https://www.playbook.com/pricing) for full plan details.

### Quota headers[​](#quota-headers "Direct link to Quota headers")

Every response from an organization-scoped endpoint carries the current state:

| Header                  | Meaning                                                                                                       |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `X-API-Quota-Limit`     | Requests included in the plan this period.                                                                    |
| `X-API-Quota-Remaining` | Requests left in the included allowance. Reaches `0` and stays there once usage crosses into the billed band. |
| `X-API-Quota-Reset`     | When the counter rolls over (ISO 8601).                                                                       |

Read `X-API-Quota-Remaining` and slow down before you hit zero rather than reacting to the `402`.

### When the quota runs out[​](#when-the-quota-runs-out "Direct link to When the quota runs out")

Requests return **`402`** with the code `api_quota_exceeded` and a `Retry-After` header holding the seconds until the counter resets.

`402` rather than `429` is deliberate: an exhausted monthly quota stays exhausted for days, so a client that treats it as a `429` and retries with backoff would hammer the API for the rest of the period without ever succeeding. **Do not retry a `402`** — either wait for `X-API-Quota-Reset` or raise the ceiling.

### Usage billing[​](#usage-billing "Direct link to Usage billing")

On Pro and Team, a workspace admin can lift the wall from the included allowance to the ceiling by enabling **usage billing** on the Developers tab in the Playbook app. It requires a payment method on file. Requests beyond the included allowance are then billed per request on the next invoice, alongside seats and storage — there is no separate invoice.

Until it is enabled, the included allowance **is** the wall. Nothing is ever billed without that explicit opt-in.

### When the counter resets[​](#when-the-counter-resets "Direct link to When the counter resets")

The window follows the workspace's monthly billing date, so it is monthly even on an annual plan. Workspaces without a paid subscription reset on the first of the calendar month. `X-API-Quota-Reset` always carries the authoritative timestamp — do not assume the 1st.

## Pagination[​](#pagination "Direct link to Pagination")

List endpoints accept `page` and `per_page` query parameters:

```
GET /v1/{org}/assets?page=2&per_page=50
```

`GET /v1/{org}/assets` also supports stable keyset pagination for a complete workspace crawl:

```
GET /v1/{org}/assets?cursor=true&per_page=100

GET /v1/{org}/assets?cursor=true&per_page=100&after_cursor=OPAQUE_VALUE
```

Read `pagy.has_next_page` to decide whether to continue, and pass `pagy.after_cursor` back unchanged. Cursor mode uses the default descending `created_at` order and cannot be combined with `page` or ascending sort. It is a **full crawl and reconcile mechanism**, not a change feed: it does not report deletions or only the records changed since the last crawl.

## Async ingest[​](#async-ingest "Direct link to Async ingest")

Uploads are **asynchronous**. Creating an asset (from a URL, a batch of URLs, or a signed upload) returns immediately with a **skeleton asset**. The row exists, but the bytes are still being fetched and processed.

Poll `GET /v1/{org}/assets/{token}` until `is_skeleton` is `false`, then check:

* `media_type` populated means the asset processed successfully.
* `source_error` non-null means ingest failed (the message is the latest worker error).
* `is_link: true` means it was kept as a bare link (only when `as_link: true` was passed).

For batch URL ingest, pass a `uuid` per item to correlate each async result back to your request. See the [Upload guide, Async Ingest Semantics](/docs/guides/upload.md#async-ingest-semantics) for the full contract and recommended polling cadence.

## Driving Playbook from an AI agent[​](#driving-playbook-from-an-ai-agent "Direct link to Driving Playbook from an AI agent")

Everything above is also available to AI assistants through the hosted **MCP server**, which exposes the same operations as tools, with the same scopes and the same async semantics, and can read these docs at runtime via `read_documentation`. See the [MCP guide](/docs/guides/mcp.md).
