Search the docs

Find a page, a section, or an endpoint.

Allow and block lists

Who may write to an address, and who it may write to.

Two lists, one per direction. Who may write to an address, and who that address may write to. They are separate endpoints because they answer different questions and have different consequences — one decides what you are charged for storing, the other decides what leaves.

Both are scoped to a single address. There is no list that silences a sender for a whole account over the API, deliberately: a key that administers one address must not be able to change what fourteen other addresses receive.

With no rules at all

An address with no receive rules accepts mail from everybody, and the spam filter decides where each message lands, unless its receive_policy is list (see Accept only your allow list). An address with no send rules may write to whoever its send policy allows: anyone under open, the people who wrote first under reply_only, and nobody under list. An empty list never blocks mail and never lets anything through on its own; it hands the decision to the default.

The order they apply in

Receiving, for each message, stops at the first step that decides:

  1. The address must exist. Mail to an address nobody owns, and no routing rule catches, is delivered nowhere; no rule is read.
  2. This address's own rule about the sender, if one matches.
  3. Otherwise an account-wide rule from the dashboard, if one matches.
  4. Under receive_policy: list, the message is refused unless that rule is inbox and the message passed DMARC.
  5. Otherwise the spam filter files it, by its content and its SPF, DKIM and DMARC results.

Within one list, a rule naming an address beats a rule naming its domain. A rule matches the From: header, which the sender writes. So a rule can take a message out of spam only when it passed DMARC: inbox on acme.com rescues real acme.com mail, and a forged acme.com message the filter called spam stays in spam. The same goes for archive and trash. SPF or DKIM passing without a DMARC verdict doesn't count, because either can pass for the forger's own domain. Mail between two Pidgeon addresses passes: it arrives with a verified signed sender (authentication.signature: "pass"). Rules that make mail less trusted (reject, spam, and filing mail the filter let through) apply whatever the authentication says. Nothing is refused for failing authentication unless you wrote a rule that refuses it. The Firewall verdict is worked out separately and no rule changes it, so read firewall and the authentication results rather than trusting the folder.

Sending, for each recipient:

  1. A matching rule decides: block refuses, allow permits. An address rule beats a domain rule, so block acme.com with allow bob@acme.com lets Bob through.
  2. Otherwise the send policy decides.

Who may write to you

GET    /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules
POST   /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules   { "action": …, "value": … }
DELETE /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules?rule_id=…

value is an address or a domain and the string says which: billing@acme.com matches one person, acme.com matches everybody there. You are not asked to classify it, because a caller asked to classify it will eventually classify it wrong.

action is one of five:

actionWhat happens
rejectThe message is refused before it is stored. No row, no thread, nothing.
spamStored, and filed as spam.
trashStored, and filed as deleted.
archiveStored, and skips the inbox.
inboxNever file this sender as spam, when its mail passes DMARC. The rescue.
curl -X POST https://pidgeon.ai/api/v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-d '{ "action": "reject", "value": "noreply@spam.example" }'

reject is the one worth understanding

The other four decide where a message goes. reject decides whether there is one at all — it refuses before the insert, so nothing is stored, nothing is threaded, no storage is charged and no webhook fires.

That last part is the reason it exists. message.spam is a deliverable event, so before this an address under sustained abuse was paying storage and webhook deliveries for every message its owner had already said they did not want. For an inbox a person reads, spam is right — you want to be able to look. For an address no human will ever open, reject is.

It is not a security control. The From: header is written by the sender, and anybody blocked can change it for the price of a new address. What reject buys is not paying for mail from senders who have not bothered, which is most of them. If you need to be certain who sent something, read the authentication results on the message rather than its From:.

The more specific rule wins

A rule naming an address beats a rule naming its domain, so blocking a whole domain and letting one person through does what it looks like:

reject  acme.com              — everybody at acme.com
inbox   billing@acme.com      — except billing@, which is never spam

And an address's own rule beats an account-wide one. Rules made in the dashboard apply to every address; rules made through this endpoint apply to one, and the one wins. That ordering is what makes this address receives verification codes from one service and nothing else expressible without touching anything else you own.

Accept only your allow list

Set an address's receive_policy to list and only mail from your allow list reaches it. Your allow list is the inbox rules: this address's own, or account-wide ones from the dashboard. Everybody else is refused the way reject refuses: before anything is stored, with no webhook, no event and nothing for your agent to read. Refused mail shows in the dashboard under Senders → Refused.

PATCH /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348   { "receive_policy": "list" }
POST  /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules   { "action": "inbox", "value": "billing@acme.com" }
receive_policyWho may write to it
openAnyone. The default, and what every address did before this existed.
listOnly senders with an inbox rule, on mail that passes DMARC.

A listed sender counts only when the message is authenticated. The From: header is written by the sender, so a list that trusted it would let in anybody who can guess one name on it. A message gets through only when it passed DMARC, or came straight from another Pidgeon address with a verified signed sender (authentication.signature: "pass"). SPF or DKIM passing on their own don't count. So a sender whose domain publishes no DMARC record is refused even when listed; their mail can't prove who sent it.

No bounce goes back. Most refused mail under list is from strangers or forgers, and a bounce to a forged From: lands on somebody who never wrote.

A key with admin on the address can turn list on, but not off. That holds even on an address that belongs to a person, where no key can change anything else. Turning it back to open needs the address's owner, from the dashboard. send_policy follows the same rule. GET /receive-rules returns the policy beside the rules.

Who you may write to

GET    /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/send-rules
POST   /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/send-rules   { "effect": "allow" | "block", "value": … }
DELETE /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/send-rules?rule_id=…

effect rather than action, because the outbound side has a verdict rather than a destination. The same matching applies — address beats domain, adding replaces.

These rules hang off the address's send policy, which GET returns beside them and which is set with PATCH /v1/identities/{id}:

send_policyWho it may write to
openAnyone.
reply_onlyAnyone who has written to it first, plus its allow list.
listOnly its allow list.

A block rule refuses under every policy, including open. A block list that a setting could switch off is not a block list. An allow rule matters only under reply_only and list; under open everybody is already allowed.

A refusal tells you which kind it was in error.code: blocked means a rule said no, not_permitted means the policy did. Both are 403, and the difference is where you go to fix it — a rule somebody wrote, or the address's send_policy. For a while both answered forbidden and the only way to tell was to read the sentence, which is not a thing to ask of a program.

A refusal is whole. One disallowed recipient refuses the send and nothing reaches anybody, including the recipients that were fine — see sending for why that is not rejected.

Reply is not a third list

An address that may receive from somebody and must not answer them is one block rule on the send side. There is no separate reply list, because it would be a second way to express a rule that already exists.

reply_only is the opposite case — may write, but only to people who wrote first — and it is a policy rather than a list.

Reading the rules from an agent

The MCP server exposes both as read-only tools — list_receive_rules and list_send_rules — so an agent asking why did nothing arrive or why was that refused can answer it. Writing a rule is deliberately not a tool: an agent that can edit its own lists can quietly stop its own mail.