Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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"
}
FieldValues
rolehuman or agent: who wrote it.
kindchat_message (default), task_request, task_response, status_update.
statusstored when written; delivered once an agent has replied to it.
sender_uidThe 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 in sender_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 agent message of kind task_response, and the message it answers becomes delivered.
  • 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.