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/changes if 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.

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

ParameterMeans
inOne folder, including Trash, which q alone skips
fromSubstring of the sender's address or display name
toSubstring of any recipient
has_attachmentWhether there is at least one file
is_unreadWhether a person has read it
is_starredStarred
label_idOne 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.