The short answer
Amazon SES gives you sending, DKIM signing and a receipt rule that drops each incoming email into S3 and announces it on SNS; everything that turns that into an agent's inbox is yours. That means verifying the notifications, parsing raw MIME, storing and threading messages, screening what an agent will read, delivering webhooks with retries and keeping each tenant's reputation apart. Pidgeon runs on SES and S3 itself, so this is the list of what we built; build it yourself when you need control of every byte and have one tenant or someone to run it.
Receiving
An SES receipt rule does two things with an email: writes the raw bytes to an S3 bucket and publishes a notification to an SNS topic. Your domain's MX points at the SES inbound endpoint for one region, so receiving lives in whichever region you chose.
The endpoint SNS calls is public, so it has to prove each notification is genuine before reading anything: check the SNS signature, fetch the signing certificate only from an AWS host, and match the topic. SNS retries when you answer with an error, so the same notification can arrive more than once. Record each one as claimed before you process it, and treat "claimed but never finished" as a retry rather than a duplicate, or a transient failure loses the message. A unique key on the stored message is the real guard against delivering it twice. Anything that fails needs a job that picks it up again later.
Raw MIME and parsing
What lands in S3 is MIME as the sender's client wrote it: nested multiparts, a dozen charsets, inline images, calendar invitations and attachments that may be the only thing in the message worth reading. Parse it once, with a library you trust, and keep the original bytes, because a parse is a reading of the message and the original is the evidence. Pidgeon writes a copy to its own archive and sets aside, under a separate prefix, any message whose parse reported a problem.
Then comes the part that matters most to an agent: separating the new text from the quoted history. Gmail, Outlook and Yahoo each mark quotes differently, Outlook often with a header block and no blockquote at all, and the "On … wrote:" line comes in whatever language the sender's client uses. A forward is the opposite case: what it carries is the content, so it should not be cut. Get either wrong and the agent reads three copies of its own last email, or none of the invoice.
Storage and threading
A webhook payload is not an inbox. Something has to store each message with its attachments, file it into a thread, track what was read and let a person look at it next month.
Threading on SES has a trap of its own. When you send through SES, the recipient sees SES's Message-ID, not the one you set, so a reply quotes an id you never stored. Keep the MessageId the send call returns and match replies against it as well as your own. Beyond the headers, decide what joins a thread when they are missing: Pidgeon falls back to the normalised subject within 30 days, and a newly composed message never joins an existing conversation, so five identical requests to five suppliers stay five threads.
Screening
SES adds verdicts for SPF, DKIM, DMARC, spam and viruses to each notification. Those are inputs, not decisions. An agent needs more: whether the visible sender is authentic enough to trust a code it sent, whether the HTML hides text a person would never see, and whether anything in the body is addressed to a model. Pidgeon reads SES's authentication verdicts, runs its own spam classifier rather than SES's verdict, and puts every inbound message through Pidgeon Firewall before the agent reads it.
Webhooks and retries
Your agent needs to hear that mail arrived, and an HTTP call made during ingestion has nowhere to record that it failed. So delivery is a queue: one row per event per endpoint, claimed before it is sent so two workers never send it twice, and signed with HMAC so the receiver can check it.
Then the policy. Pidgeon tries seven times over a little more than seven hours, then keeps the event as a dead letter that can be replayed by hand. An endpoint that times out is held back so it cannot delay everyone else's deliveries, and one that fails twenty times in a row is disabled. Delivery is at least once, so every event carries a stable id for the receiver to deduplicate on.
Sending and per-tenant reputation
Sending is the part SES does best, and it still leaves work:
- DKIM. Easy DKIM issues three CNAMEs. SES may take up to 72 hours to see them and then stops looking; the documented fix is to start the set-up again from the beginning, and Pidgeon does that by recreating the identity, which means new tokens to publish.
- Delivery events. Bounces and complaints reach you through a configuration set whose event destination publishes to SNS, and every send has to name it. A permanent bounce or a complaint should suppress the address, and a transient bounce should not.
- Pacing. Your account has a maximum send rate. Pidgeon reads it from SES in the background and keeps every send under it in one shared bucket.
- Tenants. SES can attribute sends to a tenant: create it, associate the sending domain and name it on every send. Pidgeon makes one per pod, so when SES pauses a tenant for its bounce or complaint rate, only that customer's mail stops. A pod's state is visible as
reputation.sendingonGET /v1/metrics(see pods). SES allows 10,000 tenants per account.
Without tenants, every customer shares one reputation, and the one who mails a stale list sets it for the rest.
When building it is the right call
Build it on SES directly when:
- The mail is intake, not conversation. A receipt rule into a Lambda that parses invoices needs most of this list not at all.
- You must hold the bytes in your own AWS account, for a contract or a regulator, and can staff what that implies.
- You have one tenant and an engineer who will own receiving, threading and deliverability as a product, including the on-call.
Use an inbox API when many customers each need their own addresses, a person will read along, or the agent's value is in the conversation rather than the plumbing. Pidgeon gives you all of the above, starting from one call to POST /v1/identities, with your own domain on every plan and the mail stored in the EU.
Start
The inbox quickstart shows the result: an address, a send and a webhook in a few calls. Pods covers the per-tenant reputation, threads the matching rules, and retries the delivery schedule. If you are comparing the two shapes first, read email API vs inbox API.