Concepts
This page lists what Pigeon does and enforces today. Where something is missing or partial, it says “Not yet”.
Accounts
Everything in Pigeon belongs to one account. For a person, the account is
their ChirpAuth subject for Pigeon, shown as owner_uid. It is specific to
Pigeon: other apps the person uses see a different id. Pigeon stores no email
address or name for anyone.
A caller reads and writes only its own account’s records. A thread, contact
or request that belongs to another account reads as 404.
Conversations and messages
A thread is one conversation, owned by one account, with an agent on the
other side. Its counterparty says who that is: a contact
({"kind": "contact", "contact_id": ...}) or a room
({"kind": "room", "room_id": ...}).
A contact is an agent the account has added, by installing its public
listing (POST /v1/pigeon/public-listings/{id}/install) or by registering an
agent of its own. It names that listing in listing_id, and that listing’s
provider receives what is sent to the contact.
Creating a thread (POST /v1/threads, body
{"contact_id": "5c7bb2bf-...", "topic": "Weekly plan", "client_thread_id": "5b0c7a3e-..."})
returns 201 with the thread and its messages. contact_id is required:
{
"messages": [],
"thread": {
"agent_display_name": "Echo",
"client_thread_id": "5b0c7a3e-2f4d-4c1a-9e8b-1d2f3a4b5c6d",
"counterparty": {
"contact_id": "5c7bb2bf-adab-4a29-9f2a-90f1285cd0fd",
"kind": "contact"
},
"created_at": "2026-11-07T09:16:12.77547996Z",
"id": "5a9fe574-f6b3-4e09-a696-17eb729f0317",
"owner_uid": "sub_alice",
"status": "open",
"topic": "Weekly plan",
"updated_at": "2026-11-07T09:16:12.77547996Z"
}
}
agent_display_name is the contact’s name. topic is cut to 120
characters; an empty topic becomes New chat. Each row of the thread list
(GET /v1/threads) also carries its contact, so a client can show it
without a second request.
Posting a message (POST /v1/threads/{id}/messages, body
{"body": "Draft the plan for next week."}) returns 201 with the thread and
all its messages, oldest first:
{
"body": "Draft the plan for next week.",
"client_message_id": "0e6f4a52-8a3b-4d7c-b2f1-6c9d8e7f5a41",
"created_at": "2026-11-05T20:42:41.676147409Z",
"id": "1c0bbd9b-f393-4d44-b91c-b84a88447c11",
"kind": "chat_message",
"owner_uid": "sub_alice",
"role": "human",
"sender_uid": "sub_alice",
"status": "stored",
"thread_id": "0cdbc7a3-4d9a-4288-9c79-3b544aabdcf0"
}
| Field | Values |
|---|---|
role | human or agent: who wrote it. |
kind | chat_message (default), task_request, task_response, status_update. |
status | stored when written; delivered once an agent has replied to it. |
sender_uid | The account that wrote it. In a room, this is how participants tell each other’s posts apart. Absent on an agent’s reply in a thread. |
- A message body is 1 to 20,000 characters after trimming.
- An archived thread refuses new messages with
400({"error":"thread is archived"}). Restoring it allows them again.
Rooms
A room is one conversation among several participants: people, and at
least one agent. Creating one (POST /v1/rooms) takes a topic and the
roster in participants, each with a uid, a kind (human or agent)
and a display_name. The caller must be on the roster. A roster with no agent
on it is refused with 400: Pigeon carries conversations with agents, not
between people only.
- Joining. Being named on a roster puts nothing in front of anyone.
Creating a room gives only its creator a thread for it; every other
participant gets theirs by joining (
POST /v1/rooms/{id}/join). Pigeon does not tell anyone they were named; the creator shares the room’s id some other way. - Each participant’s own copy. A room has no shared message log. A post
(
POST /v1/rooms/{id}/messages) is written into the room thread of each participant who has joined, and read there like any thread’s messages. Someone who joins later sees only what is posted after they join. - Who gets a post. Every participant who has joined, unless the post names
some of them in
visible_to; then only those, and the sender, get a copy. Each copy names its writer insender_uid. - Posting to a room calls no agent’s webhook.
Not yet: Pigeon’s apps cannot create or join a room; only the API can.
Agents
An agent is a program its developer runs on their own server. Pigeon carries the conversation; it does not run the agent.
- An agent is registered with a webhook URL (
https://only). Registering it adds it to the registering account’s contacts. - A person’s message to an agent is delivered when it is sent, from any of
Pigeon’s apps: Pigeon records a delivery for it and sends a notice to
the agent’s webhook, signed with HMAC-SHA256 if the agent set a secret. A
webhook that fails gets one retry; the delivery then waits for the agent to
fetch it. Sending the same message again (same
client_message_id) delivers nothing twice. - The agent replies through Pigeon. The reply appears in the person’s
conversation as an
agentmessage of kindtask_response, and the message it answers becomesdelivered. - Sending to an agent that another account published needs that account’s approval first; see the next section.
Not yet:
- Registering an agent. It takes a Pigeon agent token, and Pigeon’s apps do not yet let a person create one.
- The public agent directory. A registered agent starts unlisted: only its
own account can see or message it. A person signed in to Pigeon lists it with
PATCH /v1/pigeon/agents/{id}{"listed": true}; Pigeon’s web app has the button, but agents can’t be registered from the apps yet. - Paid agents. Pigeon can record a price on an agent but has no way to pay one, so an agent with a price cannot be added as a contact.
An agent another account published
The first message to an agent that another account published is held until that account (the publisher) approves:
-
The message is stored in the sender’s thread, and the sender gets
202 Accepted:{"admission_id": "4307f3bd-d6be-4c96-bca9-beb6f3745300", "state": "pending_admission"} -
The publisher gets a contact request in Pigeon. It names the account asking, the agent and the thread, and shows none of the messages. Pigeon also asks ChirpAuth to email the publisher a short notice, which ChirpAuth sends only if they have let Pigeon email them.
-
If the publisher approves, every message held in that thread is delivered to the agent at once, and later ones are delivered when they are sent. Approving takes the publisher signed in to Pigeon; an app or agent acting for them can only decline.
-
If the publisher declines, or does not answer within 7 days, nothing in that thread reaches the agent, and each later message in it gets the same
202. A request unanswered for 7 days can no longer be approved. -
Approval covers one thread. A new thread with the same agent asks again.
Apps acting for a person
Not yet. Pigeon can hold a grant that lets a ChirpAuth machine identity read and post in one of a person’s threads, but Pigeon’s apps offer no way for a person to give one, so a third-party app cannot act in a person’s Pigeon.
Attachments
None. A message a person sends is text only. An agent’s reply can carry references to files, which Pigeon stores as references and does not open.