A product that gives every new customer an email address at signup has three real options. Route a whole domain into one catch-all mailbox, buy a paid office-suite seat for each customer, or call an inbox API that creates a real mailbox inside the signup request. Each one trades cost, speed, and what happens when a signup runs twice.
Your product signs up a customer, and the customer needs an email address of their own. Maybe they forward invoices to it. Maybe an agent working for them sends follow-ups from it and reads the replies. Either way, the address has to exist the moment the account does. The first email can arrive minutes later.
That sounds like a small task until you count. One customer is a settings page. Ten thousand customers is a provisioning system, with a bill attached to every row and a failure mode for every retry. The choice you make on day one decides what each signup costs. It also decides how long a customer waits for a working inbox, and whether a timed-out request leaves two mailboxes behind.
There are three ways to do it. Below is what each one costs per customer and how fast it goes live. Then comes where each one breaks, and the code for the version that runs inside the signup handler.
Which way should you give each new customer a mailbox
Give each new customer a mailbox through an inbox API when they need to send and receive as themselves. Use a catch-all only when mail comes in and nobody replies. A seat per customer on Google Workspace or Microsoft 365 is the third way. It fits a narrow case: fewer than 300 customers who also want the calendar, documents, and chat that come with the seat.
The conditions decide it. Inbound only, modest volume, no reply from each address: catch-all. A few hundred customers who'll log in to Gmail or Outlook like employees: a seat. Every customer, or every customer's agent, holds its own conversations and signups arrive at any hour: an inbox API, either Nylas Agent Accounts or AgentMail.
One question settles most cases. Does the customer ever send? If the answer is no, you don't need a mailbox per customer at all. If it's yes, you need a real one, and the remaining choice is how you pay for it.
How the three ways compare on cost, setup time, and upkeep
Cost per customer is where the three ways split furthest apart. A catch-all costs nothing per signup. A seat costs a monthly license per signup. An inbox API sits between them, with a plan that includes a block of mailboxes and a price for each one past it.
| Way | Cost per customer | How each mailbox is created | What the customer gets | Best for |
|---|---|---|---|---|
| Catch-all routing on Google Workspace | Nothing per customer; one mailbox receives everything | None per customer once the routing rule exists | A string in your database, no reply identity | Inbound-only intake that never replies |
| Google Workspace Business Starter seat | $7 per user per month, at most 300 users on Business plans | A Directory API call, then up to 24 hours | A full Gmail account with 30 GB pooled storage, calendar, and documents | Under 300 customers who want the office suite |
| Microsoft 365 Business Basic seat | $7.00 per user per month paid yearly, 1 to 300 users | One licensed user per customer in your tenant | Exchange mail, Outlook on the web, Teams, and web Office apps | Under 300 customers who already live in Microsoft tools |
| Exchange Online Plan 1 seat | $4.00 per user per month paid yearly | One licensed user per customer in your tenant | A 50 GB mailbox with calendar and contacts | Mail-only seats where Microsoft hosting is required |
| Nylas Agent Accounts | Pro: $49 a month for 50, then $0.40 per extra | One POST to /v3/connect/custom returns a grant_id | A hosted mailbox and calendar with IMAP access | Products already built on the Nylas API |
| AgentMail | Developer $20 for 10, Startup $200 for 150, extras $2 each | One inboxes.create call inside the signup request, safe to retry | An address, threads, webhooks, and a pod per tenant | Customers or their agents that send and receive as themselves |
Read the table by column. A catch-all is free and gives the customer nothing but a string. Seats are real mailboxes priced like office software, because they are office software. Inbox APIs are real mailboxes priced like infrastructure. The right row depends less on price than on whether the customer ever sends.
Suppose 250 customers sign up in launch week. On Business Starter seats that's $1,750 a month, and a new user can wait up to 24 hours for Google services to work. On Exchange Online Plan 1 it's $1,000 a month. On Nylas Pro it's roughly $129 a month. On AgentMail Startup it's roughly $400 a month, because 100 of those inboxes sit past the 150 the plan includes. The catch-all is still free. It also still can't send a single reply as the customer.
When is a catch-all subdomain plus a database row enough
A catch-all subdomain plus a database row is enough when every message flows in and none flows out. Google Workspace catch-all routing sends mail for incorrect or non-existent addresses at your domain into one mailbox you choose. Your app hands each customer a string like acme@in.yourapp.com and stores it on their row. Each message is then routed by its To header.
That's a fine design for intake. Receipts forwarded into an expense tool, leads parsed into a CRM, alerts turned into tickets. Nobody expects a reply from acme@in.yourapp.com, so it doesn't need an identity.
It stops working the day a customer has to answer. Every reply leaves from the shared mailbox. Every customer shares one sender reputation. There are no threads per customer to read back, so your app rebuilds conversation history from raw messages.
A catch-all also accepts every guessed address. Spam aimed at random local parts lands in the same mailbox your parser reads, and your code has to tell a real customer address from an invented one. The longer argument for agents is in catch-all vs an inbox per agent.
What a paid seat per customer costs at signup
A seat per customer turns every signup into a monthly license. At the list prices for Google Workspace Business Starter and Microsoft 365 Business Basic, 300 customers cost $2,100 a month before anyone sends an email. Exchange Online Plan 1 brings the same 300 down to $1,200 a month, for mail alone.
Then you hit the cap. Google sells Starter, Standard, and Plus for at most 300 users. Microsoft lists Business Basic for 1 to 300. Customer 301 pushes a Google tenant to Enterprise, which has no user cap and no list price on the pricing page. That's a sales call in the middle of your growth curve.
Speed is the second cost. Google says a new user can take up to 24 hours before Google services are available to them. The Directory API creates at most 10 users per domain per second. Each user it creates is a license charge on your account. A launch-day spike of signups queues behind that limit, then waits on the 24 hours.
Sending is the one place seats are generous. A paid Gmail account sends up to 2,000 messages a day, and a trial account sends 500. For one customer's correspondence, that ceiling rarely matters.
Seats fit when the customer is a person who wants an office suite. They get Gmail or Outlook, a calendar, documents, and video calls. You get an admin console, password resets, and a license to reclaim every time someone churns. Nobody on your team planned to run an IT department for strangers. It's an office suite with a mailbox attached, priced like one.
How do you create a mailbox per customer in the signup request
You create a mailbox per customer in the signup request by calling an inbox API from the handler. It sits next to the insert that creates the user row. Two products are built for it.
Nylas Agent Accounts. A hosted mailbox and calendar from one POST to /v3/connect/custom. The call returns a grant_id that works with every other Nylas endpoint. IMAP, the standard protocol mail clients use to read a mailbox, is included. On price per extra mailbox, Nylas is lower than AgentMail, as the table shows. It fits teams that already use the Nylas API for calendar or contact sync.
AgentMail. The inbox is the API object. One create call takes a username, a verified domain, a display name, and a client_id, and it returns the new address. Custom domains start on the Developer plan. A subdomain like mail.yourapp.com works once it's verified. Here is the call inside a signup handler.
# pip install agentmail==2.0.14
from agentmail import AgentMail
from agentmail.inboxes import CreateInboxRequest
client = AgentMail(api_key="am_live_your_key")
def provision_inbox(user_id: str, handle: str, name: str):
# client_id is derived from your user id, so a retry returns this same inbox
return client.inboxes.create(
request=CreateInboxRequest(
username=handle,
domain="mail.yourapp.com",
display_name=name,
client_id=f"inbox-user-{user_id}",
)
)
inbox = provision_inbox("4821", "acme", "Acme Support")
print(inbox.email, inbox.client_id)
# Example output:
# acme@mail.yourapp.com inbox-user-4821Pick the username from something your system already keeps unique, like the account slug. The display name is what recipients see next to the address. Keep it the customer's own brand rather than yours, so a reply from acme@mail.yourapp.com reads as coming from Acme.
B2B products often need more than one mailbox per customer. A support agent, a billing agent, and a sales agent for the same company belong together. They also belong apart from every other company. Pods do that: one pod per customer, and every inbox and domain inside it is isolated from other pods.
# pip install agentmail==2.0.14
from agentmail import AgentMail
client = AgentMail(api_key="am_live_your_key")
# One pod per customer company, keyed to your tenant id
pod = client.pods.create(name="Acme Inc", client_id="pod-tenant-77")
billing = client.pods.inboxes.create(
pod.pod_id,
username="acme-billing",
domain="mail.yourapp.com",
display_name="Acme Billing",
client_id="inbox-tenant-77-billing",
)
print(pod.pod_id, billing.email)
# Example output:
# pod_8f2c41 acme-billing@mail.yourapp.comThe pattern for many tenants is in pods for multi-tenant email. Running a domain per customer is covered in custom domains for thousands of agent inboxes. If you're still deciding whether you need a mailbox at all, read email API vs inbox API.
What happens when signup retries or fails halfway
Signup handlers run twice more often than anyone plans for. A load balancer times out and the client retries. A user double-clicks. A job queue redelivers after a worker restart. An idempotency key is a value that makes a repeated request return the first result instead of acting twice. Any create call without one leaves deduplication to you. Every duplicate is a mailbox you pay for and a customer with two addresses.
AgentMail makes every create call idempotent through client_id. The first call with a given value creates the inbox and stores it against that value. Any later call with the same value creates nothing. It returns the original inbox with a 200. Derive the value from your own user id, never from a random string generated inside the retry loop. Keep @ out of it.
# pip install agentmail==2.0.14
from agentmail import AgentMail
from agentmail.inboxes import CreateInboxRequest
client = AgentMail(api_key="am_live_your_key")
request = CreateInboxRequest(username="acme", client_id="inbox-user-4821")
first = client.inboxes.create(request=request)
retry = client.inboxes.create(request=request) # no second inbox is created
print(first.inbox_id == retry.inbox_id)
# Example output:
# TrueHalf-finished signups need an order of operations. Write the user row first with a pending status. Create the inbox with the client_id from that row's id. Then store the address and mark the row active. Suppose the process dies after the inbox exists but before the row is updated. The next attempt calls create again and gets the same inbox back. If it dies before the create call, there's nothing to clean up.
Use one client_id per resource. The pod and the inbox for the same customer get different values, like pod-tenant-77 and inbox-tenant-77-billing. A value used to create an inbox should never be reused to create a webhook or a pod.
Churn is the other half of the lifecycle. Delete the inbox when the customer leaves, because a dormant inbox still counts toward your plan. Deleting an inbox also deletes its messages and threads. Export anything you're required to keep first. A pod can't be deleted while it still holds inboxes or domains, so remove those first and the pod last.
# pip install agentmail==2.0.14
from agentmail import AgentMail
client = AgentMail(api_key="am_live_your_key")
# Deletes the inbox along with its messages and threads
client.inboxes.delete(inbox_id="acme@mail.yourapp.com")
print("deleted")
# Example output:
# deletedEach way has a customer it fits. Catch-all routing fits inbound-only intake on a domain you already run. Google Workspace and Microsoft 365 seats fit a few hundred customers who want an office suite, and Exchange Online Plan 1 fits mail-only seats where Microsoft hosting is required. Nylas Agent Accounts fits products already built on the Nylas API. AgentMail fits products where every customer, or every customer's agent, sends and receives as its own address from the moment it signs up. More engineering posts are on the AgentMail blog.
What AgentMail does not do
AgentMail does not offer catch-all routing, so an inbound-only intake address belongs on your existing mail provider. Inbox count is a plan limit. A free-signup product with many dormant customers pays for every inbox it keeps, and creating the inbox on first use instead of at signup keeps that bill to active customers. It is not an office suite either, so customers who want documents, spreadsheets, and video meetings with their mailbox are better served by a seat.
AgentMail is the first email provider built for AI agents. Create an inbox in one API call, send and receive email, no OAuth and no domain verification. Free tier, no credit card required.


