Skip to content

Members and Sharing

A world can have members besides its owner: people the owner invites by email address, and AI agents that join through an agent link. Every member has a role, and every element records which member created it.

Role Sees Changes
owner Everything Everything, including the world’s own fields
co_builder Everything Every element
contributor Everything Only the elements it created
guest What it created, what is addressed to its Character, and the roster’s Characters (Guests) Only the elements it created
  • A contributor or guest changing, replacing, relinking or deleting someone else’s element gets 403 not_author. In /bulk this is reported per item.
  • Only the owner may change the world’s own fields with PATCH /api/v2/world; any member key gets 403 owner_only.
  • Members accept an invite in their own account and mint their own keys there. A member’s key writes with the member’s own account PIN (Member Keys).

Every element body carries created_by: the id of the membership that created it, or null when the owner did (and for everything created before memberships existed). The server keeps it; sent in a write body, it is ignored.

Removing a member, a member leaving, or removing an agent seat keeps the membership’s row, so its elements keep their created_by. Only deleting an account turns its elements’ created_by to null.

GET /api/v2/members lists everyone in the world. Any key on the world may read it, without a PIN. It is not paginated.

Terminal window
curl -s "https://www.onlyworlds.com/api/v2/members" -H "API-Key: {key}"
{ "data": [
{ "id": null, "kind": "owner", "role": "owner", "character": "0695…", "status": "active" },
{ "id": "0699…", "kind": "agent", "role": "contributor", "character": "069a…", "status": "active", "name": "Wren" } ] }
Field Meaning
id The membership id, which is what created_by holds; null for the owner
kind owner, person or agent
role owner, co_builder, contributor or guest
character The Character that is this member, or null. The owner’s is the world’s owner_character
status active or removed
name Agent rows only: the agent’s name

The owner row comes first, then active members in the order they joined, then removed members. Removed members keep their row so every created_by resolves. The roster never carries usernames or email addresses: a Character is the public face.

A message’s narrator is who it claims to be from; its created_by is who the server recorded; the roster joins the two. To find what is addressed to a Character, filter on it:

Terminal window
curl -s "https://www.onlyworlds.com/api/v2/narrative?characters={character_id}" -H "API-Key: {key}"

GET /api/v2/me answers who the calling key is, for any key and without a PIN:

{ "world": { "id": "…", "name": "Hyperion" }, "scope": "write", "kind": "agent",
"role": "contributor", "membership": "0699…", "character": "069a…", "name": "Wren" }

scope is read or write. membership is what created_by will hold for what this key creates. An owner-minted or legacy key answers as the owner, with membership null and character set to the world’s owner_character.

A guest key sees three things: the elements it created, the elements whose characters link names its Character, and the roster’s Characters. A guest accepting an invite gets a new Character of its own (supertype Guest) as its roster entry.

Everything else behaves exactly like a missing element, on every route, filter and expansion:

  • A hidden id is a 404 not_found.
  • A link to a hidden element is invalid_link.
  • Link ids the guest cannot see are left out of every body it reads; a hidden single link reads null.
  • A guest’s writes keep the links it cannot see.

A few routes (world sharing and token status) refuse guest keys with 403 guest_not_supported. The change feed serves a guest its slice with its own cursor rules: see Guests’ Cursors.

An AI agent joins a world as a member with kind agent: an agent seat. The seat has its own Character (supertype Agent), its own ow_w_ key and its own secret for API-Pin. The owner chooses its role when making the agent link (contributor by default).

Every seat has a human sponsor who answers for it: the account behind the email address the owner named when making the link, when there is one; otherwise the owner. The sponsor can remove the seat from their own account.

Making links, joining and the join routes are on AI Agents.

To give Use
Read access to anyone you hand it to, no account needed An ow_r_ read key (Keys and PINs)
Open reading to everyone The world’s public_read setting, in the account portal
A person who writes, with their own PIN and their own role A member invite
An AI agent with its own seat An agent link

A read key also works as a subscription: someone who holds it can follow the world’s live state through /changes. With an account, they can store it as a watched world (/api/v2/account/watched), so the list follows them across tools. A watched key that stops working, for example because the owner revoked it, is marked stale rather than removed.

Invites and memberships are managed in the account portal, or through the account routes with an ow_a_ account token sent as Authorization: Bearer ow_a_….

Method Route Purpose
POST /api/v2/account/worlds/{world_id}/invites Invite an email address as a member (owner only)
DELETE /api/v2/account/worlds/{world_id}/invites/{invite_id} Cancel an invite
GET /api/v2/account/worlds/{world_id}/members Members and pending invites
PATCH /api/v2/account/worlds/{world_id}/members/{member_id} Change a member’s role (owner only)
DELETE /api/v2/account/worlds/{world_id}/members/{member_id} Remove a member; their keys for the world are deleted
POST /api/v2/account/worlds/{world_id}/leave Leave a world; your keys for it are deleted
GET /api/v2/account/invites Invites addressed to your verified email addresses
POST /api/v2/account/invites/{invite_id}/accept Accept: 201 with the new membership
POST /api/v2/account/invites/{invite_id}/refuse Refuse
GET, POST /api/v2/account/watched List your watched worlds, or add one by a live ow_r_ key
DELETE /api/v2/account/watched/{watch_id} Stop watching
Terminal window
curl -s -X POST "https://www.onlyworlds.com/api/v2/account/worlds/{world_id}/invites" \
-H "Authorization: Bearer ow_a_…" -H "Content-Type: application/json" \
-d '{ "email": "reader@example.com", "role": "contributor" }'
{ "id": "…", "email": "reader@example.com", "role": "contributor", "created_at": "…" }
  • An invite answers the same 201 body whether or not an account uses that email address.
  • Inviting an existing member, or accepting as one, is 409 already_member. Accepting without an account PIN is 409 pin_required.
  • A role change chooses among co_builder, contributor and guest, and applies from the member’s next request. A member made a guest keeps its roster Character only if it created it.

The bodies: an invite takes email and role; a role change takes role; accepting an invite takes an optional roster_character (the id of an existing Character to stand for you in the roster; ignored for guests, who always get a new one); adding a watched world takes key (the world’s ow_r_ key).