Search the docs
Find a page, a section, or an endpoint.
Mailboxes and folders
Where mail is, and how to stay in step with it.
Every address with an inbox has one. A mailbox is folders with counts and a page of conversations in each — the shape a person sees, read and written by the API rather than only by the client.
The folders
Ten: inbox, starred, sent, drafts, scheduled, snoozed, archive,
spam, trash and unauthenticated. identity_id is required — there is
no merged view across addresses here, because two folders joined together are
a folder nobody has. For a list of conversations across every address, use
GET /threads, which is workspace-wide and whose rows now
carry the folder, the sender and a preview.
Four of them are destinations. folder on a conversation may be set to
inbox, archive, spam or trash. The rest are consequences of
something: starred is a flag, sent and drafts are what the message is,
and scheduled and snoozed hold a conversation until a time arrives.
Snoozing and scheduling are dashboard actions and have no API of their own —
you can read both folders, and a conversation leaves them when its time comes
rather than because you moved it.
Spam is never in inbox, and the inbox page says how much there is. It
carries held_back: { spam, how_to_include }, where spam is the number of
unread messages filed there. An empty inbox with spam above zero means the
mail came and the classifier distrusted it: read spam next.
unauthenticated is a view over the other nine, not a tenth place. Mail
in it says it is from somebody and failed to prove it — SPF, DKIM or DMARC —
and the same message is also in Inbox or Spam, wherever the classifier put
it. Nothing moves anything in or out. Read
authentication on the message for what the sending
domain's own records actually said.
curl "https://pidgeon.ai/api/v1/mailboxes?identity_id=0c939bd7-2d69-4150-a2ac-de994f0e4887" \
-H "Authorization: Bearer $PIDGEON_API_KEY"total and unread are null rather than 0 where the product does not
count that folder. Zero would be a claim; null says we do not count it.
One folder, a page at a time
A row is a conversation — the newest message in each thread — not a
message. It carries no body: a list of forty conversations does not want
forty bodies. GET /messages?thread_id= opens one.
curl "https://pidgeon.ai/api/v1/mailboxes/inbox?identity_id=0c939bd7-2d69-4150-a2ac-de994f0e4887&limit=50" \
-H "Authorization: Bearer $PIDGEON_API_KEY"cursor is opaque, and it is not an offset. It carries a sort key and an
id together, because several messages can arrive in the same millisecond and
a cursor on the timestamp alone repeats or drops rows at a page boundary. An
offset would be worse than either: rows shift underneath it and a client
syncing an inbox silently loses mail. Pass back what next_cursor gave you;
do not construct one.
A cursor we did not write is page one, not an error. It arrives in a URL, and a URL is a thing people edit.
Staying in step
An agent that watches a mailbox goes away and comes back, and the question it has is not "what is in the inbox" but "what has happened since I last looked". Paging the folder and diffing it against what you remember is the answer without this endpoint, and it is not one anybody should have to write.
curl "https://pidgeon.ai/api/v1/mailboxes/changes?identity_id=0c939bd7-2d69-4150-a2ac-de994f0e4887&since=$CURSOR" \
-H "Authorization: Bearer $PIDGEON_API_KEY"The first cursor comes off a listing, not from this endpoint, and
sync_cursor sits beside data on every page of GET /mailboxes/{folder}.
It is not a page cursor: the two are different reads and each refuses the
other, with a sentence saying which you handed it.
Per mailbox, not per folder. A message moving from the inbox to the
archive is one event; asking per folder would deliver it as a disappearance
in one poll and an appearance in another, and would mean nine polls where one
does. Each row carries folder, so you learn where it went.
Arrivals are in it. New mail is the most important thing that happens to a mailbox, and a client polling the listing for arrivals and this for changes would be polling twice for one job.
A deletion carries the id and nothing else. There is nothing else left — the message is gone, and keeping a record of what it said would make "delete" mean "delete most of it".
since is not a page cursor. Both are opaque, and they walk different
orders: cursor pages a folder newest-first, since walks change time.
Passing one where the other belongs is refused rather than answered, because
the wrong answer would be a convincing one.
410 resync means start again. We keep the record of what was deleted for
thirty days — the same window raw messages get, because a record that a
message was deleted should not outlive the message. A cursor older than that
cannot be answered completely, and an empty delta would be indistinguishable
from "nothing changed".
Taking all of it with you
GET /v1/export streams every message your key can read as one mbox file —
the format Apple Mail, Thunderbird and most archivers import. Pass
identity_id for one address. Each message carries an X-Pidgeon-Mailbox
header, so a file with several addresses in it can be split again, and
originals come back byte for byte while storage still holds them. Four
exports an hour per workspace.
curl https://pidgeon.ai/api/v1/export \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-o pidgeon.mbox