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:

  1. One pod per customer. A pod holds that customer's addresses and domains, and sends on its own reputation.
  2. The first agent's inbox, in your own onboarding. One call makes the pod, the address and the key.
  3. 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.
  4. 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, gone

Your 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" }
}'
json
{
"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_…", "…": "…" }
}
}
  • address takes the same fields as POST /v1/identities: an address, or a local_part with a domain or domain_id, and type, display_name and the rest. It always goes in the new pod.
  • key takes name (the pod's name if you leave it out), access, expires_at and test. access is required: until 26 November 2026 a key without it is still a pod key at your admin key's level, answered with a warnings entry, and from that date it is a 400. 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.
  • name and client_id are each unique in your account, so a second call for the same user is a 409, 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.

ChoiceExampleJudged
Your company name on our domainuser-8812@yourapp.pidgeon.emailTogether: every customer on one domain
A subdomain per customer, of a domain you ownassistant@acme.agents.yourapp.comApart: one domain per customer
The customer's own domainassistant@ai.acme.comApart, 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:

WhoReads
Your admin key, or any key made over every addressEvery pod in your account
A pod keyIts own pod and nothing else. Another pod's address answers 404
A key made for some addressesThose addresses
People on your Pidgeon accountEvery 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 with grace_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:

json
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:

  1. revokes every key of the pod, and every key those minted, so nothing can send while the rest happens;
  2. deletes each address in the pod with all of its mail: messages, threads, attachments and the stored originals;
  3. 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"
json
{ "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.