Search the docs
Find a page, a section, or an endpoint.
Messages
Reading one, acting on it, undoing that, and searching.
Reading a message is a GET. Everything a person does to one — archiving it,
marking it spam, putting it back — is a state change the API makes as well,
and every one of them can be undone.
Changing a conversation
Two fields you can set with a key: is_starred is about the mailbox, and
folder is where the conversation lives.
curl https://pidgeon.ai/api/v1/messages/msg_9a13c \
-X PATCH \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_starred": true, "folder": "archive" }'is_read is about a reader, and a key is not one
There is a third field, and setting it with an API key is refused. Read
state belongs to a person: the unread count answers "has somebody looked at
this", and software looking at a message is not that. So is_read is
readable — it tells you whether anybody has read it — and not writable by a
credential with no person behind it.
That is a deliberate refusal rather than a missing feature. It used to succeed and do nothing, which is worse: your next read would say the message was still unread and the bug would look like yours.
What you probably want instead:
folder: "archive"if the point is "I have handled this". It is durable, it is what a person would do, and it moves the conversation out of the inbox for everybody.GET /mailboxes/changesif the point is "do not show me this twice". A cursor is the right way to track what you have already processed, and it survives your process restarting.
Everything here is thread-scoped: archiving one message of a conversation
and leaving the rest in the inbox is never what anyone means. The four
folders you can move a conversation to are inbox, archive, spam and
trash — the ones that are places. Starred is a view over a flag, which is
why it is is_starred and not a destination.
POST /messages/actions applies one action to up to 200 conversations, which
is the same cap the dashboard uses.
Undo
Unusual for an API to have, and the argument is that software makes mistakes at a speed a person does not. An agent that archived the wrong forty conversations should be able to say so.
curl https://pidgeon.ai/api/v1/messages/undo \
-X POST \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "undo": [ ... ] }'The undo value is the rows as they were before. Hand it back; do not read
it. Access to the messages it names is re-checked, so the worst a tampered
one can do is set flags on mail you can already set flags on.
Search
Full-text over one mailbox, ranked by relevance. A result carries the folder it was found in — the whole reason you are searching is that you do not know where it lives.
curl "https://pidgeon.ai/api/v1/messages/search?identity_id=idn_4f2a1&q=invoice&from=marcus&is_unread=true" \
-H "Authorization: Bearer $PIDGEON_API_KEY"Not paged: page two of a relevance ranking is a place nobody goes. Narrow the query instead — which is what the filters are for.
Narrowing it
| Parameter | Means |
|---|---|
in | One folder, including Trash, which q alone skips |
from | Substring of the sender's address or display name |
to | Substring of any recipient |
has_attachment | Whether there is at least one file |
is_unread | Whether a person has read it |
is_starred | Starred |
label_id | One label, by id from GET /labels |
Everything is optional except having something: a filter with no words is
a legitimate search — "everything unread from Marcus" is a question — and
neither words nor filters is not, so that is a 400 rather than your whole
mailbox.
in is the only way to look in Trash. A plain search skips it on purpose,
because finding something you threw away is not usually what you meant.
label_id is an id rather than a name, and it is the same parameter
GET /mailboxes/{folder} takes. A label can be renamed; a stored query that
quietly started matching a different pile would be a bug nobody could see. An
id that names nothing on that mailbox is a 400 rather than an empty page —
a wrong id is a bug in your code, and an empty list would let you ship it.
These are the same seven things the mail client's search box understands as
from:marcus is:unread label:Invoices. They are parsed into these parameters
before anything leaves the browser, which is why the API takes fields rather
than a syntax you would have to learn. label: is the one that differs: the
box takes the name you see and resolves it, because that is what a person
types.