Search the docs

Find a page, a section, or an endpoint.

Threads

A conversation, its owner, and the state it carries.

A thread is a row, not a grouping key. Messages are threaded when they arrive, by Message-ID, In-Reply-To and References, and the thread carries the two things software needs to coordinate with a person: a status and an owner.

One call across every address

GET /threads is workspace-wide — identity_id narrows it to one mailbox and is optional — and every row names the address it is in. It is the listing to build a shared inbox on, because the folder listing takes one address and one folder, and three addresses over four folders is twelve requests for one screen.

A row carries enough of the newest message to draw it without a second call: folder, from, from_name, preview and has_attachments, beside identity, subject, status, assignee_id and message_count. They are the newest message's facts, so folder is where that message is filed and moves when you move it.

Fetch the conversation itself with GET /messages?thread_id=… when somebody opens it. The row is for the list.

Status

StatusMeans
openNobody has finished with it.
waitingWaiting on somebody outside.
resolvedDone.
human_reviewA person needs to look.

Owner

OwnerMeans
humanA person is reading it.
automationDeterministic code owns it.
agentA model owns it.

The pair is the coordination primitive. agent + open is software working on something; human + human_review is the same conversation, in somebody's inbox, waiting for them.

Hand a conversation to a person

When your software is out of its depth, give the conversation back. It appears in the inbox, and thread.updated — carrying the new owner — tells whatever was driving that it no longer is.

curl https://pidgeon.ai/api/v1/threads/feaa4f72-af24-4fa5-ad05-368a0a9217c6 -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "owner_type": "human", "status": "human_review" }'

The SDK's handOff is the same two fields, named. It is one call because it is one intention, and splitting it into two PATCHes is how a conversation ends up owned by a person but not marked for review.

Who is answering it

owner_type says which kind of thing is answering. assignee_id says which person — and the two compose rather than replacing one another. A thread with status: "human_review" and no assignee is a handover nobody has taken, which is the state a queue exists to make visible.

Somebody's id comes from GET /workspace/members. null puts the conversation back in the unassigned queue, which is a decision like any other and emits thread.unassigned.

curl https://pidgeon.ai/api/v1/threads/feaa4f72-af24-4fa5-ad05-368a0a9217c6 -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "assignee_id": "82438cb3-89a0-4c58-a79e-542e091f9ee8" }'

List one person's queue with ?assignee_id=, and the work nobody has picked up with ?assignee_id=unassigned. thread.assigned and thread.unassigned are both subscribable, so a queue running through Pidgeon can post to Slack or open a ticket when a conversation changes hands.

Assignment is a property of a shared address — one created as a team mailbox, or an agent's. A person's own inbox has no queue, however many colleagues they have.

Taking it back

There is no automatic hand-back. A person who has been given a conversation keeps it until somebody sets the owner again, which is deliberate: software deciding on its own that a human is finished is the failure mode this exists to prevent.

Pidgeon ships no agent runtime

The API and the webhooks are the substrate you build one on. There is no prompt, no model and no loop here, and saying otherwise would be the wrong kind of promise.