Search the docs
Find a page, a section, or an endpoint.
The hand-over
Software answers, gets stuck, gives it to a person, and takes it back.
A conversation has an owner, and the owner can change. This page is that, end to end, against your own addresses: software answers, gets stuck, gives the conversation to a person, and takes it back. Five minutes, four requests, and two things you do in a browser.
An approval queue is not a hand-over
The distinction is the whole of it, and it is easy to skim past.
An approval queue says the software wrote something and somebody should look at it before it goes. The software is still answering; a person is checking its work.
A hand-over says a person has the conversation now. The software has stepped back, it is not drafting anything, and everything watching has been told — because the owner is a field on the thread, not a state in your process. The other party keeps replying to the same address and is never informed that anything changed.
| Owner | Means |
|---|---|
human | A person is answering. |
automation | Deterministic code is answering. |
agent | A model is answering. |
What you need
An agent address — type: "agent" — a key, and a webhook subscribed to
thread.*. If you have not made an address yet, do that first; the whole of
the setup is one request.
curl https://pidgeon.ai/api/v1/identities -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{
"domain_id": "your-domain-id",
"local_part": "scout",
"type": "agent"
}'An agent address is the one type that is both: software answers it, and a
person can step in. That is why the walkthrough uses one — and why the hand
back below returns the thread to agent rather than to automation.
1 · Mail arrives
Send something to your new address from anywhere — your own mail client is
fine. You get message.received, carrying thread_id.
Everything after this is that one id.
2 · Your software takes it
Nothing owns a conversation implicitly. Mail arriving at an agent's address does not make the agent the owner, because a mailbox is not a process: say so.
curl https://pidgeon.ai/api/v1/threads/$THREAD_ID -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "owner_type": "agent", "status": "open" }'thread.updated fires, carrying previous_owner_type alongside owner_type.
Both sides, every time — which is what lets you tell a change of owner from a
change of status without keeping a copy of the last one you saw.
Open the address in the web client now and the conversation says An agent is answering this.
3 · It gets stuck, and gives the conversation back
One call, because it is one intention.
curl https://pidgeon.ai/api/v1/threads/$THREAD_ID -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "owner_type": "human", "status": "human_review" }'human_review with no assignee is the state this whole primitive exists
for: software asked for a person, and nobody has come yet. It is not the same
as a conversation that merely has nobody's name on it, and the inbox spells
the two differently — Waiting for a person against Unassigned.
Splitting this into two PATCHes is how a conversation ends up owned by a
person and not marked for review, which is a thread nobody is looking for.
4 · A person takes it
This step is deliberately not an API call.
Open the inbox. The conversation is in it, under the same address, with Waiting for a person under the subject and a Take it button beside it. Press it.
thread.assigned fires, and thread.updated with it. Your software learns
who has the conversation without you building anything: it is watching the
same events it has been watching all along.
If the mailbox has a team, the strip above the folder filters to exactly this — Unassigned is the queue of hand-overs nobody has picked up.
5 · They reply, as the address
From the web client, in the ordinary way. The message goes out from the
agent's own address, in the same thread and with the same References — the
person on the other end sees a reply to their email and is told nothing about
any of this. There is no "escalated to a human" banner, because there was no
change of address for one to explain.
That is the part worth pausing on. A hand-over that changes the sending address is not a hand-over, it is a transfer, and every one of those costs the other party a new thread and a re-explanation.
6 · And back again
curl https://pidgeon.ai/api/v1/threads/$THREAD_ID -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "owner_type": "agent", "status": "open", "assignee_id": null }'The person can do the same thing from the inbox — Hand back, beside the line that says they have it.
There is no automatic hand back. A person who has been given a conversation keeps it until somebody sets the owner again, and that is deliberate: software deciding on its own that a person has finished is exactly the failure this primitive exists to prevent.
What fired, and what you can watch
| Event | Fires when |
|---|---|
message.received | Mail arrives at the agent’s address. Step 1. |
thread.created | The first message in a conversation makes one. |
thread.updated | The owner changed. Carries the old owner and the new. Steps 2, 3 and 5. |
thread.assigned | Somebody has taken it. Step 4. |
thread.unassigned | Nobody has it any more — a hand back clears the name. |
Every one of them is subscribable, so a queue running through Pidgeon can post to Slack, open a ticket, or wake your process when a conversation changes hands. Nothing here polls.
What Pidgeon does not do
There is no prompt, no model and no loop in any of this. The API and the events are the substrate you build an agent on; the agent is yours. Saying otherwise would be the wrong kind of promise.