Search the docs
Find a page, a section, or an endpoint.
Authentication
Bearer keys, what one reaches, and how to narrow it.
Every request carries an API key as a bearer token. Keys are server-side only and never valid in a browser — a key in client JavaScript is a key anyone who opens devtools now owns.
curl https://pidgeon.ai/api/v1/identities -H "Authorization: Bearer $PIDGEON_API_KEY"What a key can reach
What it was given, and no more. A key is one of three kinds:
- A list of addresses, set when it is created and editable afterwards. A key with an empty list reaches no mailbox at all, which is a perfectly good key: it can still read events and manage webhooks.
- One pod (
pod_id): every address in that pod, including ones added later, and the pod's own domains. The key you hand one of your customers. - Every address on the account, which only the dashboard mints.
Within that list, each address is narrowed by its own capabilities. Capabilities are derived from the address's type and the plan rather than stored, so they cannot drift from what the product shows: an application address has no inbox, and asking for its threads returns nothing rather than an error.
A key's reach is a snapshot, not a subscription. Creating payroll@
tomorrow does not hand it to a key you issued last month — except when that
key created the address itself, which is precisely its business.
Asking a key what it is
Everything else here answers what is in an account. GET /v1/me answers
what the credential in your hand may do — which you otherwise learn by trying
something and reading the refusal, inside a model's tool call, where nobody
sees it.
curl https://pidgeon.ai/api/v1/me -H "Authorization: Bearer $PIDGEON_API_KEY"It returns the key's ceiling, how many addresses it reaches, and the
workspace. Not the addresses themselves — GET /v1/identities lists those,
with a cursor — but the count, which is what tells a key that reaches nothing
apart from one that is misconfigured.
The rung is the thing that surprises people later. A revoked key fails at
once and a key granted nothing returns an empty list, but a key at read
installs perfectly and then cannot answer an email. Check it at startup rather
than at the first send. It costs nothing: no write, no side effect, and it
does not count against anything your plan sells.
Keys that mint keys
A key can create another key, scoped to a subset of what it reaches. This is what makes per-customer credentials something software provisions: mint a key for a tenant, hand it over, revoke it when they leave.
curl https://pidgeon.ai/api/v1/api-keys -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "name": "Acme", "identity_ids": ["identity-id"] }'Pass one of identity_ids or pod_id. From the dashboard, leaving the
list out means "every address this account owns" — but a field left out must
never be the widest possible answer for a credential, so over the API you say
what you mean. Only a key over the whole account may mint a pod key. Every id has to be one the calling key already reaches: a key cannot
delegate reach it does not have, and asking for one it cannot see returns the
same "not found" as an address in somebody else's account.
Rotating
POST /v1/api-keys/{id}/rotate gives a key a new secret and keeps
everything else: its id, what it reaches, its ceiling, its expiry, and
every key it minted, which keep working. That is the difference from
revoking. If your admin key mints a key per customer, rotating it does not
take a single customer down.
curl -X POST https://pidgeon.ai/api/v1/api-keys/$KEY_ID/rotate -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "grace_period_seconds": 3600 }'- The old secret stops at once, unless you pass
grace_period_seconds. It can keep working for at most 86400 seconds, a day, so your servers have time to move over. The key'sprevious_token_expires_atsays when it stops. - One previous secret at a time. Rotating again inside the window ends the older secret immediately.
- Who may rotate: the key itself, a key that minted it, or an owner of the account in the dashboard. Not a sibling and not its parent: those get the same "not found" as a key that does not exist.
- Not idempotent. Each call issues a new secret, and the response is the only place it appears, so retrying a call that timed out leaves you holding the secret from the call that answered.
- Idempotency keys survive it. They belong to the key's id, not its secret, so a retry that started under the old secret replays under the new one.
A key never expires unless you give it expires_at when you create it. A
key that stops working on a schedule nobody chose is an outage with a
calendar invite.
Revoking
A revoked key stops working immediately, and so does every key it minted, however deep. That is deliberate: a leaked key's first useful move is to mint a second one and keep it, so a revoke that stopped at the key you knew about would leave you believing you had rotated something you had not.
A key may revoke itself. If your software detects it has been compromised it can say so without waiting for a person.
The event log keeps which key made which call, so a key you are unsure about can be answered rather than guessed at.
The SDK
@pidgeon-ai/sdk is a thin typed client over exactly these endpoints, plus
the webhook verifier. It sets the header, parses the envelope, and turns a
failure into an error it returns rather than throws, carrying the same
code the API returned — nothing else, which is why it is worth using.
npm install @pidgeon-ai/sdkEvery TypeScript sample in these docs is written against it. The cURL and Python samples are the same calls without one, so nothing here is only reachable through a package.
Idempotency
Everything that sends mail accepts an Idempotency-Key header: sending,
replying, reply-all, forwarding, sending or scheduling a draft, answering an
invitation, and creating, changing or cancelling a calendar event that mails
its attendees. So do the creates — addresses, drafts, domains, labels, pods,
routing rules, webhooks.
The same key with the same request returns the first response, marked
Idempotent-Replay: true, and sends nothing. The same key with a different
request is a 409, because that is a bug in the caller rather than a retry.
A failure is not kept, so retrying after one runs the request again. Keys
are kept for 24 hours.
Make one key per send and reuse it on that send's retries. The SDKs take it
as an argument — headers: { 'Idempotency-Key': … } in TypeScript,
idempotency_key= in Python — and do not make one up for you.
curl https://pidgeon.ai/api/v1/messages -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Idempotency-Key: 8f21c-invoice-4417" -H "Content-Type: application/json" -d '{ "from": "hello@yourdomain.com", "to": "a@example.com", "text": "Hi" }'