# API reference

The OnlyWorlds API reads and writes world data on onlyworlds.com over HTTPS, in JSON. Two dialects share one host:

- **`/api/v2/`**: cursor pagination, flat UUID link arrays, create and upsert, bulk writes and a change feed. Use it for new work. Every page in this section describes it unless it says otherwise.
- **`/api/worldapi/`**: the [Classic API](/docs/development/api/classic), the original dialect. It stays unchanged and supported for existing clients.

**Base URL**: `https://www.onlyworlds.com/api/v2/`. `GET /api/` and `GET /api/v2/` answer a small JSON index (the interactive reference, the OpenAPI document, the health check and these docs), with no key.

**Interactive reference**: [onlyworlds.com/api/docs](https://www.onlyworlds.com/api/docs) · **OpenAPI document**: [onlyworlds.com/api/v2/openapi.json](https://www.onlyworlds.com/api/v2/openapi.json)

A first read and write, from minting the key to the reply, is on [Getting started](/docs/getting-started/#your-first-call).

## Authentication

Every request carries a world key in the `API-Key` header. Writes also carry the PIN in `API-Pin`. The key alone decides the world: no world id goes in a path or a body. The one exception is a world its owner opened to everyone: its reads take no key and name the world as `?world=<id>` ([Public worlds](/docs/development/api/public#the-keyless-reads)).

```bash
curl -s "https://www.onlyworlds.com/api/v2/world" \
  -H "API-Key: {key}"
```

Key types, PIN rules, member keys and account tokens are on [Keys and PINs](/docs/getting-started/keys).

### Which credential each route takes

**506 operations in 15 route families.** Each counts under every credential the API document lists for it. 47 take no key as well as a key or an account token: the [keyless reads](/docs/development/api/public#the-keyless-reads) of a world open to anyone.

| Routes | No key | Key alone | Key and PIN | Account token | All |
|:--|--:|--:|--:|--:|--:|
| `/{type}` | 45 | 155 | 441 | 45 | 441 |
| `/account` |  |  |  | 38 | 38 |
| `/world` | 1 | 5 | 6 | 2 | 6 |
| `/links` |  | 1 | 3 |  | 3 |
| `/me` |  | 1 | 3 | 1 | 3 |
| `/auth` | 2 |  |  |  | 2 |
| `/files` | 2 |  |  |  | 2 |
| `/health` | 2 |  |  |  | 2 |
| `/join` | 2 |  |  |  | 2 |
| `/media` |  |  | 2 |  | 2 |
| `/bulk` |  |  | 1 |  | 1 |
| `/changes` |  | 1 | 1 |  | 1 |
| `/members` |  | 1 | 1 | 1 | 1 |
| `/public` | 1 |  |  |  | 1 |
| `/search` | 1 | 1 | 1 | 1 | 1 |
| All | 56 | 165 | 459 | 88 | 506 |

Each route, by the credentials it takes:

- **`/{type}`**, No key, Key alone, Key and PIN, Account token: `GET /character/{id}/context`, `GET /{type}` (×22), `GET /{type}/{id}` (×22)
- **`/{type}`**, Key alone, Key and PIN: `GET /{type}/{id}/files` (×22), `GET /{type}/{id}/files/{file_id}/download` (×22), `GET /{type}/{id}/proposals` (×22), `POST /{type}/{id}/proposals` (×22), `DELETE /{type}/{id}/proposals/{pid}` (×22)
- **`/{type}`**, Key and PIN: `POST /{type}` (×22), `DELETE /{type}/{id}` (×22), `PATCH /{type}/{id}` (×22), `PUT /{type}/{id}` (×22), `POST /{type}/{id}/adopt` (×22), `POST /{type}/{id}/files` (×22), `DELETE /{type}/{id}/files/{file_id}` (×22), `DELETE /{type}/{id}/hidden` (×22), `PUT /{type}/{id}/hidden` (×22), `POST /{type}/{id}/links/{field}` (×22), `POST /{type}/{id}/proposals/{pid}/accept` (×22), `POST /{type}/{id}/proposals/{pid}/decline` (×22), `POST /{type}/{id}/review` (×22)
- **`/account`**, Account token (or `X-Session-Token`): `GET /account`, `PATCH /account`, `GET /account/allowance`, `POST /account/ask`, `DELETE /account/clients/{client_id}`, `GET /account/invites`, `POST /account/invites/{invite_id}/accept`, `POST /account/invites/{invite_id}/refuse`, `POST /account/join`, `POST /account/sort`, `GET /account/tokens`, `POST /account/tokens`, `DELETE /account/tokens/{token_id}`, `GET /account/watched`, `POST /account/watched`, `DELETE /account/watched/{watch_id}`, `GET /account/worlds`, `POST /account/worlds`, `POST /account/worlds/handover`, `GET /account/worlds/holding/{narrative_id}`, `POST /account/worlds/{world_id}/invites`, `DELETE /account/worlds/{world_id}/invites/{invite_id}`, `GET /account/worlds/{world_id}/keys`, `POST /account/worlds/{world_id}/keys`, `DELETE /account/worlds/{world_id}/keys/{key_id}`, `POST /account/worlds/{world_id}/leave`, `GET /account/worlds/{world_id}/links`, `POST /account/worlds/{world_id}/links`, `DELETE /account/worlds/{world_id}/links/{link_id}`, `GET /account/worlds/{world_id}/members`, `DELETE /account/worlds/{world_id}/members/{member_id}`, `PATCH /account/worlds/{world_id}/members/{member_id}`, `POST /account/worlds/{world_id}/messages`, `POST /account/worlds/{world_id}/read`, `GET /account/worlds/{world_id}/seats`, `POST /account/worlds/{world_id}/seats`, `DELETE /account/worlds/{world_id}/seats/{seat_id}`, `POST /account/worlds/{world_id}/seats/{seat_id}/key`
- **`/world`**, No key, Key alone, Key and PIN, Account token: `GET /world`
- **`/world`**, Key alone, Key and PIN, Account token: `GET /world/{world_id}/lenses`
- **`/world`**, Key alone, Key and PIN: `GET /world/files`, `GET /world/hidden`, `GET /world/proposals`
- **`/world`**, Key and PIN: `PATCH /world`
- **`/links`**, Key alone, Key and PIN: `GET /links`
- **`/links`**, Key and PIN: `POST /links`, `DELETE /links/{link_id}`
- **`/me`**, Key alone, Key and PIN: `GET /me`
- **`/me`**, Key and PIN, Account token: `PUT /me/lenses`
- **`/me`**, Key and PIN: `PUT /me/character`
- **`/auth`**, No key: `GET /auth/clients`, `POST /auth/token`
- **`/files`**, No key: `POST /files/removed`, `POST /files/uploaded`
- **`/health`**, No key: `GET /health`, `GET /health/deep`
- **`/join`**, No key: `POST /join`, `POST /join/preview`
- **`/media`**, Key and PIN: `POST /media/remove-ticket`, `POST /media/ticket`
- **`/bulk`**, Key and PIN: `POST /bulk`
- **`/changes`**, Key alone, Key and PIN: `GET /changes`
- **`/members`**, Key alone, Key and PIN, Account token: `GET /members`
- **`/public`**, No key: `GET /public/counts`
- **`/search`**, No key, Key alone, Key and PIN, Account token: `GET /search`

**No key**: the document lists no credential. **Key alone**, as the document declares it: a legacy 10-digit key sends its PIN on every read of a world that has one ([Keys and PINs](/docs/getting-started/keys#the-pin)). **Account token**: 38 of these also take an app's `X-Session-Token` in its place ([Account tokens](/docs/getting-started/keys#account-tokens)).

Generated from the API's own document (`openapi.json`): the security each operation lists, as read on 2026-11-01. Paths are under `/api/v2/`; `{type}` is any of the 22 element types.

## Response shapes

A list answers in an envelope:

```json
{ "data": [], "has_more": false, "next_cursor": null }
```

A single element answers as the bare element object. Every error answers in one envelope, `{"error": {"type", "code", "message", "param", "doc_url"}}`, described on the [error reference](/api/errors/).

## Resources

`{type}` is one of the [22 element types](/docs/schema/), by its name in lowercase: `character`, `location`, and so on (the OpenAPI document enumerates them). The URL names the element type; the key names the world.

| Method | Route | Purpose | Page |
|:--|:--|:--|:--|
| GET | `/api/v2/{type}` | List elements (paginated, filterable) | [Reads](/docs/development/api/reads) |
| GET | `/api/v2/{type}/{id}` | Get one element | [Reads](/docs/development/api/reads) |
| POST | `/api/v2/{type}` | Create | [Writes](/docs/development/api/writes) |
| PUT | `/api/v2/{type}/{id}` | Create or replace by id (upsert) | [Writes](/docs/development/api/writes) |
| PATCH | `/api/v2/{type}/{id}` | Partial update | [Writes](/docs/development/api/writes) |
| DELETE | `/api/v2/{type}/{id}` | Delete | [Writes](/docs/development/api/writes) |
| POST | `/api/v2/{type}/{id}/links/{field}` | Add or remove ids on one multi-link field | [Link fields](/docs/development/api/links) |
| POST | `/api/v2/bulk` | Create and upsert many elements of mixed element types | [Writes](/docs/development/api/writes#bulk) |
| GET | `/api/v2/search` | Find elements by the words in their name and text | [Reads](/docs/development/api/reads#search) |
| GET | `/api/v2/character/{id}/context` | One Character's context in one read | [The draught](/docs/development/api/draught) |
| GET | `/api/v2/changes` | The world's change feed, and full export | [Changes](/docs/development/api/changes) |
| GET | `/api/v2/world` | The world named by the key | [Reads](/docs/development/api/reads#the-world) |
| PATCH | `/api/v2/world` | Update the world's own fields (the owner, or a co-builder for every writable field but `owner_character`) | [Writes](/docs/development/api/writes#the-world) |
| POST | `/api/v2/{type}/{id}/adopt` | Change who made an element (the owner and co-builders) | [Members](/docs/development/api/members#created_by) |
| GET | `/api/v2/members` | The world's roster | [Members](/docs/development/api/members) |
| GET | `/api/v2/me` | Who the calling key is | [Members](/docs/development/api/members) |
| PUT | `/api/v2/me/character` | A person member picks the Character that is them | [Members](/docs/development/api/members#picking-your-character) |
| PUT | `/api/v2/me/lenses` | Choose the lenses you start with | [Lenses on the API](/docs/development/api/lenses) |
| GET | `/api/v2/world/{world_id}/lenses` | The world's lenses, resolved for you | [Lenses on the API](/docs/development/api/lenses) |
| GET, POST | `/api/v2/{type}/{id}/proposals` | An element's proposals; propose a change | [Proposed changes](/docs/development/api/proposals#propose) |
| POST, DELETE | `/api/v2/{type}/{id}/proposals/{pid}` (`/accept`, `/decline`) | Accept, decline or withdraw a proposal | [Proposed changes](/docs/development/api/proposals#accept-decline-withdraw) |
| GET | `/api/v2/world/proposals` | The world's proposals (the queue) | [Proposed changes](/docs/development/api/proposals#the-queue) |
| POST | `/api/v2/{type}/{id}/review` | Accept or decline a proposed new element | [Proposed changes](/docs/development/api/proposals) |
| GET, POST, DELETE | `/api/v2/links`, `/api/v2/links/{link_id}` | A co-builder's invite links | [Members](/docs/development/api/members#a-co-builders-links) |
| POST | `/api/v2/media/ticket` | A ticket for one image upload | [Images](/docs/development/api/images) |
| POST | `/api/v2/media/remove-ticket` | A ticket to remove one uploaded image | [Images](/docs/development/api/images#removing-an-image) |
| GET, POST | `/api/v2/{type}/{id}/files` | An element's files; a ticket for one file upload | [Files](/docs/development/api/files) |
| GET | `/api/v2/{type}/{id}/files/{file_id}/download` | A download link for one file | [Files](/docs/development/api/files#downloading-a-file) |
| DELETE | `/api/v2/{type}/{id}/files/{file_id}` | Remove a file | [Files](/docs/development/api/files#removing-a-file) |
| GET | `/api/v2/world/files` | Every file in the world the key may see, and the world's total | [Files](/docs/development/api/files#listing-files) |
| PUT, DELETE | `/api/v2/{type}/{id}/hidden` | Hide or unhide an element (the owner and co-builders) | [Hidden elements](/docs/development/api/hidden#hiding-and-unhiding) |
| GET | `/api/v2/world/hidden` | The world's hidden elements (the owner and co-builders) | [Hidden elements](/docs/development/api/hidden#hiding-and-unhiding) |
| POST | `/api/v2/join/preview` | Read a join link (agent, person or player) without using it | [Agents](/docs/development/agents) |
| POST | `/api/v2/join` | Redeem an agent's or a player's code, no key | [Agents](/docs/development/agents), [Members](/docs/development/api/members#player-links) |
| GET | `/api/v2/auth/clients` | The tools registered to sign a person in, no key | [Sign in with OnlyWorlds](/docs/development/api/signin#registered-tools) |
| POST | `/api/v2/auth/token` | Trade a sign-in code for an account token, no key | [Sign in with OnlyWorlds](/docs/development/api/signin#trading-the-code) |
| GET | `/api/v2/public/counts` | The platform's totals, no key | [Public worlds](/docs/development/api/public#how-big-onlyworlds-is) |
| GET | `/api/v2/health` | Liveness check, no key needed | |
| GET, PATCH | `/api/v2/account` | The account itself, on an account token | [Sign in with OnlyWorlds](/docs/development/api/signin#the-account) |
| various | `/api/v2/account/...` | Account token routes: worlds, keys, invites, members, links, seats, watched worlds | [Keys and PINs](/docs/getting-started/keys#account-tokens), [Members](/docs/development/api/members#managing-members) |

Trailing slashes are tolerated on every route: `/api/v2/character/{id}` and `/api/v2/character/{id}/` both resolve, with no redirect.

## Limits

| Limit | Value |
|:--|:--|
| Request rate | 300 requests a minute per key or account token, 60 a minute per address for a request with neither. Reads and writes count together, and a `/bulk` call is one request. The minute is the clock's. Every counted reply carries `RateLimit-Limit` (the budget), `RateLimit-Remaining` (what is left this minute) and `RateLimit-Reset` (seconds to the minute's end), errors included. Over the budget: `429` [`too_many_requests`](/api/errors/#too_many_requests) with `Retry-After`. The health check, the [Classic API](/docs/development/api/classic) and the [MCP server](/docs/development/mcp) are not counted |
| Busy server | Under load the server answers `503` [`server_busy`](/api/errors/#server_busy) with a `Retry-After` header: each worker process runs six requests at once, and a request waits up to 10 seconds for a slot |
| Request body | Up to 2.5 MB (2,621,440 bytes). A larger body answers `413` [`payload_too_large`](/api/errors/#payload_too_large) and nothing is written. Split a large `/bulk` into several calls |
| Wrong PINs | Repeated failures lock the key out: `429` [`rate_limited`](/api/errors/#rate_limited) with `Retry-After` |
| `/bulk` | Up to 1000 items per request |
| List pages | `limit` from 1 to 1000, default 100; a larger one is read as 1000. [Search](/docs/development/api/reads#search) takes 1 to 200, default 50, the same way. The change feed defaults to 500 and refuses more than 1000 (`422`) |
| Images | 15 MB an image; 200 upload tickets a world in 24 hours (`429` [`quota_exceeded`](/api/errors/#quota_exceeded)); 1 GB of images per account by default (`403` [`storage_full`](/api/errors/#storage_full)). See [Images](/docs/development/api/images) |
| Files | 25 MB a file, 250 MB a world, 200 file tickets a world in 24 hours. See [Files](/docs/development/api/files#limits) |
| `expand` | One level deep: a stub is `{id, name, supertype, subtype, image_url}`, with no links of its own |

Sizes count on a 1024 base: 1 MB is 1,048,576 bytes and 1 GB is 1,073,741,824 bytes. The API document lists the API's limits, each with its exact value, as `info.x-ow-limits` in [openapi.json](https://www.onlyworlds.com/api/v2/openapi.json).

There are no webhooks. To follow a world's changes, poll [`/changes`](/docs/development/api/changes) with your stored cursor; `?head=true` is the cheap check for whether anything moved.

## Stability

- `/api/v2/` is the current API. The [Classic API](/docs/development/api/classic) at `/api/worldapi/` keeps answering for the tools built on it; it is frozen, not developed.
- New fields and routes arrive without notice and never change what existing fields mean. A field, route or behaviour is removed only after it has been announced on the [feedback page](https://www.onlyworlds.com/feedback)'s changelog and in the [API document](https://www.onlyworlds.com/api/v2/openapi.json), at least six months ahead.
- Every reply's `X-OW-Schema-Version` header names the version of the standard that shapes it.
- Error codes and field names are a contract: a client may branch on them. Error messages are not.
- The standard's files, the TypeScript SDK, the Unity SDK, the toolkit and schema-dist are MIT-licensed.

**Watching the API document.** A tool that watches the document reads it like this: ask for gzip (it is about 3 MB plain, about 300 KB compressed), keep the `ETag` the answer carries, and send it back on the next read in `If-None-Match` exactly as received (it may arrive weak, `W/"…"`; keel accepts either). An unchanged document answers `304` with no body; it also serves `Cache-Control: public, max-age=300`, so nothing is gained by reading it more often than every five minutes.

## API pages

| Page | Covers |
|:--|:--|
| [Keys and PINs](/docs/getting-started/keys) | Key types, the PIN, member and agent-seat keys, account tokens |
| [Reads and pagination](/docs/development/api/reads) | The list envelope, cursors, filters, search, expansion, sparse fields, the world |
| [Link fields](/docs/development/api/links) | How links read and write, and the link operations route |
| [Writes and bulk](/docs/development/api/writes) | Create, upsert, patch, delete, idempotency, bulk |
| [Changes and export](/docs/development/api/changes) | The change feed, full export, guests' cursors |
| [Members and sharing](/docs/development/api/members) | Roles, the roster, guests, invites, player links, agent seats, read keys |
| [Hidden elements](/docs/development/api/hidden) | Hiding an element from everyone but the owner, the co-builders and its creator |
| [The draught](/docs/development/api/draught) | One Character's context in one read, under a policy |
| [Lenses on the API](/docs/development/api/lenses) | A world's lenses and their readings, and the lenses a member starts with |
| [Proposed changes](/docs/development/api/proposals) | Proposing a change to an element, the queue, accepting and declining |
| [Public worlds](/docs/development/api/public) | A world open to everyone: its page and the keyless reads |
| [Sign in with OnlyWorlds](/docs/development/api/signin) | How a registered tool signs a person in through onlyworlds.com and gets an account token |
| [Images](/docs/development/api/images) | Hosting an element's picture on OnlyWorlds |
| [Files](/docs/development/api/files) | Attaching PDFs, text files and world bundles to an element |
| [CORS](/docs/development/api/cors) | Which browser origins may call the API |
| [Classic API](/docs/development/api/classic) | The original `/api/worldapi/` dialect |
| [Error reference](/api/errors/) | Every error code, its cause and its fix |