Search the docs
Find a page, a section, or an endpoint.
Inboxes for your platform
A pod, an inbox and a key for each of your customers, made in your own onboarding.
If your product creates agents for its own customers, each of those agents can have an inbox of its own, made during your onboarding, without your customer ever seeing Pidgeon. The shape is four steps:
- One pod per customer. A pod holds that customer's addresses and domains, and sends on its own reputation.
- The first agent's inbox, in your own onboarding. One call makes the pod, the address and the key.
- A pod key per customer. It reaches that customer's pod and nothing else. Your server keeps one admin key and makes the rest, in the same call.
- A domain for each customer, when you want their mail judged on its own. Read Which domain before you choose.
POST /v1/pods a pod, its address and its key, in one call
GET /v1/identities?pod_id=… a pod's addresses
GET /v1/api-keys?pod_id=… a pod's keys
POST /v1/api-keys/{id}/rotate a new secret for a key; nothing below it changes
DELETE /v1/api-keys/{id} a key, and every key it minted, stop working
DELETE /v1/pods/{id}?delete_addresses=true a pod, its keys, its addresses and their mail, goneYour admin key, and a pod key per customer
The key on your server is an admin key: a key over every address in
your account, at admin. Make it in Developers → API keys, and leave the
addresses unchosen. It is the only key that can create or delete pods,
and the only one that can mint a key for a pod.
A pod key is what you hand one user. It reaches every address in its
pod, including ones the pod gains later, and nothing outside it. It cannot
see another pod, list your keys, mint a key for another pod, or create or
delete a pod, its own included. Asking about another pod gets 403 or
404, and the answer says nothing about what is in it.
The first agent's inbox, in one call
Call this from your own onboarding, at the moment your customer creates
their first agent. POST /v1/pods with address and key makes the pod, an
address in it and a pod key for it. All three are made or none is: if the address can't be
made, the pod is removed again and you get the error.
curl https://pidgeon.ai/api/v1/pods \
-H "Authorization: Bearer $PIDGEON_ADMIN_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: provision-user-8812" \
-d '{
"name": "user-8812",
"client_id": "user-8812",
"address": { "address": "user-8812@yourapp.pidgeon.email", "type": "agent" },
"key": { "access": "send" }
}'{
"data": {
"id": "2f0f6a1c-9a4e-4e26-9a60-2b4c0f5e1d33",
"name": "user-8812",
"client_id": "user-8812",
"is_default": false,
"identity_count": 1,
"created_at": "2026-10-24T09:00:00.000Z",
"address": { "id": "…", "email": "user-8812@yourapp.pidgeon.email", "pod_id": "2f0f6a1c-…", "…": "…" },
"key": { "id": "…", "pod_id": "2f0f6a1c-…", "access": "send", "test": false, "token": "pgn_live_…", "…": "…" }
}
}addresstakes the same fields asPOST /v1/identities: anaddress, or alocal_partwith adomainordomain_id, andtype,display_nameand the rest. It always goes in the new pod.keytakesname(the pod's name if you leave it out),access,expires_atandtest.accessis required: until 26 November 2026 akeywithout it is still a pod key at your admin key's level, answered with awarningsentry, and from that date it is a400.access: "send"is the usual choice for a user's agent: it can read and send, and cannot delete its address or change who reaches it.- Leave either out and it is not made. A body with neither is a plain pod.
nameandclient_idare each unique in your account, so a second call for the same user is a409, not a second pod.
The token is shown once, and a retry says so
Send an Idempotency-Key. A retry with the same key and body returns the
same pod, address and key, marked Idempotent-Replay: true, with
"token": null. Pidgeon never stores a key's secret, so it cannot show it
twice.
If your first answer was lost and you never saw the token, rotate the key:
POST /v1/api-keys/{id}/rotate with the key.id from the replay gives it a
new secret, and the pod and address are untouched. An Idempotency-Key
is kept for a day (Idempotency).
Hand the agent its inbox. Give the agent the address and the key the way
you give it any secret, and point it at
pidgeon.ai/skill.md, which teaches it to
work an inbox, or connect it to https://pidgeon.ai/api/mcp with the key in
an Authorization: Bearer header. Never put the key in a URL.
Which domain
Where your customers' agents send from decides whose mail is judged together, so it is worth choosing before the first send.
| Choice | Example | Judged |
|---|---|---|
| Your company name on our domain | user-8812@yourapp.pidgeon.email | Together: every customer on one domain |
| A subdomain per customer, of a domain you own | assistant@acme.agents.yourapp.com | Apart: one domain per customer |
| The customer's own domain | assistant@ai.acme.com | Apart, and on the customer's own name |
The first works at once and needs no DNS. Our recommendation, once you send
for more than a handful of customers, is the second or the third. You own
yourapp.com, so you publish a subdomain's records yourself and your customer
never sees a DNS screen. How isolation works, below, says why.
Adding a customer's domain
A subdomain of yours, or your customer's own domain, goes into the customer's pod. With their pod key (or your admin key), the domain goes in that pod, and every address made on it there sends on that pod's reputation.
POST /v1/domains { "domain": "ai.acme.com" }: the domain, in the key's pod
GET /v1/domains/{id} the records to publish, each with its own status
GET /v1/domains/{id}/zone-file the same records as a file a DNS server imports
GET /v1/domains/{id}/setup-link a one-click link at the user's DNS provider, where it has one
POST /v1/domains/{id}/verify check now, instead of waiting for the next look
POST /v1/identities { "domain_id": "…", "local_part": "assistant", "type": "agent" }Show the records in your own screens. dns_records in
GET /v1/domains/{id} is what your user copies to their DNS, and each
record's status says whether it is live yet, so your product can tick them
off as they appear without the user ever seeing Pidgeon. Where their provider
supports Domain Connect, setup-link answers supported: true and a URL that
applies them all in one approval; otherwise it answers supported: false and
the records are the way.
A subdomain is the usual choice: it leaves the company's own mail where it
is, and ai.acme.com is the user's from the moment its records are live. An
agent's address on it carries an ID card anyone can check,
naming the user's domain.
Many users at once. A platform adds its users' domains and makes their addresses in bursts, one user at a time. On Enterprise the per-account rates for adding domains, verifying and writing are twenty times the others', and an account in good standing may make 5,000 addresses a day, so onboarding a batch of users does not stop half way. Limits has the numbers for every plan.
How isolation works
Pidgeon keeps each pod's sending apart. Every pod sends as its own
Amazon SES tenant.
SES watches each tenant's bounces and complaints separately, and when one
tenant's numbers go bad it pauses that tenant alone, without affecting the
others. So when one of your customers mails a thousand dead addresses, their
sends start failing with 503 and everybody else's keep going.
GET /v1/metrics?pod_id=… shows a pod's own numbers and whether it is
paused (Pods).
Gmail and Outlook judge the domain, not the pod. Gmail tracks volume, feedback and limits per sending domain and per IP address (Google's sender guidelines), and it knows nothing about pods. Customers who send from one shared domain share that domain's standing in the recipient's inbox: if one of them is marked as spam often enough, mail from the others on that domain lands in spam too, even though SES never paused them. Outlook.com requires SPF, DKIM and DMARC of each sending domain above 5,000 messages a day (Microsoft), and Pidgeon publishes all three for every domain.
So a domain per customer is what carries the isolation through to the inbox: a subdomain of yours for each customer, or the customer's own domain. Gmail's Postmaster Tools can show a subdomain's reputation on its own, and still counts all of a domain's subdomains together towards its bulk-sender limit.
Who can read whose mail
A key or a person reaches a pod's mail only through a grant, and the grants are these:
| Who | Reads |
|---|---|
| Your admin key, or any key made over every address | Every pod in your account |
| A pod key | Its own pod and nothing else. Another pod's address answers 404 |
| A key made for some addresses | Those addresses |
| People on your Pidgeon account | Every agent's address, in the app |
So you, the platform, can read every customer's mail: through your admin key, and through the app for anyone on your account. A pod key cannot read across customers, so a customer holding their own pod key cannot read another's. If your contract with your customers says you will not read their mail, that is your policy today, not something Pidgeon enforces.
Rotate or revoke
Two different tools, and the difference is everything below the key.
- Rotate (
POST /v1/api-keys/{id}/rotate) gives a key a new secret and keeps its id, so every key it minted keeps working. Rotate your admin key on a schedule, or when someone who had it leaves: no user goes down. The old secret can keep working for up to a day withgrace_period_seconds. - Revoke (
DELETE /v1/api-keys/{id}) ends a key and every key it minted, however deep. Revoking a user's pod key cuts that user off. Revoking your admin key cuts off every user it provisioned, at once. That is the kill switch: use it when the admin key has leaked, not to replace it.
Details and the rules on who may do which are in Authentication.
Test mode, for CI and development
A test key (pgn_test_…) runs the whole pipeline and
nothing it sends leaves Pidgeon or counts against your plan. Give each user
a test key as well as a live one, from the same call:
POST /v1/pods
{ "name": "user-8812-ci", "address": { "address": "user-8812-ci@yourapp.pidgeon.email" }, "key": { "access": "send", "test": true } }Or mint one for an existing pod with POST /v1/api-keys and
{ "pod_id": "…", "access": "send", "test": true }.
A test key can't create pods: a test key calling POST /v1/pods gets
403. A test key at admin over the account or a pod can create
test addresses, which count against no
plan and are deleted after a day, so each CI run can connect an address of
its own.
Simulated inbound
Nobody outside will ever answer a test send, so POST /v1/test/inbound
plays the other side. It delivers a message to one of the test key's
addresses through the real inbound pipeline: the sender rules, the spam
filter, the Firewall, threading, message.received and your webhooks.
curl https://pidgeon.ai/api/v1/test/inbound \
-H "Authorization: Bearer $PIDGEON_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user-8812-ci@yourapp.pidgeon.email",
"from": "customer@example.com",
"subject": "Where is my order?",
"text": "It never arrived."
}'Failures on purpose
Your error handling can be tested against the real API too. With a test key,
a send to rejected@… comes back in rejected while the rest go, a send to
bounce@… bounces a few seconds later, and the Pidgeon-Test-Fail header
answers 429, 503, a slow answer or a lost one, on sends and on minting
and rotating keys. Causing failures has
the list.
Listing one user's pod
Every address carries its pod_id, and every key its pod_id, so a pod
lists like this:
curl "https://pidgeon.ai/api/v1/identities?pod_id=$POD_ID" \
-H "Authorization: Bearer $PIDGEON_ADMIN_KEY"
curl "https://pidgeon.ai/api/v1/api-keys?pod_id=$POD_ID" \
-H "Authorization: Bearer $PIDGEON_ADMIN_KEY"The key list includes revoked keys, so you can tell which key was which
after the fact. It needs your admin key; a pod key gets 403.
Tearing a user down
DELETE /v1/pods/{id} removes a user from Pidgeon. It deletes their mail,
permanently, and nothing can bring it back. In order, it:
- revokes every key of the pod, and every key those minted, so nothing can send while the rest happens;
- deletes each address in the pod with all of its mail: messages, threads, attachments and the stored originals;
- deletes the pod.
A pod that still holds an address is refused with 409 unless you pass
delete_addresses=true, so deleting mail is never something a call does
by accident.
curl -X DELETE "https://pidgeon.ai/api/v1/pods/$POD_ID?delete_addresses=true" \
-H "Authorization: Bearer $PIDGEON_ADMIN_KEY"{ "data": { "id": "2f0f6a1c-…", "deleted": true, "addresses_deleted": 1, "keys_revoked": 2 } }- It needs your live admin key. A pod key gets
403, on its own pod too. So does a test key. - Refused, always: your account's default pod, and a pod holding a
domain. Delete the domain first (
DELETE /v1/domains/{id}). - Refused before anything happens: a pod holding a person's own inbox, which only that person can delete.
- If it fails part way, call it again. It carries on from where it
stopped. Once the pod is gone, the call answers
404. - To keep the mail, don't delete the pod. Suspend the addresses
(
PATCH /v1/identities/{id}with"status": "suspended") and revoke the pod's key: nothing can send or arrive, and the conversation stays on record.
Limits
Each pod counts against your plan's pods, and each key, live or test, against its active keys. A user with a live key and a test key uses two. Revoked keys count for nothing. The numbers per plan are in Limits.