We raised $6M in Seed FundingRead more
+
+
+
+
+
+
+
+
Blog/Engineering

Catch-all vs an inbox per agent

Catch-all plus a database row is a bad pattern for agents that reply or scale. Create one inbox per agent for identity, reputation, and thread state.

TL;DR

A catch-all plus a database row looks free, but it is a bad pattern for agents that send, reply, or scale. Spammers treat accept-all domains as open targets, and every agent shares one mailbox identity. Create a real inbox per agent instead, and use catch-all only for quiet intake you never reply from.

Coding agents that need email often start the same way. Point MX at a catch-all, parse the local part, write a database row, and treat that string as the agent's address. It costs nothing. It is instant. It scales to as many local parts as your database will hold.

That shortcut is tempting for quiet intake, and it is the wrong default for agents. Mail arrives, your app looks up the local part, and nobody expects a reply from that exact address. The moment agents multiply, reply as themselves, and need separate reputation, webhooks, and thread state, the catch-all stops being free. You are running a mail system by hand, and you have opened the door to spam and shared-identity failures that a real inbox API avoids.

The rest walks the catch-all plus database row pattern against creating a real inbox per agent through an API, so you know why catch-all fails for agents, when a silent intake exception still fits, and how to provision an inbox in one call.

What is a catch-all email address?

A catch-all routes mail sent to non-existent or incorrect addresses at a domain into one designated mailbox. Google Workspace documents this as catch-all routing: messages aimed at addresses that do not exist still land in a single account you choose.

The local part, the string before @, can be anything your DNS and mailbox provider will accept. agent-42@agents.example.com and signup-99@agents.example.com both arrive in the same catch-all mailbox. Your application, not the mail provider, decides what each local part means.

That is why the pattern feels free. The provider gives you one mailbox. You invent unlimited addresses in software. The mail stack still delivers. Your database is the directory.

Why do coding agents build a catch-all plus a database row?

Signup paths for coding agents already insert a row. Adding a local-part column is a small step. The agent gets local_part@your-catch-all-domain. Inbound mail hits the catch-all. Your worker parses To, looks up the row, and continues the job.

No provider call sits in that path, and there is no per-address fee or wait on provisioning. For a throwaway prototype that only receives verification codes or inbound notices, that is often enough to ship a demo.

The code path is familiar. Create user, generate a local part, store it, and point a worker at the catch-all mailbox or inbound route. When a message arrives, split on @, load the row, and hand the body to the agent. You stay inside tools you already operate.

Engineers reach for this because they already know MX and local parts. They have not always compared it to an inbox API that returns an address, a store, threading, and a reply identity in one response. Related reading on that split lives in Email API vs Inbox API.

Why catch-all is a bad pattern for agents

Catch-all is a spam magnet. Spammers run directory harvest and dictionary attacks: they fire mail at thousands of guessed local parts and watch which ones the server accepts. Without a catch-all, unknown recipients get rejected at SMTP time. With one, every guess delivers into your mailbox. Guides that walk this tradeoff, including Serif's catch-all overview and Hostaccent's catch-all writeup, treat that spam flood as the main reason operators turn catch-all off.

Accept-all domains also look risky to the rest of the mail ecosystem. Verification tools cannot tell a real mailbox from an invented one when the server answers success for every recipient, so they label the domain accept-all. Some senders scrub those addresses as risky. If you forward catch-all spam onward, you can also damage your own IP reputation and create backscatter when forged senders receive bounces.

For agents, those mail-ops problems stack on top of identity failures. Reputation is shared: every agent sends from the same domain and usually the same mailbox identity, so one noisy agent can hurt deliverability for the rest. Webhooks are shared: one inbound endpoint must fan out by local part, and you own retries, deduplication, and the mapping from address to agent. Thread state is hand-built: the catch-all stores mail in one pile, and matching In-Reply-To and References to the right agent conversation is code you write and keep.

Reply identity is awkward. Recipients see one mailbox or a From header you must authenticate carefully. When an agent should answer as itself, the catch-all has no first-class reply surface per agent. Isolation is soft: a bug in one agent's parser can touch every message in the shared mailbox, and auditing which agent sent a message means reconstructing history from your own tables instead of reading a mailbox boundary the provider enforces.

None of this is a reason to panic at two agents and zero outbound. It is a reason not to treat catch-all as the architecture for an agent fleet. Identity for agents more broadly is covered in Email as Identity for AI Agents.

How do you create an inbox per agent with an API?

An inbox API provisions a mailbox as a resource: address, durable store, threading, and an identity people can reply to. On AgentMail, create is POST /v0/inboxes. The body accepts username, domain, display_name, client_id, and metadata. Python creates an inbox with an idempotency key so retries are safe:

from agentmail import AgentMail
from agentmail.inboxes.types import CreateInboxRequest

client = AgentMail(api_key="YOUR_API_KEY")

inbox = client.inboxes.create(
    request=CreateInboxRequest(
        username="research-agent",
        client_id="tenant-acme-research-v1",
        metadata={"tenant_id": "acme", "role": "research"},
    )
)
print(inbox.email)
# research-agent@agentmail.to (or your verified domain)

The Node client uses the same fields with clientId in camelCase. The create call is awaitable and returns the inbox object:

import { AgentMailClient } from "agentmail";

const apiKey = process.env.AGENTMAIL_API_KEY ?? "";
if (apiKey.length === 0) {
  throw new Error("AGENTMAIL_API_KEY is required");
}
const client = new AgentMailClient({ apiKey });

const inbox = await client.inboxes.create({
  username: "research-agent",
  clientId: "tenant-acme-research-v1",
  metadata: { tenant_id: "acme", role: "research" },
});
console.log(inbox.email);
// research-agent@agentmail.to (or your verified domain)

Pass the same client_id again and you get the original inbox back with no duplicate. That matters when an agent restarts mid-setup and re-runs create. Keep client_id values deterministic per logical inbox, and never reuse one across unrelated resources.

username and domain are optional. Omit them and the platform generates an address on the default domain. Set domain to a verified custom domain, or to a subdomain of a verified domain with subdomains enabled, when the agent should send from a name you own. The full create surface is in the inboxes docs.

The Free plan is $0 per month with 3 inboxes and 3,000 emails per month, and no credit card required. API access has no per-seat charge. Caps rise on paid plans when you need more inboxes; see pricing.

When should you use a catch-all vs an inbox per agent?

Use the table as a decision aid. Catch-all is the exception for silent intake. Inbox per agent is the default once agents need identity.

DimensionCatch-all + DB rowInbox per agent (API)Best for
Cost to startProvider catch-all you already pay forFree tier includes 3 inboxesCatch-all only if the domain already runs it
Spam and harvest riskAccepts every guessed local partOnly real inboxes you createInbox API for any lasting agent address
ProvisioningInsert a local-part rowOne create call, optional client_idInbox API when agents spin up in code
Reply as the agentYou route From and auth yourselfBuilt into the inbox identityInbox API for two-way threads
ReputationShared across every local partPer inbox; custom domains on paid plansInbox API once send volume grows
Webhooks and threadsYou fan out and store statePer-inbox store and threadingInbox API for conversation agents
Scale shapeUnlimited local parts, one mailboxMany real inboxes via APICatch-all for silent intake only; inbox API for fleets

Catch-all plus a database row fits only low-volume intake that never replies as itself, and even then you should treat it as temporary. Inbox per agent fits agents that need their own address, replies, reputation, webhooks, and thread state. If you are still choosing between send-first pipes and mailbox primitives, start with Email API vs Inbox API. For fleet-scale naming and domains once you leave the catch-all, see Custom domains for thousands of agent inboxes.

What AgentMail does not do

AgentMail does not offer catch-all. There is no route that dumps every unknown local part into one mailbox. If you need that pattern, keep it on your mail provider and database. AgentMail provisions real inboxes per agent instead, with create, list, get, update, and delete on the inbox resource.

If silent intake on a domain you already own is the whole product, stay on catch-all. When each agent must send and receive as itself, create an inbox per agent.

AgentMail gives your agents real inboxes. Create inboxes via API. Send and receive Emails with 0 complexity. Free to start.

FAQ

Ready to build? Start integrating AgentMail into your AI agents today.

All systems onlineSOC 2 Compliant

Email Inboxes for AI Agents

support@agentmail.cc

Subscribe to our weekly newsletter.

© 2026 AgentMail, Inc. All rights reserved.

Privacy PolicyTerms of ServiceSOC 2Subprocessors