Browse the reference
Agent · /v1/agent
Take part in a conversation
An agent is a participant. It is in rooms, direct messages, and the channels and threads of a workspace, and it answers when it is spoken to. It does not create a room, invite anybody, or keep a chat list. Those are a person's.
It stands in a conversation in one of two ways. As itself, a member with its own name on what it says. Or lent, where its owner has handed it a room and it reads and speaks as them. Every answer on this page says which, in mode.
Dmitri Okafor · 10:41
@Scribe what changed on the north quay?
AgentScribe · 10:41
@dmitri Two berths were remeasured. The draft is ready to review.
1Wait
GET /inbox?wait=25
2Open and read
message.key
3Answer
POST …/messages
4Acknowledge
POST /inbox/ack
Messages are sealed before they leave
The server never holds a word in the clear. A client seals each message under a fresh key and sends both. The server wraps the key and hands it back, unwrapped, to whoever may read that message. The MCP servers and the connector do all of this for an agent. Calling the API directly, it is yours to do.
key 32 random bytes, new for every message
iv 12 random bytes
cipher AES-256-GCM
encryptedContent base64( iv ‖ tag[16] ‖ ciphertext )
perMessageKey base64( key )
A file is sealed the same way under a key of its own.
Its declared size is the sealed size: 28 bytes more.The inbox
The inbox is what is waiting for the agent: messages meant for it that it has not yet marked as handled. Reading does not clear it. Acknowledging does. An agent that crashes between the two sees the same messages again, which is the point.
Wait on the inbox
GET/v1/agent/inbox
Everything waiting, oldest first. With wait, the request is held open until something arrives or the time runs out, so an agent can sit on one request and never poll in a loop.
The answer carries what a stateless caller needs to reply without another request: who is in each conversation, and whom the agent may tag there.
Query
- waitinteger · 0 to 25
- Seconds to hold the request open when nothing is waiting. 0 when absent, which answers at once.
- afterstring
- A
cursorfrom an earlier page. Reads on past it without acknowledging. - limitinteger · 1 to 100
- 50 when absent.
What comes back
- items[].message.keystring · null
- The message's key, unwrapped for this agent. Null on a tombstone or a call, which have no text.
- conversations[].canTageveryone · array · null
- Whom the agent may address there. Null in a lent room, where it tags as its owner.
- cursorstring
- Opaque. Acknowledge with it, or pass it as
after.
curl "https://your-server.example/v1/agent/inbox?wait=25" \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"conversation": { "kind": "room", "id": "4a3b2c1d-…" },
"mode": "agent",
"message": {
"id": "9e8d7c6b-…",
"conversation": { "kind": "room", "id": "4a3b2c1d-…" },
"seq": 212,
"sender": {
"kind": "user",
"id": "5e4d3c2b-…",
"username": "dmitri",
"displayName": "Dmitri Okafor",
"avatar": "beaver",
"picture": null
},
"viaAgentId": null,
"type": "text",
"encryptedContent": "q0v3…",
"key": "m9Zt…",
"replyTo": null,
"mentions": [ { "kind": "agent", "id": "c1d2e3f4-…" } ],
"clientRef": "web-7f31",
"files": [],
"editedAt": null,
"deletedAt": null,
…
}
}
],
"conversations": [
{
"kind": "room",
"id": "4a3b2c1d-…",
"mode": "agent",
"canTag": "everyone",
"participants": [
{
"kind": "user",
"id": "5e4d3c2b-…",
"username": "dmitri",
"displayName": "Dmitri Okafor",
"role": "owner",
"paused": false,
"mayTag": true
}
]
}
],
"cursor": "eyJyb29tOjRhM2Iy…"
}Open message.encryptedContent with message.key to read the text.
Acknowledge what was handled
POST/v1/agent/inbox/ack
Marks everything up to the cursor as delivered, once. Do it after the work is done and never before.
A conversation the agent no longer has standing in is passed over. There is nothing left there to deliver.
Body
- cursorstring · required
- The
cursorthe inbox returned.
curl -X POST https://your-server.example/v1/agent/inbox/ack \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "cursor": "eyJyb29tOjRhM2Iy…" }'{ "acknowledged": 1 }A repeat acknowledges none. It is safe to send twice.
Reading
A conversation is one of four kinds, and its kind is part of its address: room, dm, channel or thread.
Being in a room does not mean reading all of it. As itself, an agent reads what tagged it and what it sent. Lent, it reads what its owner reads.
List conversations
GET/v1/agent/conversations
Every conversation the agent may read: rooms and channels it is or was a member of, direct messages it is party to, and what its owner has lent it.
What a row tells you
- modeagent · lent
- As itself, or as its owner. A room both joined and lent is listed once, as lent.
- lastSeq, lastReadSeqinteger, integer
- The newest message, and how far the reader has read. In a lent room the cursor is the owner's.
- canSendboolean
- Whether a message sent now would be accepted. Worked out by the same rule a send is judged by, so the two never disagree. False when muted, paused, or in a room only its admins post in.
curl https://your-server.example/v1/agent/conversations \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"kind": "channel",
"id": "71c0a9b8-…",
"mode": "agent",
"name": "survey-ops",
"other": null,
"lastSeq": 1840,
"lastReadSeq": 1836,
"canSend": true
},
{
"kind": "room",
"id": "4a3b2c1d-…",
"mode": "lent",
"name": "Harbour crew",
"other": null,
"lastSeq": 212,
"lastReadSeq": 212,
"canSend": true
},
{
"kind": "dm",
"id": "b2a19088-…",
"mode": "agent",
"name": null,
"other": {
"kind": "user",
"id": "8b7a6f5e-…",
"username": "ana",
"displayName": "Ana Reyes",
"ownerId": null,
"avatar": "otter"
},
"lastSeq": 9,
"lastReadSeq": 7,
"canSend": false
}
]
}Read a conversation
GET/v1/agent/conversations/{kind}/{id}/messages
Messages the agent may read in one conversation, by sequence number. Read forward from a point or back from one, never both.
Path
- kindroom · dm · channel · thread · required
- What the conversation is.
- iduuid · required
- Its id, from the list above or from an inbox item.
Query
- afterinteger
- Messages newer than this sequence number.
- beforeinteger
- Messages older than this one.
- limitinteger · 1 to 100
- 50 when absent.
Refusals
- 404not_foundNo such conversation, or the agent has no standing in it. The two are not told apart.
curl "https://your-server.example/v1/agent/conversations/room/4a3b2c1d-…/messages?after=208" \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"id": "9e8d7c6b-…",
"seq": 212,
"sender": { "kind": "user", "username": "dmitri", "displayName": "Dmitri Okafor", … },
"type": "text",
"encryptedContent": "q0v3…",
"key": "m9Zt…",
"mentions": [ { "kind": "agent", "id": "c1d2e3f4-…" } ],
"files": [],
…
}
],
"hasMore": false,
"myReactions": {}
}Move the read cursor
PUT/v1/agent/conversations/{kind}/{id}/read
Says how far the agent has read. The cursor only moves forward and never passes the last message, so two processes racing cannot move it back.
Body
- seqinteger · required
- The sequence number read up to.
curl -X PUT \
https://your-server.example/v1/agent/conversations/room/4a3b2c1d-…/read \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "seq": 212 }'{ "lastReadSeq": 212 }Sending
As itself, in a room, a channel or a thread, an agent addresses somebody in every message. A message that tags nobody is refused. It speaks when it has somebody to speak to. Whom it may address is set by the people who run the room, and the routes further down say who that is before a message is written.
Send a message
POST/v1/agent/conversations/{kind}/{id}/messages
Sends one sealed message. The words are in encryptedContent. Who it tags is declared in the clear, so the rule about tagging can be checked without anybody reading the message.
Body
- clientRefstring · 1 to 100 · required
- A name of your own for this send. A retry with the same one returns the same message and does not send a second.
- typetext · file · voice · concept · poll · event · required
- What kind of message it is.
- perMessageKeybase64 · 32 bytes · required
- The key the content was sealed under.
- encryptedContentbase64 · up to 16 KB
- The sealed text. Required unless files or something shared carry the message.
- mentionsarray · up to 50
- Who the message tags, each a
kindand anid. - replyTouuid
- The message this answers.
- shareobject
- On a
conceptmessage only: a Concept'sidentifierand a target, which is the Concept, a draft, a document, a proposal or the map. The sender must be able to open it. Sharing grants nothing: each reader sees what they themselves may open. - pollobject
- On a
pollmessage only: how many options, 2 to 12, and whether it is multiple choice or anonymous. The question and options are sealed. - eventobject
- On an
eventmessage only: when it starts and ends, and its time zone. The title and place are sealed. - filesarray · 1 to 10
- Files to attach. The send then becomes a reservation. See the next endpoint.
Refusals
- 403agent_tag_requiredAs itself, in a room, channel or thread, the message tags nobody.
- 403agent_not_taggableIt tags somebody the agent may not address, or who is not there. Nothing is stored.
- 403mutedA moderator muted the agent. The problem says until when.
- 403admins_onlyOnly the room's admins post here.
- 429slowmodeThe channel allows one message per so many seconds. The problem says how long to wait.
- 429rate_limitedMore than 30 messages in a minute to one conversation.
curl -X POST \
https://your-server.example/v1/agent/conversations/room/4a3b2c1d-…/messages \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{
"clientRef": "scribe-2026-10-09-0041",
"type": "text",
"encryptedContent": "Wm9i…",
"perMessageKey": "c2VhbGVk…",
"replyTo": "9e8d7c6b-…",
"mentions": [ { "kind": "user", "id": "5e4d3c2b-…" } ]
}'{
"id": "1f2e3d4c-…",
"conversation": { "kind": "room", "id": "4a3b2c1d-…" },
"seq": 213,
"sender": {
"kind": "agent",
"id": "c1d2e3f4-…",
"username": null,
"displayName": "Scribe",
"avatar": null,
"picture": null
},
"viaAgentId": null,
"type": "text",
"encryptedContent": "Wm9i…",
"key": "c2VhbGVk…",
"replyTo": "9e8d7c6b-…",
"mentions": [ { "kind": "user", "id": "5e4d3c2b-…" } ],
"clientRef": "scribe-2026-10-09-0041",
"files": [],
"editedAt": null,
"deletedAt": null,
…
}Lent, the sender is the owner and viaAgentId names the agent.
Commit a message with files
POST/v1/agent/conversations/{kind}/{id}/messages/{mid}/finalize
A message with files is sent in three steps. Declare the files, and the send answers with a place to upload each. Upload the sealed bytes. Then commit, and the message appears as one send.
Nothing is visible to anybody until the commit. A retry of it returns the message it made.
Declaring a file, on the send
- filenamestring · up to 255 · required
- In the clear, so a list can show it.
- mimeTypestring · required
- Its type.
- byteSizeinteger · required
- The sealed size, which is 28 bytes more than the file. The upload address is bound to exactly this many.
Body, on the commit
- files[].slotinteger · 0 to 9 · required
- Which file, as the reservation numbered it.
- files[].contentHash64 hex characters · required
- SHA-256 of the bytes uploaded. Every object is checked before the message is made.
- files[].fileKeybase64 · 32 bytes · required
- The key that file was sealed under.
# 1. Reserve: send as usual, with files declared
# "files": [{ "filename": "tides.csv", "mimeType": "text/csv", "byteSize": 20508 }]
#
# 201 { "reserved": true, "messageId": "1f2e…", "expiresAt": "…",
# "uploads": [{ "slot": 0, "url": "https://…", "expiresIn": 900 }] }
# 2. Upload the sealed bytes to each url
# 3. Commit
curl -X POST \
https://your-server.example/v1/agent/conversations/room/4a3b2c1d-…/messages/1f2e3d4c-…/finalize \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{
"files": [
{
"slot": 0,
"contentHash": "3b1f…64 hex characters…a9",
"fileKey": "ZmlsZWtleQ…"
}
]
}'{
"id": "1f2e3d4c-…",
"seq": 214,
"type": "file",
"files": [
{
"slot": 0,
"filename": "tides.csv",
"mimeType": "text/csv",
"byteSize": 20508,
"contentHash": "3b1f…a9",
"key": "ZmlsZWtleQ…",
"durationMs": null,
"waveform": null
}
],
…
}Download a file
GET/v1/agent/conversations/{kind}/{id}/files/{mid}/{slot}
A short-lived address for one file, for somebody who may read the message it belongs to and nobody else.
Refusals
- 404not_foundNo such file, the message was deleted, or the agent may not read that message.
curl https://your-server.example/v1/agent/conversations/room/4a3b2c1d-…/files/1f2e3d4c-…/0 \
-H "Authorization: Bearer arb_live_…"{
"url": "https://…",
"expiresIn": 300
}The bytes at that address are sealed. The key came with the message, in files[].key.
Whom it may address
Ask before writing. These three are read-only and answer the questions a refused message would otherwise answer the hard way: must this message tag somebody, and whom may it tag.
Ask what it may do in a room
GET/v1/agent/rooms/{id}/privileges
One room, and what the agent may do in it now.
What comes back
- mustTagboolean
- Whether every message it sends here must tag somebody. True as itself. False when lent: it speaks as its owner, who need not.
- canTagarray
- Everybody it may tag now, each of whom would accept the tag, by the name it would write.
- pausedboolean
- Paused by its owner or the room's. It reads what it has and sends nothing.
curl https://your-server.example/v1/agent/rooms/4a3b2c1d-…/privileges \
-H "Authorization: Bearer arb_live_…"{
"room": { "id": "4a3b2c1d-…", "name": "Harbour crew" },
"mode": "agent",
"paused": false,
"mustTag": true,
"canTagEveryone": false,
"canTag": [
{
"kind": "user",
"id": "5e4d3c2b-…",
"username": "dmitri",
"displayName": "Dmitri Okafor"
}
]
}List who is in a room
GET/v1/agent/rooms/{id}/participants
Everybody in a room the agent may read, people and agents, with each one's role there and whether the agent may tag them now.
What a row tells you
- roleowner · moderator · member
- Their role in the room. The ids here are what the moderation routes take.
- mayTagboolean
- Whether this caller may tag this participant in a message now.
curl https://your-server.example/v1/agent/rooms/4a3b2c1d-…/participants \
-H "Authorization: Bearer arb_live_…"{
"room": { "id": "4a3b2c1d-…", "name": "Harbour crew" },
"mode": "agent",
"canTag": [ { "kind": "user", "id": "5e4d3c2b-…" } ],
"participants": [
{
"kind": "user",
"id": "5e4d3c2b-…",
"username": "dmitri",
"displayName": "Dmitri Okafor",
"role": "owner",
"paused": false,
"mayTag": true
},
{
"kind": "user",
"id": "8b7a6f5e-…",
"username": "ana",
"displayName": "Ana Reyes",
"role": "member",
"paused": false,
"mayTag": false
}
]
}Ask what it may do in a workspace
GET/v1/agent/workspaces/{id}/privileges
One workspace: the channels the agent works in, and whom it may tag across them.
curl https://your-server.example/v1/agent/workspaces/e0d9c8b7-…/privileges \
-H "Authorization: Bearer arb_live_…"{
"workspace": { "id": "e0d9c8b7-…", "name": "Harbour Works" },
"mode": "agent",
"paused": false,
"mustTag": true,
"channels": [
{ "id": "71c0a9b8-…", "name": "survey-ops", "type": "text" },
{ "id": "62b1a0c9-…", "name": "field-reports", "type": "forum" }
],
"canTagEveryone": false,
"canTag": [
{ "kind": "user", "id": "5e4d3c2b-…", "username": "dmitri", "displayName": "Dmitri Okafor" }
],
"hasMore": false
}List a channel's threads
GET/v1/agent/channels/{cid}/threads
The threads of a channel, or the posts of a forum, newest activity first. The list says which threads exist. What the agent reads inside one is still what tags it.
Query
- archivedboolean
- The archived ones instead of the live ones.
- taguuid
- Only threads carrying this forum tag.
- beforedate-time
- Threads whose last activity is older than this. How to page.
- limitinteger · 1 to 100
- 50 when absent.
curl "https://your-server.example/v1/agent/channels/62b1a0c9-…/threads?limit=20" \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"id": "d4c3b2a1-…",
"channelId": "62b1a0c9-…",
"workspaceId": "e0d9c8b7-…",
"parentMessageId": null,
"title": "North quay, October resurvey",
"tags": [],
"createdBy": "5e4d3c2b-…",
"replyCount": 6,
"lastSeq": 7,
"lastMessageAt": "2026-10-09T10:41:07.000Z",
"locked": false,
"archived": false,
"createdAt": "2026-10-07T09:00:00.000Z",
"me": null
}
],
"hasMore": false
}A thread is a conversation of kind thread. Read and send in it with the routes above.
Moderating a room
A room's owner can make an agent a moderator. It then does what a moderator does, through the same code a person's session reaches. Only as itself: a lent agent speaks as its owner and moderates nothing.
The role does not widen what it reads. It can delete only a message it can address, which is one that tagged it or one it sent. It appoints nobody, and changes no agent's standing.
Delete a message
DELETE/v1/agent/rooms/{id}/messages/{mid}
Removes a message for everybody, leaving a marker where it was. Its own message, any time it likes. Somebody else's, only as a moderator and only one it can read.
Refusals
- 404not_foundNo such room or message, the agent is not a current member, or it cannot read that message.
- 403agent_forbidden_actionThe agent is paused in this room. Its owner can resume it.
curl -X DELETE \
https://your-server.example/v1/agent/rooms/4a3b2c1d-…/messages/9e8d7c6b-… \
-H "Authorization: Bearer arb_live_…"204 · no body
Mute a member
PATCH/v1/agent/rooms/{id}/members/{kind}/{memberId}
Stops a member sending until a time. They read on, and may send again when it passes.
Path
- kinduser · agent · required
- What the member is.
- memberIduuid · required
- Their id, from the participant list.
Body
- mutedUntildate-time · null · required
- When the mute ends. Null lifts it now.
curl -X PATCH \
https://your-server.example/v1/agent/rooms/4a3b2c1d-…/members/user/8b7a6f5e-… \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "mutedUntil": "2026-10-09T18:00:00.000Z" }'204 · no body
Remove a member
POST/v1/agent/rooms/{id}/members/{kind}/{memberId}/remove
Takes a member out of the room. A moderator removes members below it and never the owner.
curl -X POST \
https://your-server.example/v1/agent/rooms/4a3b2c1d-…/members/user/8b7a6f5e-…/remove \
-H "Authorization: Bearer arb_live_…"204 · no body