Search the docs

Find a page, a section, or an endpoint.

Hand-off to a human

Giving a conversation to a person, and taking 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 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"
}'

Any address with an inbox can be handed over — there has to be somewhere for the person to read it, which is the whole of the requirement. What type: "agent" changes is where a conversation starts: mail arriving at an agent address opens a thread already owned by agent, so the queue your software works is the one it finds by asking for its own threads. At a human or team address the conversation starts as a person's, and software would have to take it first.

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 already owns it

Mail arriving at an agent address opens a conversation the agent owns, so for the common case there is nothing to do here: ask for the threads you own and the new one is in the list.

That is the point of the type. An address exists to be answered by something, and which something is a fact about the address rather than a claim each arrival has to make. It used to be the other way — every conversation began as a person's, whatever it arrived at — which meant an agent following this page asked for the threads it owned and got none.

A human or team address starts its conversations as a person's, because that is who reads it. Claiming one there, or taking a conversation back after a person has had it, is the same call either way:

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 and the conversation says An agent is answering this.

thread.created fires when a conversation begins — including one your own send opened, not only one that arrived. If you are driving outreach, that is the event that tells you a new conversation exists at all.

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",
  "reason": "They want a refund above what I can approve."
}'

Somebody is told. The people who can answer as the address get a push notification and an email with the subject, who handed it over and your reason — one sentence, up to 500 characters, also carried on thread.updated. Who that is, in order: the address's owner and anybody given send access by name, which for an agent includes whoever created it; otherwise everybody, when the whole workspace can send as it; otherwise the workspace's owners. Add assignee_id to the same request and only that person is told.

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, or from Apple Mail or any mail client set up with an app password — no special step. 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.

A person's reply takes the conversation. If a person answers a conversation your software still owns — they saw it in Apple Mail and replied — it becomes theirs: owner_type turns human, they become the assignee, and thread.updated (with taken_by_reply) and thread.assigned fire. Stop answering when you see them, or the other party gets two replies.

Only a person does this. A send with your API key never takes a conversation, and neither does a person sending a draft your software wrote: that is signing it off, and the conversation stays yours. Nor does somebody who can no longer send as the address.

New mail reaches them. While a person has the conversation, the customer's next replies notify them — one notification per burst, so ten replies in a few minutes are one email.

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.