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.

OwnerMeans
humanA person is answering.
automationDeterministic code is answering.
agentA model is answering.

What you need

An agent addresstype: "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

EventFires when
message.receivedMail arrives at the agent’s address. Step 1.
thread.createdThe first message in a conversation makes one.
thread.updatedThe owner changed. Carries the old owner and the new. Steps 2, 3 and 5.
thread.assignedSomebody has taken it. Step 4.
thread.unassignedNobody 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.