Browse the reference
Account key · /v1/api
People, roles and what each may reach
Two things decide what a person can do. Their role in the organization says how much, and a grant on each Concept says where. Being a member reaches no Concept by itself. Only the owner reaches everything, because they own it.
A key may change both, and may invite. It may not remove anybody. Ending a membership cannot be undone, and a person can do it themselves in a moment.
What somebody can do in a Concept is the lesser of the two.
The organization
Who the key is, the organization, and who is in it.
People and access
Roles, which Concepts a member reaches, and invitations.
Agents
Create agents, admit them, lend them a Concept, revoke a key.
Members
Find a member's userId in the member list. Every change here is written to the organization's log, with the key that made it.
Change a member's role
PUT/v1/api/o/members/{userId}
Sets what a member may do across the organization. It takes effect on their next request.
Body
- roleEDITOR · WRITER · READER · required
- A reader reads what is approved. A writer drafts and proposes. An editor also reviews and publishes.
Refusals
- 404not_foundNobody with that id is a member.
- 403insufficient_roleThe member is the owner, or the role asked for is owner.
curl -X PUT \
https://your-server.example/v1/api/o/members/8b7a6f5e-4d3c-4b2a-9180-7f6e5d4c3b2a \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "role": "EDITOR" }'{
"ok": true,
"role": "EDITOR"
}Read a member's Concept access
GET/v1/api/o/members/{userId}/concepts
The Concepts this member has been granted, by name, with the role held in each. Names only. Nothing of what a Concept holds.
What they can actually do in one is the lesser of this role and their role in the organization.
curl https://your-server.example/v1/api/o/members/8b7a6f5e-…/concepts \
-H "Authorization: Bearer arb_live_…"[
{
"conceptId": "3c2b1a09-8f7e-4d6c-b5a4-9e8d7c6b5a41",
"slug": "coastal-atlas",
"name": "Coastal atlas",
"role": "WRITER",
"grantedAt": "2026-08-19T14:02:10.000Z"
}
]Set a member's Concept access
PUT/v1/api/o/members/{userId}/concepts
Granting and withdrawing are one call. The body is the complete set: a Concept left out of it loses access, and an empty list withdraws everything.
That makes it safe to send twice, and means a client states what the access should be and never has to work out a difference.
Body
- conceptsarray · required
- Every Concept the member should reach. May be empty.
- concepts[].conceptIduuid · required
- The Concept's id, which is not its address. Both are in the list of reachable Concepts.
- concepts[].roleOWNER · EDITOR · WRITER · READER · required
- The most they may do in that Concept.
Refusals
- 404not_foundNo such member, or one of the Concepts is not in this organization, is archived, or is deleted.
- 403insufficient_roleThe member is the owner. Their reach is not held in grants, so there is nothing to edit.
curl -X PUT \
https://your-server.example/v1/api/o/members/8b7a6f5e-…/concepts \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{
"concepts": [
{ "conceptId": "3c2b1a09-8f7e-4d6c-b5a4-9e8d7c6b5a41", "role": "WRITER" },
{ "conceptId": "7d6c5b4a-3f2e-4d1c-8b0a-9f8e7d6c5b4a", "role": "READER" }
]
}'[
{
"conceptId": "3c2b1a09-8f7e-4d6c-b5a4-9e8d7c6b5a41",
"slug": "coastal-atlas",
"name": "Coastal atlas",
"role": "WRITER",
"grantedAt": "2026-10-09T11:20:00.000Z"
},
{
"conceptId": "7d6c5b4a-3f2e-4d1c-8b0a-9f8e7d6c5b4a",
"slug": "tide-tables",
"name": "Tide tables",
"role": "READER",
"grantedAt": "2026-10-09T11:20:00.000Z"
}
]The access as it now stands.
Allow working through agents
PUT/v1/api/o/members/{userId}/agent-use
Decides whether a member may do their work here through agents of their own. It says how somebody may act and never what they may do. A writer with it is still a writer, and so is their agent.
Body
- canUseAgentsboolean · required
- On, and the member's agents may borrow the member's standing. Off, and an agent working for them is refused with
agent_use_not_enabled.
curl -X PUT \
https://your-server.example/v1/api/o/members/8b7a6f5e-…/agent-use \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "canUseAgents": true }'{
"ok": true,
"canUseAgents": true
}Invitations
Somebody joins by being invited and accepting. An invitation names a person who already has an account, the role offered, and the Concepts that come with it. Nobody is in the organization until they accept, which is why a key may send one.
Accepting and declining are the invited person's to do, signed in. They are not on this surface.
sent
PENDING
May be changed or cancelled. One waiting for each person.
ACCEPTED
by the person invited
DECLINED
by the person invited
CANCELLED
by the organization
EXPIRED
by time
Send an invitation
POST/v1/api/invitations
Offers a person a place. They are told by email, and the invitation waits on their own page whether or not the email arrives. It is open for three hours, and expiresAt says until when.
Body
- usernamestring · required
- Who to invite, by username. They must already have an account.
- roleEDITOR · WRITER · READER · required
- Their role in the organization. Never more than the owner of the key holds.
- conceptsarray · at least 1 · required
- The Concepts that come with the invitation, each with a role. An invitation that reaches nothing is refused.
Refusals
- 409already_a_memberThey are already in the organization, or the key's owner invited themselves.
- 409conflictThey already have an invitation waiting. One at a time for each person.
- 409no_active_conceptsThe organization has no Concept to grant yet. Create one first.
- 409account_not_activeTheir account is pending, suspended or disabled.
- 400validation_failedA Concept named is not in this organization or is archived, or the role is above the inviter's.
curl -X POST https://your-server.example/v1/api/invitations \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{
"username": "ana",
"role": "WRITER",
"concepts": [
{ "conceptId": "3c2b1a09-8f7e-4d6c-b5a4-9e8d7c6b5a41", "role": "WRITER" }
]
}'{
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"organization": { "id": "…", "slug": "harbour-works", "name": "Harbour Works" },
"invitedUser": { "id": "…", "username": "ana", "displayName": "Ana Reyes" },
"invitedBy": { "id": "…", "username": "dmitri", "displayName": "Dmitri Okafor" },
"role": "WRITER",
"status": "PENDING",
"concepts": [
{
"conceptId": "3c2b1a09-8f7e-4d6c-b5a4-9e8d7c6b5a41",
"slug": "coastal-atlas",
"name": "Coastal atlas",
"role": "WRITER"
}
],
"expiresAt": "2026-10-09T14:25:00.000Z",
"createdAt": "2026-10-09T11:25:00.000Z"
}invitedBy is the key's owner. The person invited sees somebody they can ask.
List invitations
GET/v1/api/invitations
Every invitation the organization has sent, newest first.
Query
- statusPENDING · ACCEPTED · DECLINED · EXPIRED · CANCELLED
- Only those in this state. All of them when absent.
curl "https://your-server.example/v1/api/invitations?status=PENDING" \
-H "Authorization: Bearer arb_live_…"[
{
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"invitedUser": { "id": "…", "username": "ana", "displayName": "Ana Reyes" },
"role": "WRITER",
"status": "PENDING",
"concepts": [ { "slug": "coastal-atlas", "role": "WRITER", … } ],
"expiresAt": "2026-10-09T14:25:00.000Z",
…
}
]Read an invitation
GET/v1/api/invitations/{id}
One invitation, in whatever state it has reached. Poll it to learn when somebody has answered.
Refusals
- 404not_foundNo such invitation in this organization.
curl https://your-server.example/v1/api/invitations/a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d \
-H "Authorization: Bearer arb_live_…"{
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"invitedUser": { "id": "…", "username": "ana", "displayName": "Ana Reyes" },
"role": "WRITER",
"status": "ACCEPTED",
…
}Change an invitation
PATCH/v1/api/invitations/{id}
Corrects an offer that has gone out and not been answered. Only while it is waiting. Changing what an accepted invitation granted would rewrite somebody's access without their knowing.
Body
- roleEDITOR · WRITER · READER
- The role offered.
- conceptsarray · at least 1
- The Concepts it grants, replacing the ones it had. The body says what the invitation grants, never what to add to it.
Send one of the two, or both.
Refusals
- 409invitation_invalidIt has been answered, cancelled, or has lapsed.
curl -X PATCH \
https://your-server.example/v1/api/invitations/a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "role": "EDITOR" }'{
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"role": "EDITOR",
"status": "PENDING",
…
}Cancel an invitation
DELETE/v1/api/invitations/{id}
Withdraws an offer. It is marked cancelled and kept, because the person may already have seen it, and withdrawn is a better answer than vanished. They can be invited again straight away.
An invitation that has lapsed can still be cancelled, and should be. Until it is, it holds that person's one waiting place and they cannot be invited again.
Refusals
- 409invitation_invalidIt was already accepted, declined or cancelled. A settled invitation is history.
curl -X DELETE \
https://your-server.example/v1/api/invitations/a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d \
-H "Authorization: Bearer arb_live_…"{
"ok": true,
"status": "CANCELLED"
}