Building a safe multi-tenant email delivery pipeline with the MailChannels Email API
By Ken Simpson | 16 minute read
Email delivery is straightforward when one trusted application sends one predictable stream of transactional messages. It becomes a very different engineering problem when your customers, plugins, or AI agents can decide what to send, who should receive it, and which domain should appear in the From header. At that point, you are no longer building an email delivery pipeline. You are building a multi-tenant security boundary.
We designed the MailChannels Email API around that problem. In this post, we explain how to build a multi-tenant sending platform on it: how sub-accounts establish per-tenant isolation, how Domain Lockdown turns domain authorization into a DNS-verifiable policy, how send limits act as circuit breakers, and how per-tenant suppression lists and signed webhooks close the feedback loop. Along the way, we compare this design with an AWS-native implementation on Amazon SES. Not because SES cannot do the job, it clearly can, but because the comparison makes the central design question concrete: how much of the email platform do you want to own?
If you run a SaaS product, a hosting platform, or an agent framework where other people’s code decides what mail gets sent, this post is for you.
The real challenge is not sending the message
Consider a SaaS application that sends email on behalf of thousands of customers. Each customer might send invoices, password resets, usage reports, invitations, notifications, or AI-generated correspondence from its own domain.
The basic delivery path looks deceptively simple:
Application event
|
v
Render a message
|
v
Call an email API
|
v
Recipient mailbox
But a safe production implementation must also answer questions such as:
- Which customer initiated the message?
- Is that customer authorized to send from the stated domain?
- Has the customer exceeded its permitted usage?
- Is the recipient already suppressed?
- Has this customer suddenly changed its sending pattern?
- Is it sending to a suspicious number of invalid recipients?
- Does the message resemble spam, phishing, malware delivery, or credential theft?
- Is one customer’s behaviour beginning to threaten the reputation of the rest of the platform?
- Should the message be accepted, delayed, rejected, or dropped?
- How will the application receive bounce, complaint, delivery, and unsubscribe events?
- How quickly can an operator contain a problematic customer without interrupting everybody else?
You can build those capabilities around a general-purpose email delivery service. The question is whether operating that control plane is a good use of your engineering and trust-and-safety teams.
What an AWS-native email pipeline looks like
Amazon’s recent post, Build an AI email pipeline with Amazon Bedrock and SES Mail Manager, is a useful illustration of how managed services can simplify email processing. Its example receives documents by email, scans them, archives them, classifies them with Amazon Bedrock, extracts attachments, and routes them into customer-specific storage. The resulting architecture includes an SES Mail Manager ingress endpoint, a traffic policy, a rule set, security add-ons, an archive, Amazon S3 buckets, two Lambda functions, two DynamoDB tables, Bedrock, IAM policies, and infrastructure-as-code.
To be clear about scope: that is an inbound document-processing pipeline, while the MailChannels Email API is an outbound email delivery service. We do not replace Bedrock, Lambda, or S3 when you need to classify incoming documents.
But the Amazon architecture exposes an important design question: how much email infrastructure should your application team have to assemble and operate themselves?
For an outbound SaaS platform, many of the hardest components are not involved in rendering or transmitting a message. They exist to decide whether a particular customer should be allowed to send that particular message, and to prevent one compromised, careless, or malicious customer from damaging everyone else.
Amazon SES has tenant primitives, but you still operate the email platform
Amazon has substantially improved the multi-tenant capabilities of SES. SES tenants can isolate sending resources and reputation, associate identities and configuration sets with particular tenants, provide tenant-specific credentials, apply reputation policies, pause problematic tenants, and maintain tenant-level suppression lists. This is meaningfully better than treating every downstream sender as part of one undifferentiated SES account.
Implementing that architecture still involves assembling and configuring the underlying AWS resources. Amazon’s own tenant implementation guide describes creating tenants, associating verified identities, configuration sets, and IP pools, updating IAM permissions and sending code, and connecting CloudWatch and EventBridge monitoring. In other words, SES provides increasingly capable AWS-native building blocks, but your team remains responsible for constructing and operating the resulting email platform.
The MailChannels Email API starts one abstraction level higher. The table below compares who owns each responsibility under the two approaches:
| Responsibility | AWS-native SES implementation | MailChannels Email API |
|---|---|---|
| Tenant boundary | Create a tenant and associate identities, configuration sets, templates, credentials, and potentially IP resources | Create a sub-account with its own API credentials and controls |
| Domain authorization | Verify identities and maintain the correct tenant-to-resource associations | DKIM plus Domain Lockdown authorization tied to an account, sub-account, or sender identity |
| Usage control | Implement the desired quota, alerting, and enforcement policy | Set a native sub-account sending limit beneath the parent account ceiling |
| Reputation management | Configure tenant reputation policies and respond to reputation findings | Built-in reputation, content, recipient, identity, and behavioural analysis |
| Abuse detection | Add application-specific inspection or third-party security components where required | Integrated spam, phishing, malware, credential-theft, URL, and behavioural detection |
| Suppression | Configure tenant suppression scope and bounce or complaint reasons | Independent sub-account suppression lists, including automatic complaint and certain hard-bounce suppression |
| Events | Configure destinations using services such as EventBridge, CloudWatch, SNS, or Firehose | Send signed HTTPS webhooks to the application |
| Containment | Configure policies and automation to pause or modify a tenant | Limit, suspend, defer, reject, drop, or otherwise constrain the narrowest risky identity |
The difference is not that AWS is incapable of supporting a sophisticated multi-tenant email platform. The difference is how much of the platform you want to own.
A smaller outbound architecture
A typical implementation on the MailChannels Email API looks like this. Your application keeps the decisions only it can make, and everything below the API call happens inside our infrastructure:
Customer, application, or AI agent
|
v
Your application
- authenticate the customer
- render the message
- select the tenant credential
|
v
MailChannels Email API
- sub-account isolation
- domain authorization
- usage enforcement
- reputation analysis
- content and URL analysis
- recipient and suppression checks
- behavioural abuse detection
- delivery and retry handling
|
v
Recipient mail systems
MailChannels webhooks ───────> Your event handler
- delivery state
- bounces
- complaints
- unsubscribes
- engagement events
Your application remains responsible for deciding what legitimate mail should be sent. We take responsibility for evaluating whether the resulting traffic is safe to inject into the global email ecosystem.
That is a much narrower application boundary. The rest of this post walks through each piece of it: tenant isolation, provisioning, domain authorization, the sending hot path, abuse management, suppression, and event handling.
Make each tenant an explicit security boundary
We provide two complementary isolation mechanisms: sub-accounts and sender IDs.
A sub-account is appropriate when a customer or workload needs its own operational boundary. Each sub-account can have:
- Separate API credentials
- An independent send limit
- Its own suppression list
- Its own webhook configuration
- Separate usage reporting
- Independent suspension and activation
- Reputation isolation from other sending identities
Sender IDs and campaign identifiers provide finer attribution within a sub-account, allowing both our systems and your application to distinguish particular applications, agents, campaigns, or sending streams without creating an excessive number of accounts.
A useful rule is to create a sub-account when you need an independently controlled trust boundary, not simply whenever another sending domain appears.
For example, a project-management SaaS provider might create one sub-account for each customer organization. Within a large customer’s sub-account, it could use sender or campaign identifiers to distinguish billing notifications, user invitations, automated project updates, and AI-generated summaries.
If the AI-summary stream begins behaving abnormally, it can be identified and constrained without treating every message from that customer, or every customer on the platform, as equally risky.
Provision a tenant during customer onboarding
Tenant provisioning can be incorporated directly into your application’s onboarding workflow. A typical sequence is:
- Create the sub-account using the parent account’s API key.
- Assign a maximum number of messages for the billing period.
- Create a sub-account API key.
- Store the returned key in your secrets manager.
- Configure the customer’s sending domain and Domain Lockdown authorization.
- Configure the customer-specific webhook endpoint or event-routing policy.
The official MailChannels SDKs expose the same lifecycle in JavaScript, Python, and PHP:
| Operation | JavaScript | Python | PHP |
|---|---|---|---|
| Create sub-account | subAccounts.create(companyName, handle) |
SubAccounts.create(company_name=..., handle=...) |
subAccounts->create(companyName: ..., handle: ...) |
| Set send limit | subAccounts.setLimit(handle, { sends }) |
SubAccounts.Limits.set(handle, sends=...) |
subAccounts->limits->set(handle, sends: ...) |
| Create API key | subAccounts.createApiKey(handle) |
SubAccounts.ApiKeys.create(handle) |
subAccounts->apiKeys->create(handle) |
| Retrieve usage | subAccounts.getUsage(handle) |
SubAccounts.retrieve_usage(handle) |
subAccounts->retrieveUsage(handle) |
The generated sub-account API key is shown only once and should be placed directly into secure secret storage rather than logged or retained in ordinary application data.
The send limit acts as a straightforward circuit breaker. Setting a tenant’s limit to zero pauses its traffic without changing the parent account or interrupting other customers. The parent account’s own limit remains the hard ceiling across its sub-accounts.
Authorize the customer’s domain
Possession of an API key should not automatically grant permission to send from every domain on the internet.
Domain Lockdown provides a DNS-based authorization layer that declares which MailChannels account, sub-account, or sender identity may send email from a domain. During message processing, we evaluate that authorization and reject traffic when the authenticated sender does not match the domain owner’s published policy.
This protects against an important multi-tenant failure mode: a legitimate customer account being used to spoof another customer, or an unrelated brand, while still passing SPF through the shared delivery provider.
Domain Lockdown complements DKIM, SPF, and DMARC. Those standards establish authentication and alignment. Domain Lockdown answers an additional platform-specific question: is this particular MailChannels customer identity authorized by the domain owner to send this mail?
That relationship is especially valuable when customers, plugins, or AI agents can influence message headers dynamically.
The hot path is one asynchronous API call
Once a tenant has been provisioned, the normal sending path does not need to know about the parent account or other tenants. The application instantiates the SDK using the tenant’s API key and queues the message.
We recommend asynchronous sending for most application traffic. The API acknowledges the request without making the application wait for the complete delivery process, and subsequent processing and delivery events are reported through webhooks.
Install the appropriate official SDK:
# JavaScript
npm install mailchannels-sdk
# Python
pip install mailchannels
# PHP
composer require mailchannels/mailchannels-php guzzlehttp/guzzle
The package names and examples below correspond to the official MailChannels JavaScript, Python, and PHP SDKs.
JavaScript
import { MailChannels } from "mailchannels-sdk";
const requiredVariables = [
"TENANT_API_KEY",
"FROM_EMAIL",
"TO_EMAIL",
];
for (const variable of requiredVariables) {
if (!process.env[variable]) {
throw new Error(`Missing required environment variable: ${variable}`);
}
}
const mailchannels = new MailChannels(process.env.TENANT_API_KEY);
const { data, error } = await mailchannels.emails.sendAsync({
from: {
email: process.env.FROM_EMAIL,
name: process.env.FROM_NAME ?? "Acme",
},
to: {
email: process.env.TO_EMAIL,
name: process.env.TO_NAME ?? "Customer",
},
subject: "Your monthly report is ready",
text: "Your monthly report is ready. Sign in to view it.",
html: "<p>Your monthly report is ready. Sign in to view it.</p>",
});
if (error) {
throw new Error(`MailChannels request failed: ${JSON.stringify(error)}`);
}
console.log("Message queued:", data);
Python
import os
import mailchannels
def required_environment_variable(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
tenant_api_key = required_environment_variable("TENANT_API_KEY")
from_email = required_environment_variable("FROM_EMAIL")
to_email = required_environment_variable("TO_EMAIL")
mailchannels.api_key = tenant_api_key
response = mailchannels.Emails.queue(
{
"from": {
"email": from_email,
"name": os.environ.get("FROM_NAME", "Acme"),
},
"to": [
{
"email": to_email,
"name": os.environ.get("TO_NAME", "Customer"),
}
],
"subject": "Your monthly report is ready",
"text": "Your monthly report is ready. Sign in to view it.",
"html": "<p>Your monthly report is ready. Sign in to view it.</p>",
}
)
print("Message queued:", response)
PHP
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use MailChannels\Client;
function requiredEnvironmentVariable(string $name): string
{
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException(
sprintf('Missing required environment variable: %s', $name)
);
}
return $value;
}
$tenantApiKey = requiredEnvironmentVariable('TENANT_API_KEY');
$fromEmail = requiredEnvironmentVariable('FROM_EMAIL');
$toEmail = requiredEnvironmentVariable('TO_EMAIL');
$client = new Client(apiKey: $tenantApiKey);
$response = $client->emails->queue([
'from' => [
'email' => $fromEmail,
'name' => getenv('FROM_NAME') ?: 'Acme',
],
'to' => [
'email' => $toEmail,
'name' => getenv('TO_NAME') ?: 'Customer',
],
'subject' => 'Your monthly report is ready',
'text' => 'Your monthly report is ready. Sign in to view it.',
'html' => '<p>Your monthly report is ready. Sign in to view it.</p>',
]);
print_r($response->toArray());
The three examples perform the same essential operation: select the tenant credential, construct the message, and queue asynchronous delivery.
Because the request is authenticated with the sub-account’s key, we automatically evaluate it in the correct tenant context. Usage is charged against that sub-account, its send limit applies, its suppression list is checked, and its webhook configuration receives the resulting events.
There is no need for the application to pass a loosely trusted tenant_id and hope that every downstream component applies it correctly. The credential itself establishes the operational boundary.
Abuse management is part of the delivery path
Tenant isolation is necessary, but it is not sufficient.
A platform can isolate ten thousand customers perfectly and still suffer poor deliverability if it allows each isolated tenant to send phishing, malware, unsolicited email, or messages to badly maintained recipient lists.
Amazon SES tenant management continuously monitors signals including bounce rates, complaint rates, feedback-loop data, and third-party reputation findings, and configured reputation policies can warn about or pause a problematic tenant.
Our reputation and abuse-control layer evaluates messages before delivery, and its analysis can incorporate:
- The parent account, sub-account, sending domain, sender identity, and campaign
- Domain Lockdown authorization and authentication posture
- Sender, domain, IP, URL, and historical reputation
- Message body, attachments, headers, links, and authentication alignment
- Spam, phishing, malware, and credential-theft characteristics
- Sending volume and velocity
- Invalid-recipient and hard-bounce patterns
- Complaint and unsubscribe behaviour
- Sudden changes in content, domains, URLs, recipient lists, or sending volume
- Signals associated with purchased, scraped, or algorithmically generated recipient lists
We can then accept, defer, reject, drop, or apply additional controls to the message at the narrowest available identity boundary. A suspicious campaign can be treated differently from the rest of the sub-account, and a problematic sub-account can be constrained without unnecessarily interrupting other customers.
This changes the role of your application. Instead of trying to reproduce years of email-abuse intelligence in a series of Lambda functions and database tables, your application supplies strong identity context and lets our systems apply continuously updated controls at delivery time.
Suppression is tenant-aware
A recipient’s decision to reject or complain about one tenant’s messages should not necessarily prevent another tenant from sending a legitimate message to the same address.
We therefore maintain a separate suppression list for each sub-account. Complaints create suppression entries automatically, as do certain hard bounces such as invalid-recipient responses. Applications can also create and remove suppression entries through the API.
We distinguish between two suppression types:
- Transactional suppression, which blocks all mail to the recipient
- Non-transactional suppression, which blocks marketing, lifecycle, newsletter, and promotional messages while permitting necessary transactional mail
This distinction matters in real applications. A customer who unsubscribes from product announcements may still need to receive a password reset, security alert, invoice, or account-closure notice.
Your application should still maintain its own source of truth for consent, subscriptions, and customer communication preferences. The suppression layer provides an additional enforcement boundary at the point of delivery.
One webhook endpoint closes the loop
A production email pipeline does not end when the send API returns.
We send HTTPS webhook events for message processing, delivery, drops, hard and soft bounces, complaints, unsubscribes, opens, and clicks. Your application verifies the webhook signature, correlates the event with its internal message record, and updates customer-visible status or preference data.
A typical handler performs only a few application-specific tasks:
processed or delivered
-> update delivery status
hard-bounced
-> mark address invalid in application data
complained
-> disable non-essential communication
-> flag the tenant for review
unsubscribed
-> update the recipient's preferences
dropped
-> record the policy outcome
-> determine whether customer intervention is required
Sender metrics can also be grouped by sub-account or campaign, making it possible to identify the largest senders, tenants with deteriorating delivery performance, or sub-accounts approaching their limits without first constructing a custom cross-tenant aggregation system.
What your application still owns
Using a managed abuse layer does not absolve a platform of responsibility for its customers. Your application should still own:
- Customer authentication and authorization. A user must not be able to select another customer’s sending credential or domain.
- Consent and business rules. Your product must decide whether there is a legitimate reason to contact a recipient.
- Domain onboarding. Customers must publish the required DKIM and Domain Lockdown records.
- Content generation. Templates, personalization, and AI-generated content remain application concerns.
- Secret management. Sub-account API keys should be encrypted, access-controlled, and rotated when necessary.
- Webhook processing. Delivery events should update your internal records and customer-facing reporting.
- Customer intervention. A suspended or repeatedly abusive customer may require investigation, remediation, or termination.
The important point is that these are primarily product and customer-governance responsibilities. You do not also need to invent the underlying email abuse-detection system.
Takeaways
The most important architectural simplification is not the reduction in the number of boxes on a diagram. It is the reduction in responsibilities your team must continuously operate.
With a lower-level cloud delivery service, your engineers may need to become experts in tenant-resource association, credential policy, event-routing infrastructure, reputation monitoring, suppression hierarchy, IP strategy, abuse detection, automated enforcement, and incident response.
With the MailChannels Email API, the core implementation is substantially smaller:
Provision a sub-account
|
Set its limit and issue its key
|
Authorize its sending domain
|
Send asynchronously
|
Process signed webhooks
Behind that small interface is a continuously operating system that evaluates sender authorization, content, recipient quality, reputation, and behaviour before deciding how each message should be handled.
Email delivery is easy when every sender is trusted. We built the MailChannels Email API for the real world, where your platform must continue delivering good mail even when some of its senders are careless, compromised, or actively abusive. That is what makes the pipeline not merely simpler, but safer.
Where to start
Start with the Email API overview, then read the multi-tenancy guide, the reputation and abuse-control documentation, the Domain Lockdown documentation, and the webhook guide.
Official SDK quickstarts are available for JavaScript, Python, and PHP. Provisioning a sub-account, locking down a test domain, and sending your first asynchronous message takes an afternoon, and it is the same five-step loop you will run in production.