---
title: "Changes"
method: GET
path: "/api/v2/changes"
tags: ["Changes"]
---

# Changes

`GET /api/v2/changes`

## Request

- Base URL: `https://www.onlyworlds.com`
- URL: `https://www.onlyworlds.com/api/v2/changes`
- Auth: one of:
  - API key in header `API-Key`
  - API key in header `API-Key` and API key in header `API-Pin`

Every write to the world, in order: each change is an `upsert` (with the
element) or a `delete`, with its type, id and `change_seq`. Pass the
response's `cursor` back as `since` until `has_more` is false, then poll
with it to stay in sync. Any other query parameter is a 422.

## Query parameters

- `since` string
- `limit` integer
- `head` boolean

## Response `200`

A page of the change feed, oldest change first (by change_seq, then id).

- ChangesPage: One page of the feed. To sync: start without `since` (or since=0), apply each change in order, pass `cursor` back as `since` until `has_more` is false, then keep polling with it. On 409 resync_required, pull again from since=0.
  - `cursor` union, required: Pass back as `since`, as is (treat it as opaque).
    - string
    - integer
  - `changes` union[], required: An `upsert` carries the whole element; a `delete` carries only its type, id, deleted_at, and who deleted it through which door (deleted_by, deleted_via). A `delete` also says why (`reason`): `deleted`, or `hidden` for an element hidden from you. A hidden row reaches you only for a hide made after your view last started (you joined, or your epoch last moved), whatever cursor you send. It may still name an id you never held (an element made and hidden while you were not reading): drop it like any delete of an unknown id.
    - union
      - object
        - `op` 'upsert', required
        - `type` 'ability' | 'collective' | 'character' | 'construct' | 'creature' | 'event' | 'family' | 'institution' | 'language' | 'law' | 'location' | 'map' | 'marker' | 'narrative' | 'object' | 'phenomenon' | 'pin' | 'relation' | 'species' | 'title' | 'trait' | 'zone', required
        - `id` string, uuid, required
        - `change_seq` integer, required
        - `updated_at` string, date-time, nullable, required
        - `element` object, required: The element as GET /{type}/{id} returns it (the schema named by `type`, capitalized: Character, Location...).
      - object
        - `op` 'delete', required
        - `type` 'ability' | 'collective' | 'character' | 'construct' | 'creature' | 'event' | 'family' | 'institution' | 'language' | 'law' | 'location' | 'map' | 'marker' | 'narrative' | 'object' | 'phenomenon' | 'pin' | 'relation' | 'species' | 'title' | 'trait' | 'zone', required
        - `id` string, uuid, required
        - `change_seq` integer, required
        - `deleted_at` string, date-time, nullable, required
        - `deleted_by` string, uuid, nullable: The world membership that deleted it, or null (the owner, the server, or a delete from before doors were recorded).
        - `deleted_via` 'seat' | 'mcp' | 'declared' | 'api' | 'v1' | 'sync' | 'server' | 'null', nullable: The door of the delete, as an element's `changed_via`.
        - `reason` 'deleted' | 'hidden' | 'out_of_view', required: Why the element left your copy. `deleted`: it was deleted (a tombstone). `hidden`: the world's owner or a co-builder hid it from you; remove your copy, and never offer to keep it (the row's deleted_at, deleted_by and deleted_via say when, who and through which door it was hidden; a GET of it is 404, never 410). `out_of_view` is reserved: nothing sends it yet.
  - `has_more` boolean, required
  - `head` integer, required: The world's current change_seq. A cursor past it is stale (the world was rewound): pull again from since=0.
  - `epoch` integer, nullable, required: A guest's view epoch (the third part of its cursor), null for everyone else but a contributor's (person or seat), which carries one too. It moves when the reader's view changes with no change_seq move (a Character given or taken, a role, an element hidden from a guest or unhidden): a tip whose epoch differs from the one you last applied means walk again, and the walk answers 409 resync_required. Read it here, never out of the cursor.
  - `epoch_reason` 'role' | 'view' | 'null', nullable, required: Why `epoch` last moved, beside it (null when `epoch` is null). `role`: your own role or membership moved; keep what you hold that a walk from since=0 does not name. `view`: what you may see moved (a delete, a hide, an unhide, a Character given or taken); drop it. `view` too for an epoch that never moved. It names the last move only: when your tip is more than one epoch past the one you applied, walk with your cursor, and the 409's `reason` covers every move since it (`role` if any was). A tool never empties a copy on its own.

## Other responses

- `401`: Missing, invalid or revoked credential, or a wrong PIN (authentication_error; code key_revoked for a revoked key, invalid_credentials otherwise).
- `409`: A guest's view of this world changed since its cursor (or the cursor's shape is not this caller's: a guest's is seq:id:epoch, everyone else's seq:id): pull again from since=0 and keep what you hold that the new walk does not name (code resync_required). Where the server has the hidden mark switched on, a contributor's cursor is seq:id:epoch too, and the first call with a contributor's older seq:id cursor is this 409. There the envelope also says why, in `reason`: `role` (your own role or membership moved: keep what the new walk does not name) or `view` (what you may see moved: drop it); both since your cursor, `role`. A tool never empties a copy on its own.
- `422`: Invalid request: an unknown or malformed query parameter, cursor or id (invalid_request); `param` names the culprit.
- `429`: Too many failed PIN attempts; see the Retry-After header (rate_limited). Or too many requests: over the request budget (code too_many_requests, type rate_limited): 300 requests a minute per key or account token, 60 a minute per address for a request with neither. Retry after the Retry-After header's seconds.
- `503`: The server is busy: every request slot stayed full for 10 s (code server_busy, type api_error). Retry after the Retry-After header's seconds.

## Changes

- **2026-10-20** `78df39c4c62e`: 5 warning
  - added the new `known_of_not_writable` enum value to the `error/code` response property for the response status `401`
  - added the new `known_of_not_writable` enum value to the `error/code` response property for the response status `409`
  - added the new `known_of_not_writable` enum value to the `error/code` response property for the response status `422`
  - added the new `known_of_not_writable` enum value to the `error/code` response property for the response status `429`
  - …1 more
- **2026-10-20** `4dd5b1cd0687`: 10 warning
  - added the new `character_taken` enum value to the `error/code` response property for the response status `401`
  - added the new `character_taken` enum value to the `error/code` response property for the response status `409`
  - added the new `character_taken` enum value to the `error/code` response property for the response status `422`
  - added the new `character_taken` enum value to the `error/code` response property for the response status `429`
  - …6 more

[Operation history](https://skmtc.dev/onlyworlds/apis/onlyworlds-api/changes/api/v2/changes/get.md)

---

[API](https://skmtc.dev/onlyworlds/apis/onlyworlds-api.md) · [All operations](https://skmtc.dev/onlyworlds/apis/onlyworlds-api/llms.txt) · [OpenAPI document](https://skmtc.dev/onlyworlds/apis/onlyworlds-api/revisions/78df39c4c62e?raw)
