"
}
],
"attachments": [
{
"type": "image/png",
"filename": "logo.png",
"content": "$LOGO_BASE64",
"content_id": "logo"
}
]
}
JSON
```
```javascript Node.js theme={null}
import { readFile } from "node:fs/promises";
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY, FROM_EMAIL, TO_EMAIL, LOGO_PATH } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
if (!FROM_EMAIL) throw new Error("Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)");
if (!TO_EMAIL) throw new Error("Set TO_EMAIL");
if (!LOGO_PATH) throw new Error("Set LOGO_PATH to the image you want to embed, e.g. ./logo.png");
const logoBytes = await readFile(LOGO_PATH);
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
// Setting content_id is what embeds the attachment inline: it's referenced from the HTML
// body via the matching cid: URI, instead of showing up as a downloadable attachment.
const { data, error } = await mailchannels.emails.send({
from: { email: FROM_EMAIL, name: "Your Name" },
to: { email: TO_EMAIL, name: "Recipient" },
subject: "Hello from MailChannels",
html: `
Hello! Here's our logo:
`,
attachments: [
{
type: "image/png",
filename: "logo.png",
content: logoBytes.toString("base64"),
contentId: "logo",
},
],
});
if (error) {
console.error("Send failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import base64
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
from_email = os.environ.get("FROM_EMAIL")
to_email = os.environ.get("TO_EMAIL")
logo_path = os.environ.get("LOGO_PATH")
if not from_email:
sys.exit("Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)")
if not to_email:
sys.exit("Set TO_EMAIL")
if not logo_path:
sys.exit("Set LOGO_PATH to the image you want to embed, e.g. ./logo.png")
with open(logo_path, "rb") as f:
logo_base64 = base64.b64encode(f.read()).decode("ascii")
# Setting content_id is what embeds the attachment inline: it's referenced from the HTML
# body via the matching cid: URI, instead of showing up as a downloadable attachment.
response = mailchannels.Emails.send(
{
"from": {"email": from_email, "name": "Your Name"},
"to": [{"email": to_email, "name": "Recipient"}],
"subject": "Hello from MailChannels",
"html": '
',
'attachments' => [
[
'content' => base64_encode($logo),
'filename' => basename($logo_path),
'type' => mime_content_type($logo_path) ?: 'application/octet-stream',
'content_id' => 'logo',
],
],
]);
print_r($response->toArray());
```
`content_id` must be unique across all attachments in the request.
Never send an attachment that you received from a user without first scanning it for viruses and malware.
# Authentication
Source: https://docs.mailchannels.com/email-api/authentication
Learn how to create and manage API keys for your account and sub-accounts.
## What is an API key?
An API key is a unique token that authenticates your requests to the MailChannels Email API. It serves two purposes: proving
who you are, and determining what you are allowed to do.
## Authenticating a request
Every request to the Email API must include your API key. The examples below send a minimal email and demonstrate how the
key is set.
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <emails->send([
'from' => ['email' => $from_email, 'name' => 'Your Name'],
'to' => ['email' => $to_email, 'name' => 'Recipient'],
'subject' => 'Hello from MailChannels',
'text' => 'Hello! This is a plain-text email.',
]);
print_r($response->toArray());
```
Similar to a password, an API key grants access to your account. Store it securely and never commit it to source control
or expose it in client-side code.
## API key management
### Creating an API key
Visit the [API Keys page](https://dash.mailchannels.com/account/api-keys) in the Console to create a key for
your account.
1. Click **Create API Key**.
2. Add a descriptive label.
3. Set the scope to `Sending Email`.
4. Click **Create API Key** and copy the key. You won't be able to see it again.
The full key is shown exactly once at the moment it is created. The redacted version is available via the Console and API,
but never the original key. There is no way to recover a key.
### Scope
Scope determines what an API key is allowed to do. Scope is assigned when the key is created in the Console and
cannot be changed afterwards. The API currently supports the following scopes: `Sending Email` (api) and `Managing Inbound Filtering` (inbound).
If you create a key with `Sending Email`, you can access every endpoint specified in the
[Email API reference](/email-api/api-reference-introduction).
If you create a key with `Managing Inbound Filtering`, you have access to MailChannels Inbound Filtering.
Inbound filtering blocks unwanted mail before it reaches your infrastructure. If you would like to learn more, view the
[Inbound Filtering documentation](https://www.mailchannels.com/inbound/).
### Deleting an API key
Delete an API key in the Console by clicking the **Revoke** button next to it. In the dialog that appears, click **Revoke API Key**
to confirm. All subsequent requests that use the key will fail.
## Sub-account key management
API keys for sub-accounts can be managed via the API. The parent account can create, list, and delete keys for each sub-account
it owns.
### Creating a key
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY before running}"
: "${SUB_ACCOUNT_HANDLE:?Set SUB_ACCOUNT_HANDLE (the sub-account to create a key for)}"
curl -X POST \
"https://api.mailchannels.net/tx/v1/sub-account/$SUB_ACCOUNT_HANDLE/api-key" \
-H "X-Api-Key: $PARENT_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, SUB_ACCOUNT_HANDLE } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!SUB_ACCOUNT_HANDLE) throw new Error("Set SUB_ACCOUNT_HANDLE (the sub-account to create a key for)");
const mailchannels = new MailChannels(PARENT_API_KEY);
const { data, error } = await mailchannels.subAccounts.createApiKey(SUB_ACCOUNT_HANDLE);
if (error) {
console.error("Create sub-account API key failed:", error);
process.exit(1);
}
// data.key is shown only once. Store it securely.
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
handle = os.environ.get("SUB_ACCOUNT_HANDLE")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not handle:
sys.exit("Set SUB_ACCOUNT_HANDLE (the sub-account to create a key for)")
mailchannels.api_key = parent_api_key
response = mailchannels.SubAccounts.ApiKeys.create(handle)
print(response)
```
```php PHP theme={null}
subAccounts->apiKeys->create($sub_account_handle);
print_r($response->toArray());
```
### Listing keys
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY before running}"
: "${SUB_ACCOUNT_HANDLE:?Set SUB_ACCOUNT_HANDLE (the sub-account whose keys you want to list)}"
curl "https://api.mailchannels.net/tx/v1/sub-account/$SUB_ACCOUNT_HANDLE/api-key" \
-H "X-Api-Key: $PARENT_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, SUB_ACCOUNT_HANDLE } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!SUB_ACCOUNT_HANDLE) throw new Error("Set SUB_ACCOUNT_HANDLE (the sub-account whose keys you want to list)");
const mailchannels = new MailChannels(PARENT_API_KEY);
const { data, error } = await mailchannels.subAccounts.listApiKeys(SUB_ACCOUNT_HANDLE);
if (error) {
console.error("List sub-account API keys failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
handle = os.environ.get("SUB_ACCOUNT_HANDLE")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not handle:
sys.exit("Set SUB_ACCOUNT_HANDLE (the sub-account whose keys you want to list)")
mailchannels.api_key = parent_api_key
response = mailchannels.SubAccounts.ApiKeys.list(handle)
print(response)
```
```php PHP theme={null}
subAccounts->apiKeys->list($sub_account_handle);
print_r($response->toArray());
```
### Deleting a key
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY before running}"
: "${SUB_ACCOUNT_HANDLE:?Set SUB_ACCOUNT_HANDLE}"
: "${API_KEY_ID:?Set API_KEY_ID (the id of the key to delete)}"
curl -X DELETE \
"https://api.mailchannels.net/tx/v1/sub-account/$SUB_ACCOUNT_HANDLE/api-key/$API_KEY_ID" \
-H "X-Api-Key: $PARENT_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, SUB_ACCOUNT_HANDLE, API_KEY_ID } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!SUB_ACCOUNT_HANDLE) throw new Error("Set SUB_ACCOUNT_HANDLE");
if (!API_KEY_ID) throw new Error("Set API_KEY_ID (the ID of the key to delete)");
const mailchannels = new MailChannels(PARENT_API_KEY);
const { success, error } = await mailchannels.subAccounts.deleteApiKey(
SUB_ACCOUNT_HANDLE,
Number(API_KEY_ID),
);
if (!success) {
console.error("Delete sub-account API key failed:", error);
process.exit(1);
}
console.log(`Deleted API key ${API_KEY_ID} for sub-account ${SUB_ACCOUNT_HANDLE}`);
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
handle = os.environ.get("SUB_ACCOUNT_HANDLE")
key_id = os.environ.get("API_KEY_ID")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not handle:
sys.exit("Set SUB_ACCOUNT_HANDLE")
if not key_id:
sys.exit("Set API_KEY_ID (the id of the key to delete)")
mailchannels.api_key = parent_api_key
response = mailchannels.SubAccounts.ApiKeys.delete(handle, key_id)
print(response)
```
```php PHP theme={null}
subAccounts->apiKeys->delete($sub_account_handle, $api_key_id);
print_r($response->toArray());
```
## Key limits
Each account can have 100 API keys. Each sub-account can have 100 API keys. Sub-account keys do not count towards the
parent account's limit.
Attempting to create another key past that limit returns an error. Delete an unused key before creating a new one.
## Best practices
### Storage
Use a secrets manager or environment variable to store your API key, rather than hard-coding it in source code. This
reduces the risk of accidental exposure.
### Rotation
Regularly rotate your API keys to minimize the impact of a potential compromise. To rotate a key, create a new one,
update your applications to use it, and then revoke the old key.
### One key per application
Use a unique API key for each application or service that integrates with the Email API. Then, if a key is compromised,
you can revoke it without affecting other applications.
# MailChannels Email API: pricing, plans, and limits
Source: https://docs.mailchannels.com/email-api/billing/pricing
Understand Email API plans, volume tiers, send counting, usage limits, and billing for governed platform email.
MailChannels Email API uses a straightforward volume-based pricing model for governed platform email for governed platform email. You start on a free developer plan with no credit card charge, and move to a paid tier when your sending volume grows. This page explains how emails are counted, what happens when you reach your limit, and how billing is handled so there are no surprises.
## Free developer plan
The free plan lets you send up to **100 emails per day** at no cost. It is designed for testing, development, and low-volume application emailapplication email. The free plan is fully functional for API sending but does not include features like open and click tracking.
Until you add a payment method, sending is restricted to addresses belonging to verified [users](https://dash.mailchannels.net/account/users) on your account. Add a user, click the link in the verification email, then use that address as the recipient.
The 100-sends-per-day limit **resets at 00:00 UTC** each day, regardless of your time zone.
## Paid plans
Paid plans use a **tier-based pricing model** based on the number of emails you send per month. You choose a plan that matches your expected monthly volume.
* The **entry-level paid plan** covers up to **10,000 emails per month**.
* Higher tiers are available for larger volumes, each with a fixed monthly price.
* Annual billing discounts may apply.
* All plans include the full set of core Email API features.
There is currently no official paid tier between the free plan (\~100 emails/day) and the 10,000 emails/month plan. If you need more than the free tier allows, the next step up is the 10k/month plan.
Visit the [MailChannels pricing page](https://www.mailchannels.com/pricing/#for_devs) to see current volume tiers and costs.
## How emails are counted
MailChannels counts the number of **sends**, not the number of API calls.
If you use the `personalizations` field to send one message to 100 different recipients in a single API call, that counts as **100 sends**,, not 1.
Plan your volume estimates around the total number of recipient deliveries, not the number of requests you make to the API.
## What happens when you exceed your limit
If you reach your daily limit (free plan) or monthly limit (paid plan), MailChannels **pauses further email delivery**. Subsequent API calls to send messages will fail.
MailChannels never silently holds or drops messages that were already successfully submitted via the API. If a send call returned a success response before you hit the limit, that message will be delivered.
When you hit your limit, you have two options:
1. **Wait** for your quota to reset at the start of the next billing cycle.
2. **Upgrade** to a higher volume plan to immediately increase your send limit.
If you upgrade mid-cycle, MailChannels applies a **pro-rated charge** for the increased volume cap for the remainder of the month.
Monitor your [usage](https://dash.mailchannels.net/overview). Upgrading before you hit the limit avoids any interruption to your critical emails.
## Unused quota does not roll over
Unused emails from one billing cycle do not carry over to the next. Each month you receive a fresh allotment based on your plan.
For example: if your plan allows 50,000 emails/month and you send only 30,000 this month, you still have 50,000 available next month,, not 70,000.
## Billing
* Your card is charged at the **start of each billing cycle** (monthly by default, unless you are on annual billing).
* If you upgrade mid-cycle, the additional charge is **pro-rated** for the remaining days in the month.
* Payments are processed by a **PCI-DSS certified payment partner**.. MailChannels does not store your card number directly.
* If a payment fails, MailChannels will notify you to update your payment details to avoid service interruption.
* You can manage billing and view invoices on the [billing page](https://dash.mailchannels.net/billing).
## Rate limits and burst sending
There is no fixed messages-per-second cap on the Email API. You can send at high throughput as long as you stay within your monthly quota and your traffic remains inside normal sender-risk patternsEmail API. You can send at high throughput as long as you stay within your monthly quota and your traffic remains inside normal sender-risk patterns.
However, if your account suddenly sends an unusually large burst, especially on a small or free plan, the anti-abuse system may temporarily rate-limit your sending speed to protect shared reputation and, especially on a small or free plan, the anti-abuse system may temporarily rate-limit your sending speed to protect shared reputation and system health. This is only triggered by abnormal patterns, such as a compromised system attempting to send millions of messages in a short window.
For normal use cases, your monthly volume quota is the primary constraint. If you have a use case requiring very high burst rates, contact MailChannels in advance or spread the send out over time.
## Click tracking URLs
When you enable click tracking for outbound emails, MailChannels automatically replaces HTTP and HTTPS links in your message body with custom tracking URLs. These URLs let you monitor recipient engagement and track click events in real time.
MailChannels uses a stateless encoding system for tracking links. The tracking URL has this structure:
```text theme={null}
https://[BASE_DOMAIN]/[CUSTOMER_HASH]/[SID_HASH]/[ENCODED_TOKEN]?v=2
```
* **BASE\_DOMAIN** -- the MailChannels click-tracking service domain, e.g. `click.mailchannels.net`.
* **CUSTOMER\_HASH** and **SID\_HASH** -- SHA-256 hashes of your customer handle and Sender ID, used for routing without exposing your account details.
* **ENCODED\_TOKEN** -- a URL-safe Base64-encoded package containing the destination URL, the Message ID, and an HMAC-SHA256 digital signature.
* **?v=2** -- tells MailChannels servers to decode the token inline.
Because the destination URL and Message ID are embedded directly in the token and signed with HMAC-SHA256, there is no database lookup on click. R. Redirects are fast and reliable. Tampered URLs are automatically rejected: if even a single character of the destination is changed, the signature check fails and the server returns a `404 Not Found`. The system also supports key rotation via the Key ID field, so MailChannels can update signing keys without breaking links already sent to recipients.
Click tracking is not available on the free developer plan. Upgrade to a paid plan to enable it.
# Handling complained events
Source: https://docs.mailchannels.com/email-api/complained-events
Learn about the best practices for responding to complained webhook events.
## What is a complained event?
A complained event is generated when a recipient files a spam complaint about an email message. This indicates that the
recipient considers the email to be unwanted or unsolicited.
The recipient's email address is automatically added to your suppression list so you won't accidentally send mail to them
again, but you should remove the address from your mailing list as well. For more information, see
[suppressions and unsubscribe handling](/email-api/suppressions-unsubscribes).
## What to do if you receive a complained event
If you receive a complained event, you should remove the recipient's email address from your mailing list to avoid sending
to them again in the future.
If you receive a large number of complaints, you should investigate the cause and take action to reduce your complaint rate.
This could involve improving the quality of your email content, ensuring that you have proper permission to email your
recipients, or making it easier for recipients to unsubscribe from your emails.
If you receive a large number of complaints, your sending may be restricted or your account may be suspended.
It's important to monitor your complaint rate and take action to reduce it if necessary.
## Fields for complained events
* `email`: Sender email address.
* `customer_handle`: Your account ID.
* `timestamp`: Unix timestamp of the event.
* `event`: The type of event, `complained` in this case.
* `recipients`: List of recipient email addresses that filed complaints.
* `campaign_id` *(optional)*: The campaign ID associated with the message, if applicable.
* `request_id` *(optional)*: A unique identifier generated to track the original HTTP request. This will be blank if the
message is sent via SMTP instead of the API.
* `smtp_id`: A unique identifier used to track the message, matching the `Message-Id` header.
## Example complained event
```json theme={null}
{
"email": "sender@example.com",
"customer_handle": "examplecorp",
"timestamp": 1700000000,
"event": "complained",
"recipients": [
"recipient@example.net"
],
"campaign_id": "spring-sale-2026",
"request_id": "4b06083a23724f279c8d11a370e641f8",
"smtp_id": ""
}
```
# DKIM signing and key management
Source: https://docs.mailchannels.com/email-api/configuring-dkim
MailChannels manages DKIM signing and key rotation for you.
## DKIM
You can choose to manage your own keys, or let MailChannels manage your keys for you. If you choose to manage your own keys,
you are responsible for creating the key pair, publishing the DNS record, and rotating keys when necessary. If you choose
to let MailChannels manage your keys, we manage the keys, and all you have to do is update the DNS.
**If you want to manage your own keys:** include `dkim_domain`, `dkim_selector` and `dkim_private_key` in your API request
to send a message.
**If you want MailChannels to manage your keys:** MailChannels generates and stores the private key, and handles the signing
process.
### Create a key pair
Generate a key pair for your domain. `selector` is a string that identifies the key and is used in the DNS record and email headers.
`algorithm` defaults to `rsa` and `key_length` defaults to `2048`. Choose a short, descriptive selector that helps you
identify the key, for example "marketing2026" or "transactional20260101".
```bash cURL theme={null}
#!/usr/bin/env bash
# Create a DKIM key pair managed by MailChannels. The response includes the
# public key and the DNS TXT record to publish.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${DOMAIN:?Set DOMAIN (the sending domain, e.g. example.com)}"
: "${SELECTOR:?Set SELECTOR (e.g. mc1)}"
curl -X POST "https://api.mailchannels.net/tx/v1/domains/$DOMAIN/dkim-keys" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <dkim->create(
domain: $domain,
selector: $selector,
algorithm: 'rsa',
keyLength: 2048,
);
print_r($response->toArray());
```
The response includes the suggested DNS record under `dkim_dns_records`:
```json Response theme={null}
{
"domain": "example.com",
"selector": "mc1",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...",
"status": "active",
"algorithm": "rsa",
"key_length": 2048,
"created_at": "2026-05-13T18:30:00Z",
"dkim_dns_records": [
{
"name": "mc1._domainkey.example.com",
"type": "TXT",
"value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
}
]
}
```
### Publish the DNS record
Add the returned TXT record to your domain's DNS. Receivers fetch this record to verify the DKIM signatures on your mail.
Here is an example record:
DNS Name: `mc1._domainkey.example.com`
TXT Record value: `v=DKIM1; k=rsa; p=public_key_value`
The format of the record is specified in [RFC 6376, section 3.6.1](https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1).
### Send a message using the managed key
Once you have created a key and published the DNS record, your mail will automatically be signed using that key. If you have
multiple keys and want to specify which one to use, include `dkim_selector` in your send request.
See the [API reference](/api-reference/send/send-an-email) for more details.
```bash cURL theme={null}
#!/usr/bin/env bash
# Send a message signed with a MailChannels-managed DKIM key. Optionally set
# dkim_selector; the private key stays with MailChannels.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
: "${DKIM_SELECTOR:?Set DKIM_SELECTOR (the selector of an active key)}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <emails->send([
'from' => ['email' => $from_email],
'to' => ['email' => $to_email],
'subject' => 'Hello from MailChannels',
'text' => 'Signed with a MailChannels-managed DKIM key.',
'dkim_selector' => $dkim_selector,
]);
print_r($response->toArray());
```
### Rotate a key
Rotating a key refers to the process of creating a new key, updating your DNS records, and retiring the old key. This is
a security best practice that limits the potential damage if a private key is compromised. MailChannels provides a tool
to rotate your DKIM keys. When you rotate a key, a new key pair is generated, and the old key is marked as `rotated`.
```bash cURL theme={null}
#!/usr/bin/env bash
# Rotate an active DKIM key.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${DOMAIN:?Set DOMAIN (the sending domain, e.g. example.com)}"
: "${CURRENT_SELECTOR:?Set CURRENT_SELECTOR (the active key to rotate out)}"
: "${NEW_SELECTOR:?Set NEW_SELECTOR (the selector for the new active key)}"
curl -X POST "https://api.mailchannels.net/tx/v1/domains/$DOMAIN/dkim-keys/$CURRENT_SELECTOR/rotate" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <dkim->rotate($domain, $current_selector, $new_selector);
print_r($response->toArray());
```
The response contains both keys, each with its suggested DNS record:
```json Response theme={null}
{
"new_key": {
"domain": "example.com",
"selector": "mc2",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...new...",
"status": "active",
"algorithm": "rsa",
"key_length": 2048,
"created_at": "2026-05-13T18:30:00Z",
"dkim_dns_records": [
{
"name": "mc2._domainkey.example.com",
"type": "TXT",
"value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...new..."
}
]
},
"rotated_key": {
"domain": "example.com",
"selector": "mc1",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...",
"status": "rotated",
"algorithm": "rsa",
"key_length": 2048,
"status_modified_at": "2026-05-13T18:30:00Z",
"gracePeriodExpiresAt": "2026-05-16T18:30:00Z",
"retiresAt": "2026-05-27T18:30:00Z",
"dkim_dns_records": [
{
"name": "mc1._domainkey.example.com",
"type": "TXT",
"value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
}
]
}
}
```
The rotated key remains valid for signing for a **3-day grace period** (`gracePeriodExpiresAt`) and is automatically
marked `retired` 2 weeks after rotation (`retiresAt`). To avoid signature-verification failures during the cutover:
1. Publish the new key's DNS record **before** updating your send requests to use the new selector.
2. Leave the rotated key's DNS record in place until the grace period ends — in-flight mail signed with it still
needs the record for verification.
3. Once the grace period ends, update your send requests to use the new selector. Signing with the old selector
will fail after it transitions to `retired`.
# Content types
Source: https://docs.mailchannels.com/email-api/content-types
Learn about the content types you can use in your email body and how to use them effectively.
## Creating the email body
The `content` array contains the body of your email. Each entry is a `{type, value}` object, where `type` is a
[MIME](https://datatracker.ietf.org/doc/html/rfc2045) content type and `value` is the body for that type.
When you supply more than one entry, MailChannels encodes the message as `multipart/alternative`.
### Plain text and HTML
Every message that includes a `text/html` part should include a `text/plain` part as well, with the plain text part first.
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <Hello from MailChannels."
}
]
}
JSON
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY, FROM_EMAIL, TO_EMAIL } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
if (!FROM_EMAIL) throw new Error("Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)");
if (!TO_EMAIL) throw new Error("Set TO_EMAIL");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.emails.send({
from: { email: FROM_EMAIL, name: "Your Name" },
to: { email: TO_EMAIL, name: "Recipient" },
subject: "Hello from MailChannels",
text: "Hello from MailChannels.",
html: "
Hello from MailChannels.
",
});
if (error) {
console.error("Send failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
from_email = os.environ.get("FROM_EMAIL")
to_email = os.environ.get("TO_EMAIL")
if not from_email:
sys.exit("Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)")
if not to_email:
sys.exit("Set TO_EMAIL")
# Passing both text and html sends a multipart/alternative message; clients
# pick the best part they can render.
response = mailchannels.Emails.send(
{
"from": {"email": from_email, "name": "Your Name"},
"to": [{"email": to_email, "name": "Recipient"}],
"subject": "Hello from MailChannels",
"text": "Hello from MailChannels.",
"html": "
',
]);
print_r($response->toArray());
```
Significant differences between the plain text and HTML versions hurt deliverability. Make sure the plain text version
is the same message as the HTML version, just without formatting. It should contain all the same text, links, and images.
### Supported MIME types
The following types are generally readable in most email clients:
* `text/plain`
* `text/html`
* `text/richtext`
* `text/css`
When multiple parts are present, the rendered body orders them as `text/plain`, then `text/html`, then any other types.
### Prohibited MIME types
The following types are blocked because they are frequently associated with malware delivery. Including them in
`content` returns an error:
* `application/x-msdownload`
* `application/vnd.ms-htmlhelp`
* `application/java-archive`
* `application/x-sh`
* `application/x-shellscript`
Other MIME types may be blocked from time to time when our abuse-prevention systems detect ongoing abuse involving
that type.
# cURL quickstart
Source: https://docs.mailchannels.com/email-api/curl/quickstart
Send an email with the MailChannels Email API using cURL.
Send an email in a few minutes. This guide walks through sending your first request to the MailChannels Email API.
## Prerequisites
Before your first request, complete the following steps.
[Sign up](https://dash.mailchannels.com) if you don't already have an account.
Open [Account > API Keys](https://dash.mailchannels.com/account/api-keys) and click **Create API Key**.
Add a descriptive label and set the scope to `api`. Copy the key; you won't be able to see it again.
Treat your API key like a password. Don't commit it to source control or expose it in client-side code.
For each domain you'll send from, add a `TXT` record on the `_mailchannels` subdomain authorizing your account to send.
Replace `examplecorp` with your MailChannels account ID (visible in the panel footer when you log in):
```
_mailchannels.example.com TXT "v=mc1 auth=examplecorp"
```
See the [Domain Lockdown guide](/email-api/domain-lockdown) for details.
Add MailChannels to the SPF record on each sending domain so recipient servers accept your mail:
```
example.com TXT "v=spf1 include:relay.mailchannels.net ~all"
```
If you already have an SPF record, merge in `include:relay.mailchannels.net` rather than creating a second record.
A domain must have only one SPF record.
Your sending domain must resolve. It needs a valid `A` or `MX` record. Without one, mail from the domain is rejected with
`550 5.1.2 [SDNF] Sender Domain Not Found`. Most registered domains already have these records. Confirm yours with the
[check-domain tool](https://dash.mailchannels.com/domain-health). See [Troubleshooting](/email-api/troubleshooting) for details.
## Install the cURL SDK
```bash cURL theme={null}
# cURL ships with macOS and most Linux distributions. If it's missing,
# install it with your platform's package manager:
# macOS (Homebrew)
brew install curl
# Debian / Ubuntu
sudo apt-get install curl
# Windows (winget) — note: curl ships with Windows 10 (1803+) and Windows 11
winget install cURL.cURL
# Verify the install
curl --version
```
## Send your first email
To send an email, provide the recipients, sender, subject, and message content. The example below sends an HTML email to a
single recipient.
Until you add a payment method, sending is restricted to addresses belonging to verified [Users](https://dash.mailchannels.com/account/users)
**on your account**. Add a user, click the link in the verification email, then use that address as the `to` email below.
```bash cURL theme={null}
#!/usr/bin/env bash
# Check that the required variables are set. This is just a sanity check for
# this example script
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <Hello,
"
}
]
}
JSON
```
Set the following before running:
* `MAILCHANNELS_API_KEY` — the API key generated above.
* `FROM_EMAIL` — an address on the domain you configured with Domain Lockdown and SPF above (e.g. `hello@example.com` if
you used `example.com` in the Domain Lockdown and SPF DNS records).
* `TO_EMAIL` — a recipient inbox you can check, e.g. the address you use to sign up for MailChannels.
## Next steps
* Read the [webhooks guide](/email-api/webhooks) to learn how to get real-time notifications about email delivery,
bounces, and complaints.
* Explore the [API reference](/email-api/api-reference-introduction) to see what else you can do with the API.
# Custom headers
Source: https://docs.mailchannels.com/email-api/custom-headers
Learn how to add custom headers to your emails.
## What are custom headers?
Custom headers let you attach RFC 5322 compliant headers to your messages. This is useful for message classification,
tracking, and per-recipient identifiers.
Some headers are restricted to prevent abuse, and MailChannels may limit how many times the same header can be repeated. See [Restricted headers](#restricted-headers) for a non-exhaustive list.
### Adding custom headers
Include custom headers in the top-level `headers` object to apply them to every recipient of the message:
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <emails->send([
'from' => ['email' => $from_email, 'name' => 'Your Name'],
'to' => ['email' => $to_email, 'name' => 'Recipient'],
'subject' => 'Hello from MailChannels',
'text' => 'Hello from MailChannels.',
'headers' => [
'X-Campaign-Id' => 'welcome-2026',
],
]);
print_r($response->toArray());
```
Using custom headers incorrectly could significantly harm your email deliverability.
### Custom headers within personalizations
Custom headers can also be added to individual personalizations, allowing for recipient-specific headers. This is useful for
personalized unsubscribe links, tracking, or identification headers that vary per recipient.
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL_1:?Set TO_EMAIL_1}"
: "${TO_EMAIL_2:?Set TO_EMAIL_2}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <emails->send([
'from' => ['email' => $from_email, 'name' => 'Your Name'],
'personalizations' => [
[
'to' => [['email' => $to_email_1]],
'headers' => ['X-Campaign-Id' => 'welcome-2026-cohort-a'],
],
[
'to' => [['email' => $to_email_2]],
'headers' => ['X-Campaign-Id' => 'welcome-2026-cohort-b'],
],
],
'subject' => 'Hello from MailChannels',
'text' => 'Hello from MailChannels.',
]);
print_r($response->toArray());
```
When a header is defined both globally and in a personalization object, the value in the personalization object is used.
## Restricted headers
The following names cannot be used as keys in the `headers` JSON object. This list is non-exhaustive.
* Authentication-Results
* BCC
* CC
* Content-Transfer-Encoding
* Content-Type
* DKIM-Signature
* From
* Message-ID
* Received
* Reply-To
* Subject
* To
# Custom tracking
Source: https://docs.mailchannels.com/email-api/custom-tracking
Route click tracking, open tracking, and unsubscribe links through your own custom domain to reinforce your brand and improve email deliverability.
## What is custom tracking?
By default, MailChannels uses shared domains for click-tracking links, open-tracking pixels,
and unsubscribe URLs embedded in your emails. A custom tracking domain replaces those shared
domains with one you own (e.g., `click.example.com`), improving brand consistency and deliverability.
Each custom tracking domain is defined by three user-configurable fields:
* **`hostname`** — the subdomain you control, e.g., `click.example.com`.
* **`scope`** — the event type it handles: `click`, `open`, or `unsubscribe`.
* **`name`** — a unique short label used to select this domain at send time (e.g., `clickdemo`).
Custom tracking domains are not available on the free developer plan. Upgrade to a paid plan to enable it.
## Registering a custom tracking domain
Provide the `hostname`, `scope`, and a unique `name` for the domain.
```bash cURL theme={null}
#!/usr/bin/env bash
# Register a custom click-tracking domain.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
curl -X POST "https://api.mailchannels.net/tx/v1/custom-tracking-domains" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <
If the domain has not been verified yet, the API returns the DNS records you need to add.
The domain is not active at this stage.
```json theme={null}
{
"token": "550e8400-e29b-41d4-a716-446655440000",
"txt_record_name": "_mailchannels-verify.click.example.com",
"txt_record_value": "550e8400-e29b-41d4-a716-446655440000",
"instructions": "Add a DNS TXT record: _mailchannels-verify.click.example.com → \"550e8400-e29b-41d4-a716-446655440000\" and a CNAME record: click.example.com → links.mailchannels.net, then retry this request."
}
```
Add both records to your domain's DNS before retrying:
```plaintext theme={null}
_mailchannels-verify.click.example.com TXT 550e8400-e29b-41d4-a716-446655440000
click.example.com CNAME links.mailchannels.net
```
The TXT record proves you own the hostname. The CNAME record routes tracking traffic to MailChannels infrastructure.
Once both records have propagated, call the registration endpoint again with the same request body.
If verification succeeds, the API returns the domain in the `active` status.
If either record is not yet visible to MailChannels, the response includes an `instructions` field
describing which record is still missing. Wait for DNS propagation and retry.
## Sending with custom tracking
Reference the custom tracking domain by its `name` in `tracking_settings` or `unsubscribe_settings`
when sending a message. The domain must have `active` status.
A full send example:
```bash cURL theme={null}
#!/usr/bin/env bash
# Send an email with the custom click-tracking, open-tracking, and unsubscribe-handling domains.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST "https://api.mailchannels.net/tx/v1/send" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <Hello! Visit our sale page."
}
],
"tracking_settings": {
"click_tracking": {
"enable": true,
"custom_domain_name": "clickdemo"
},
"open_tracking": {
"enable": true,
"custom_domain_name": "opendemo"
},
},
"unsubscribe_settings": {
"custom_domain_name": "unsubscribedemo"
}
}
JSON
```
## Managing custom tracking domains
### Listing domains
Retrieve all registered domains for your account. Optional filters include `name`, `status`, and `scope`.
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve custom tracking domains for the account.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY (parent or sub-account) before running}"
curl -X GET "https://api.mailchannels.net/tx/v1/custom-tracking-domains" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY"
```
### Updating a domain
Rename a domain or toggle its status between `active` and `disabled`.
Reactivating a disabled domain triggers DNS re-verification. Both the TXT ownership record and the
CNAME record must be in place. If either is missing, the response includes an `instructions` field
describing what to add.
```bash cURL theme={null}
#!/usr/bin/env bash
# Update a custom tracking domain.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${TRACKING_DOMAIN_HOSTNAME:?Set TRACKING_DOMAIN_HOSTNAME to the hostname of the custom tracking domain to be updated}"
: "${SCOPE:?Set SCOPE to the scope of the custom tracking domain to be updated}"
curl -X PATCH "https://api.mailchannels.net/tx/v1/custom-tracking-domains/$TRACKING_DOMAIN_HOSTNAME/$SCOPE" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <
### Deleting a domain
Delete a domain by its hostname and scope. The same hostname and scope can be re-registered if needed.
Any tracking links or unsubscribe URLs in previously sent emails that used a deleted domain will
stop working immediately.
```bash cURL theme={null}
#!/usr/bin/env bash
# Delete a custom tracking domain permanently.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${TRACKING_DOMAIN_HOSTNAME:?Set TRACKING_DOMAIN_HOSTNAME to the hostname of the custom tracking domain to be removed}"
: "${SCOPE:?Set SCOPE to the scope of the custom tracking domain to be removed}"
curl -X DELETE "https://api.mailchannels.net/tx/v1/custom-tracking-domains/$TRACKING_DOMAIN_HOSTNAME/$SCOPE" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
```
# Debug a sent message
Source: https://docs.mailchannels.com/email-api/debug-sent-message
Use API responses, webhook events, and application logs to answer what happened to a MailChannels message.
Use this workflow when a recipient reports a missing message, a webhook event is unexpected, or your application needs to reconcile delivery state.
## Capture the identifiers
Store these values when you send a message:
| Identifier | Where it comes from | Why it matters |
| - | - | - |
| `request_id` | API response and webhook payloads | Connects the send request to later delivery events. |
| `smtp_id` | Webhook payloads when present | Helps correlate SMTP and API delivery records. |
| Sender and recipient | Request payload | Narrows the investigation to a domain, mailbox, or user. |
## Follow the event sequence
1. Confirm the API returned a successful response for `send` or `send-async`.
2. Search your stored webhook events by `request_id`.
3. Confirm a `processed` event exists.
4. Look for an event such as `delivered`, `hard-bounced`, `soft-bounced`, `dropped`, or `complained`.
5. If the recipient says the message is missing after a `delivered` event, ask them to check spam, quarantine, forwarding
rules, and mailbox-level filters.
`delivered` means the receiving mail system accepted the message. It does not guarantee inbox placement or that the
recipient saw the message.
## Missing webhook events
If the API returned a successful response for `send` or `send-async`, but your system did not record a webhook event:
* Confirm the webhook endpoint is still enrolled.
* Run webhook validation from the Console or the API.
* Check whether your endpoint returned a non-2xx response, timed out, or rejected the request before storing the raw payload.
* Query webhook batches for recent failed delivery attempts.
* Use [batch resend](/email-api/webhook-retries) after you fix the receiver.
## Recipient says the message was not received
Collect this information before escalating:
* Sender address and recipient address.
* Approximate send time with time zone.
* API `request_id`.
* Last webhook event and timestamp.
* Whether the recipient checked spam, junk, quarantine, forwarding, and mailbox rules.
* Any bounce or complaint event.
Review [errors](/email-api/errors) for more request troubleshooting.
# Domain Lockdown
Source: https://docs.mailchannels.com/email-api/domain-lockdown
Learn how to set up your DNS record for Domain Lockdown.
## What is Domain Lockdown?
Domain Lockdown is a DNS-based security feature that tells MailChannels which
accounts or sender identities may send email from your domain. MailChannels
blocks mail when the authenticated account or sender identity is not listed in
your DNS record.
For each domain you send from, create a `TXT` record at the `_mailchannels`
subdomain:
```text theme={null}
_mailchannels.example.com TXT v=mc1 auth=examplecorp
```
This record allows the MailChannels account `examplecorp` to send mail from
`example.com`.
### Record format
A Domain Lockdown record is a space-separated list of fields:
```text theme={null}
_mailchannels.example.com TXT v=mc1 auth=examplecorp senderid=examplecorp|x-authsender|billing@example.com sidw=examplecorp|x-authsender|billing*@example.com
```
* `v=mc1` identifies the Domain Lockdown record version. Always include this
field.
* `auth=` allows a MailChannels account or sub-account ID to send from the domain.
* `senderid=` allows one exact sender ID.
* `sidw=` allows one sender ID pattern with a limited wildcard `*`.
You must include at least one `auth`, `senderid`, or `sidw` field to specify
the authorized account or sender. You may include more than one `auth`,
`senderid`, or `sidw` field in the same record. A message is allowed if any
field matches.
Domain Lockdown by Cloudflare Worker ID (cfid) is no longer supported as of August 31, 2024.
### Finding your account ID and sender ID
Your MailChannels account ID is shown in the [panel footer](https://dash.mailchannels.com) when you log in.
Sub-account IDs are listed on the [sub-accounts page](https://dash.mailchannels.com/sub-accounts).
To find a sender ID, check the headers of a message sent through MailChannels:
```text theme={null}
X-MailChannels-SenderId: examplecorp|x-authsender|billing@example.com
X-MailChannels-Auth-Id: examplecorp
Content-Type: text/plain; charset="utf-8"
Date: Mon, 22 Aug 2022 14:15:57 -0500
From: billing@example.com
To: customer@example.net
Subject: Your invoice is ready
```
* Use the `X-MailChannels-Auth-Id` value in an `auth=` field. Note that messages
sent by a sub-account will show the sub-account ID here, not the parent.
* Use the `X-MailChannels-SenderId` value in a `senderid=` field, or as a
reference when writing a `sidw=` pattern.
### Choosing a lockdown method
Use `auth` when every sender under a MailChannels account should be allowed to
send from the domain.
```text theme={null}
_mailchannels.example.com TXT v=mc1 auth=examplecorp
```
To authorize a sub-account to also send from the domain:
```text theme={null}
_mailchannels.example.com TXT v=mc1 auth=examplecorp auth=examplecorp_sub
```
This is the simplest setup and works well when a small number of accounts control
all authorized sending for the domain.
Use `senderid` when only specific sender identities should be allowed.
```text theme={null}
_mailchannels.example.com TXT v=mc1 senderid=examplecorp|x-authsender|billing@example.com senderid=examplecorp|x-authsender|alerts@example.com
```
Messages from the same MailChannels account with any other sender ID are blocked
by this record.
Use `sidw` to allow a controlled group of sender IDs without listing every
address individually.
```text theme={null}
_mailchannels.example.com TXT v=mc1 sidw=examplecorp|x-authsender|*.head@example.com
```
This is useful for hosting providers, agencies, or platforms where multiple
customers send through the same MailChannels account and `auth` would be too
broad.
### Sender ID wildcard
The `sidw` field supports one `*` wildcard in the sender part of the sender ID.
The wildcard matches any number of characters.
```text theme={null}
_mailchannels.example.com TXT v=mc1 sidw=examplecorp|x-authsender|billing*@example.com
```
This allows sender IDs such as:
```text theme={null}
examplecorp|x-authsender|billing.no-reply@example.com
examplecorp|x-authsender|billing.alerts@example.com
examplecorp|x-authsender|billing@example.com
```
`sidw` patterns must follow these rules:
* **Format**: exactly three pipe-separated parts:
`||`.
* **Pipe character**: `|` is always a separator. Do not include a literal `|`
in the sender part; use `*` to match across that portion instead.
* **Constraint**: edge cases such as `|mailer-daemon||NOTNDR` are not supported.
* **Wildcard**: one wildcard is permitted only in the sender part. A standalone
wildcard, such as `examplecorp|x-authsender|*`, is prohibited.
Invalid `sidw` patterns are skipped. Use [Testing Your Setup](#testing-your-setup)
to confirm that your record matches the sender IDs you expect.
#### Escaping in `sidw` patterns
In the sender part of a `sidw` pattern, `*` is a wildcard. To match a literal
asterisk, write `\*`. To match a literal backslash, write `\\`.
For example, this record matches
`examplecorp|x-authsender|*depart\billing@example.com` and
`examplecorp|x-authsender|*depart\alerts@example.com`:
```text theme={null}
_mailchannels.example.com TXT v=mc1 sidw=examplecorp|x-authsender|\*depart\\*@example.com
```
Escaping any other character, such as `\a`, makes the `sidw` pattern invalid.
Some DNS providers process backslashes before publishing the TXT record. After
creating the record, verify the value DNS returns:
```bash theme={null}
resolvectl query --type=TXT _mailchannels.example.com
```
MailChannels evaluates the published TXT value, so adjust your input until DNS
returns the intended record.
### Testing your setup
After publishing the DNS record, confirm the expected TXT value is visible:
Use the [check-domain tool](https://dash.mailchannels.com/domain-health), or use the check-domain API
to verify:
```bash cURL theme={null}
#!/usr/bin/env bash
# Check that a domain's DKIM, SPF, and Domain Lockdown records are configured
# correctly and have propagated.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${DOMAIN:?Set DOMAIN (the sending domain, e.g. example.com)}"
curl -X POST "https://api.mailchannels.net/tx/v1/check-domain" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <checkDomain->check($domain);
print_r($response->toArray());
```
#### Troubleshooting invalid `sidw` patterns
Use this table for reference when a `sidw` rule is present in DNS but does
not match the sender ID you expect. Invalid `sidw` patterns are skipped
while MailChannels continues evaluating any other fields in the record.
| Invalid pattern | Why it is skipped |
| - | - |
| sidw=\*\|x-authsender\|sender\*@example.com | Wildcard `*` is allowed only in the sender part. |
| sidw=examplecorp\|x-authsender\|\* | The wildcard cannot be the entire sender part. |
| sidw=examplecorp\|x-authsender\|\*sender\*@example.com | More than one wildcard is present. |
| sidw=examplecorp\|x-authsender\|sender\|\*@example.com | The extra \| creates a fourth part. `sidw` must have exactly three pipe-separated parts. |
| sidw=examplecorp\|x-authsender\|se\nder\*@example.com | `\n` is not a valid escape sequence. In the sender part, only `\*` and `\\` are valid escapes. |
### Best practices
* Start with the narrowest rule that matches your sending model: `auth` for one
trusted account, `senderid` or `sidw` for shared sending environments.
* Keep records simple and prefer a few clear fields over broad wildcard patterns.
* Review Domain Lockdown records when you add new sending systems, customer
domains, or sender ID formats.
* Remove old `auth`, `senderid`, and `sidw` fields when they are no longer
needed.
# Email API vs SMTP
Source: https://docs.mailchannels.com/email-api/email-api-vs-smtp
Learn the differences between the Email API and SMTP, and when to choose each.
## Two ways to send
MailChannels gives you two ways to hand off your mail: the HTTP Email API and SMTP. Both deliver through the same
infrastructure and reach the same inboxes. The difference is how your application connects.
The Email API is an HTTPS interface: your application sends a single request with a JSON body describing the message. SMTP
is the long-established mail protocol: you point an existing mail server or SMTP client at MailChannels and it relays your
mail.
## How they compare
| | Email API | SMTP |
| - | - | - |
| How you integrate | Send an HTTPS request with a JSON body | Point an existing mail server or SMTP client at MailChannels |
| Connection | HTTPS request to the Email API at `api.mailchannels.net` | Relay through `smtp.mailchannels.net` using SMTP |
| Authentication | API key sent in the `X-Api-Key` header | Account ID and SMTP password using SMTP AUTH |
| DKIM signing | Managed DKIM signing available — MailChannels signs on your behalf once configured | MailChannels does not sign; your own DKIM signatures are preserved |
| Delivery events | Delivery, bounce, and engagement webhooks | abuse notifications webhooks |
Both methods require the same DNS setup on every sending domain: a [Domain Lockdown](/email-api/domain-lockdown) record
and an [SPF](/email-api/spf-dkim-dmarc) record.
## When to choose the Email API
Choose the Email API if you are:
* Building a new application, or sending from a serverless or edge environment where holding an SMTP connection is awkward.
* Relying on delivery, bounce, and engagement [webhooks](/email-api/webhooks) to track what happens to your mail.
Start with the [Email API quickstart](/email-api/curl/quickstart).
## When to choose SMTP
Choose SMTP if you:
* Already run a mail server, or use software that sends over SMTP, and want to move to MailChannels with little or no code
change.
See [Sending via SMTP](/email-api/smtp-sending) to get started.
# Errors
Source: https://docs.mailchannels.com/email-api/errors
Learn about common Email API errors, what they mean, and how to handle them.
The handling pattern depends on the language you use:
* **curl** — inspect the HTTP status returned by `curl -w "%{http_code}"` and parse the response body with `jq`.
* **JavaScript SDK** — SDK methods return `{ data, error }` (or `{ success, error }`). The `error` object has `message`
and `statusCode` fields; branch on `error?.statusCode`.
* **Python SDK** — `mailchannels.Emails.send` (and other resource methods) raise typed exceptions that subclass
`mailchannels.exceptions.MailChannelsError`. Catch the specific subclass to react to each status.
* **PHP SDK** — SDK methods throw typed exceptions that subclass `MailChannels\Exception\MailChannelsException`.
Catch the specific subclass to react to each status; use `getStatusCode()` and `getMessage()` on the caught exception.
The examples below show how each endpoint's errors might be handled together in production code: surface transient
5xx failures so the caller (or a retry layer) can react, and fail fast on 4xx so the underlying request can be
corrected.
## Send API errors
| Status | Meaning | What to do |
| - | - | - |
| `400 Bad Request` | The JSON body is malformed, a required field is missing, an email address is invalid, or non-transactional sending rules are not met. | Compare the payload with the [Email Structure](/email-api/how-emails-are-structured) guide and the API reference. |
| `403 Forbidden` | The API key is missing, invalid, scoped incorrectly, not attached to the expected account, or the sending domain is not authorized. | Confirm the `X-Api-Key` header, check the key scope, and verify [Domain Lockdown](/email-api/domain-lockdown). |
| `413 Payload Too Large` | The encoded message, headers, and attachments exceed 30 MB. | Reduce attachment size or count. See [Attachments](/email-api/attachments). |
| `500 Internal Server Error` | A transient MailChannels service error. | Retry with exponential backoff. Check [status.mailchannels.net](https://status.mailchannels.net/) if the error persists. |
| `502 Bad Gateway` | A transient infrastructure or network error. | Retry with backoff and alert if repeated attempts fail. |
```bash cURL theme={null}
status=$(curl -s -o /tmp/mc-resp.json -w "%{http_code}" \
-X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @payload.json)
case "$status" in
2*) ;;
400|403|413)
echo "Send rejected ($status):"
jq -r '.errors // .message // empty' /tmp/mc-resp.json
exit 1
;;
500|502)
echo "Send failed transiently ($status). Retry later."
exit 1
;;
*)
echo "Unexpected status $status"
exit 1
;;
esac
```
```javascript Node.js theme={null}
const { data, error } = await mailchannels.emails.send(payload);
if (error) {
switch (error.statusCode) {
case 400: // Invalid payload — compare with the Email Structure guide.
case 403: // Bad API key, wrong scope, or missing Domain Lockdown TXT record.
case 413: // Message + attachments exceed 30 MB.
console.error(`Send rejected (${error.statusCode}):`, error.message);
throw error;
case 500: // Transient MailChannels failure — surface for a retry layer.
case 502: // Transient infrastructure / network failure — surface for a retry layer.
console.error(`Send failed transiently (${error.statusCode}):`, error.message);
throw error;
default:
throw error;
}
}
```
```python Python theme={null}
import logging
import mailchannels
from mailchannels.exceptions import (
ForbiddenError,
InvalidRequestError,
PayloadTooLargeError,
ServerError,
)
log = logging.getLogger(__name__)
try:
response = mailchannels.Emails.send(payload)
except (InvalidRequestError, ForbiddenError, PayloadTooLargeError) as e:
# 400 / 403 / 413 — caller error. Fix the request before retrying.
log.error("Send rejected (%s): %s", e.status_code, e.message)
raise
except ServerError as e:
# 500 / 502 — transient. Surface so a retry layer can react.
log.error(
"Send failed transiently (%s, request_id=%s): %s",
e.status_code,
e.request_id,
e.message,
)
raise
```
```php PHP theme={null}
emails->send($payload);
} catch (InvalidRequestException | ForbiddenException | PayloadTooLargeException $e) {
// 400 / 403 / 413 — caller error. Fix the request before retrying.
error_log(sprintf('Send rejected (%d): %s', $e->getStatusCode(), $e->getMessage()));
throw $e;
} catch (ServerException $e) {
// 500 / 502 — transient. Surface so a retry layer can react.
error_log(sprintf(
'Send failed transiently (%d, request_id=%s): %s',
$e->getStatusCode(),
$e->getRequestId(),
$e->getMessage(),
));
throw $e;
}
```
## Suppression API errors
| Status | Meaning | What to do |
| - | - | - |
| `400 Bad Request` | A recipient, source, date filter, or request body is invalid. | Validate recipient formatting and use a supported source: `api`, `unsubscribe_link`, `list_unsubscribe`, `hard_bounce`, or `spam_complaint`. |
| `409 Conflict` | One or more suppression entries already exist. | Treat existing entries as success during idempotent imports, or query the suppression list before creating entries. |
| `413 Payload Too Large` | The create request exceeds the 1,000-entry limit. | Split imports into batches of 1,000 entries or fewer. |
| `500 Internal Server Error` | A transient MailChannels service error. | Retry with exponential backoff. Check [status.mailchannels.net](https://status.mailchannels.net/) if the error persists. |
| `503 Temporarily Unavailable` | Suppression service maintenance or temporary unavailability. | Retry later with backoff. |
```bash cURL theme={null}
# Assumes entries.json is already chunked to <= 1,000 entries per request.
status=$(curl -s -o /tmp/mc-resp.json -w "%{http_code}" \
-X POST https://api.mailchannels.net/tx/v1/suppressions \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @entries.json)
case "$status" in
2*) ;;
409) ;; # Entries already exist — idempotent success.
400|413)
echo "Suppression import rejected ($status):"
jq -r '.message // empty' /tmp/mc-resp.json
exit 1
;;
500|503)
echo "Suppression import failed transiently ($status). Retry later."
exit 1
;;
*)
echo "Unexpected status $status"
exit 1
;;
esac
```
```javascript Node.js theme={null}
const BATCH_SIZE = 1000;
function chunked(items, size) {
const out = [];
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
return out;
}
for (const batch of chunked(entries, BATCH_SIZE)) {
const { success, error } = await mailchannels.suppressions.create({ entries: batch });
if (success) continue;
switch (error.statusCode) {
case 409: // Entries already exist — idempotent, treat as success.
continue;
case 400: // Invalid recipient, source, or filter.
case 413: // Batch exceeds 1,000 entries.
console.error(`Suppression import rejected (${error.statusCode}):`, error.message);
throw error;
case 500: // Transient failure — surface for a retry layer.
case 503: // Suppression service maintenance — surface for a retry layer.
console.error(`Suppression import failed transiently (${error.statusCode}):`, error.message);
throw error;
default:
throw error;
}
}
```
```python Python theme={null}
import logging
import mailchannels
from mailchannels.exceptions import (
ConflictError,
InvalidRequestError,
PayloadTooLargeError,
ServerError,
)
log = logging.getLogger(__name__)
BATCH_SIZE = 1000
def chunked(items, size):
for i in range(0, len(items), size):
yield items[i : i + size]
for batch in chunked(entries, BATCH_SIZE):
try:
mailchannels.Suppressions.create(entries=batch)
except ConflictError:
# 409 — one or more entries already exist. Idempotent: treat as success.
continue
except (InvalidRequestError, PayloadTooLargeError) as e:
# 400 / 413 — caller error. Fix the batch before retrying.
log.error("Suppression import rejected (%s): %s", e.status_code, e.message)
raise
except ServerError as e:
# 500 / 503 — transient. Surface so a retry layer can react.
log.error(
"Suppression import failed transiently (%s, request_id=%s): %s",
e.status_code,
e.request_id,
e.message,
)
raise
```
```php PHP theme={null}
suppressions->create($batch);
} catch (ConflictException) {
// 409 — one or more entries already exist. Idempotent: treat as success.
continue;
} catch (InvalidRequestException | PayloadTooLargeException $e) {
// 400 / 413 — caller error. Fix the batch before retrying.
error_log(sprintf('Suppression import rejected (%d): %s', $e->getStatusCode(), $e->getMessage()));
throw $e;
} catch (ServerException $e) {
// 500 / 503 — transient. Surface so a retry layer can react.
error_log(sprintf(
'Suppression import failed transiently (%d, request_id=%s): %s',
$e->getStatusCode(),
$e->getRequestId(),
$e->getMessage(),
));
throw $e;
}
}
```
## Webhook API errors
| Status | Meaning | What to do |
| - | - | - |
| `400 Bad Request` | A webhook URL, batch ID, filter, or validation request is invalid. | Check the URL, filter values, and batch ID before retrying. |
| `404 Not Found` | The requested webhook batch or signing key does not exist. Applies to `POST /webhook-batch/{batch_id}/resend`, `GET /webhook/public-key`, and `POST /webhook/validate`. | Confirm the resource ID exists before retrying. |
| `409 Conflict` | A webhook endpoint is already enrolled for this customer (`POST /webhook` only). | Treat as success for idempotent enrollment, or query existing webhooks with `GET /webhook` before enrolling. |
| `500 Internal Server Error` | A transient MailChannels service error. | Retry and check [status.mailchannels.net](https://status.mailchannels.net/) if failures continue. |
```bash cURL theme={null}
status=$(curl -s -o /tmp/mc-resp.json -w "%{http_code}" \
-X POST "https://api.mailchannels.net/tx/v1/webhook?endpoint=$WEBHOOK_URL" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY")
case "$status" in
2*) ;;
409) ;; # Endpoint already enrolled — idempotent success.
400|404)
echo "Webhook request rejected ($status):"
jq -r '.message // empty' /tmp/mc-resp.json
exit 1
;;
500)
echo "Webhook request failed transiently. Retry later."
exit 1
;;
*)
echo "Unexpected status $status"
exit 1
;;
esac
```
```javascript Node.js theme={null}
const { success, error } = await mailchannels.webhooks.enroll(webhookUrl);
if (error) {
switch (error.statusCode) {
case 409: // Endpoint already enrolled — idempotent, treat as success.
break;
case 400: // Invalid URL, filter, or batch ID.
case 404: // Missing batch / signing key (resendBatch, publicKey, validate).
console.error(`Webhook request rejected (${error.statusCode}):`, error.message);
throw error;
case 500: // Transient failure — surface for a retry layer.
console.error("Webhook request failed transiently:", error.message);
throw error;
default:
throw error;
}
}
```
```python Python theme={null}
import logging
import mailchannels
from mailchannels.exceptions import ConflictError, InvalidRequestError, ServerError
log = logging.getLogger(__name__)
try:
mailchannels.Webhooks.create(endpoint=webhook_url)
except ConflictError:
# 409 — endpoint already enrolled. Idempotent: treat as success.
pass
except InvalidRequestError as e:
# 400 — invalid URL, filter, or batch ID.
# 404 — missing batch or signing key (Webhooks.resend_batch /
# public_key / validate). Both surface as InvalidRequestError;
# inspect e.status_code to distinguish.
log.error("Webhook request rejected (%s): %s", e.status_code, e.message)
raise
except ServerError as e:
# 5xx — transient. Surface so a retry layer can react.
log.error(
"Webhook request failed transiently (%s, request_id=%s): %s",
e.status_code,
e.request_id,
e.message,
)
raise
```
```php PHP theme={null}
webhooks->create($webhook_url);
} catch (ConflictException) {
// 409 — endpoint already enrolled. Idempotent: treat as success.
} catch (InvalidRequestException | NotFoundException $e) {
// 400 — invalid URL, filter, or batch ID.
// 404 — missing batch or signing key (resendBatch, validate).
// Both surface as different exceptions; inspect getStatusCode() to distinguish.
error_log(sprintf('Webhook request rejected (%d): %s', $e->getStatusCode(), $e->getMessage()));
throw $e;
} catch (ServerException $e) {
// 5xx — transient. Surface so a retry layer can react.
error_log(sprintf(
'Webhook request failed transiently (%d, request_id=%s): %s',
$e->getStatusCode(),
$e->getRequestId(),
$e->getMessage(),
));
throw $e;
}
```
# Handling hard-bounced events
Source: https://docs.mailchannels.com/email-api/hard-bounced-events
Learn about the best practices for responding to hard-bounced webhook events.
## What is a hard-bounced event?
A hard-bounced event is generated when a recipient's server permanently rejects an email (returning a 5xx SMTP status code). You should not attempt to resend it.
The event data includes the specific reason for the bounce, indicating whether the cause was a problem with your request (e.g., an invalid recipient address) or with the recipient's server (e.g., mailbox unavailable).
In certain cases, such as an invalid recipient address, the rejected recipient is automatically added to your suppression list.
For more information, see [suppressions and unsubscribe handling](/email-api/suppressions-unsubscribes).
## What to do if you receive a hard-bounced event
If you receive a hard-bounced event, you should investigate the reason for the bounce and take appropriate action based on the `status` code and `reason` fields.
For example, if the bounce is due to an invalid recipient address, you should remove that address from your mailing list to avoid sending to it again in the future.
Other causes of hard-bounced events include issues with the recipient's server, rejections due to failed authentication
(DKIM, SPF), or the server flagging your mail as spam.
For more in-depth information on SMTP response codes and their meanings, see our [SMTP response code documentation](https://blog.mailchannels.com/understanding-smtp-response-codes/).
## Fields for hard-bounced events
* `email`: Sender email address.
* `customer_handle`: Your account ID.
* `timestamp`: Unix timestamp of the event.
* `event`: The type of event, `hard-bounced` in this case.
* `recipients`: List of recipient email addresses that couldn't receive the message.
* `status`: The SMTP status code received for the bounce.
* `reason`: A human-readable explanation of why the message bounced.
* `campaign_id` *(optional)*: The campaign ID associated with the message, if applicable.
* `request_id` *(optional)*: A unique identifier generated to track the original HTTP request. This will be blank if the message is sent via SMTP instead of the API.
* `smtp_id`: A unique identifier used to track the message, matching the `Message-Id` header.
## Example hard-bounced event
```json theme={null}
{
"email": "sender@example.com",
"customer_handle": "examplecorp",
"timestamp": 1700000000,
"event": "hard-bounced",
"recipients": [
"recipient@example.net",
"recipient2@example.net"
],
"status": "521",
"reason": "Server does not accept mail",
"campaign_id": "spring-sale-2026",
"request_id": "4b06083a23724f279c8d11a370e641f8",
"smtp_id": ""
}
```
# How emails are structured
Source: https://docs.mailchannels.com/email-api/how-emails-are-structured
Learn how to structure your email payload for sending, including personalizations and content types.
## The request payload
The structure of the request JSON payload represents the email you want to send. Among other things, it defines the sender,
recipients, subject, and body of an email. The `personalizations` field allows you to specify unique content for each
recipient of an email. You can create highly targeted and individualized email campaigns within a single API call.
### Basic structure
The `personalizations` array applies per-recipient overrides on top of the root-level fields. This structure allows for
sending to multiple recipients, customizing subject lines per recipient, and applying different headers or DKIM settings
per email.
The example below sends a fruit sale to three recipients. The first two entries override the sender, subject, and cc/bcc
recipients; the third entry only overrides the subject and falls back to the root-level sender:
```json JSON theme={null}
{
"personalizations": [
{
"to": [
{ "name": "Banana Lover", "email": "banana-lover123@example.com" }
],
"cc": [
{ "email": "banana-fans@example.com" }
],
"from": {
"name": "Banana Department",
"email": "bananas@fruitstand.example.com"
},
"subject": "🍌 BANANAS ARE ON SALE 🍌"
},
{
"to": [
{ "name": "Apple Lover", "email": "apple-lover123@example.com" }
],
"cc": [
{ "email": "apple-fans@example.com" }
],
"bcc": [
{ "email": "archive@fruitstand.example.com" }
],
"from": {
"name": "Apple Department",
"email": "apples@fruitstand.example.com"
},
"subject": "🍎 APPLES ARE ON SALE 🍎"
},
{
"to": [
{ "name": "苹果爱好者", "email": "pingguo-fan@example.com" }
]
}
],
"from": {
"name": "Fruit Stand",
"email": "sales@fruitstand.example.com"
},
"subject": "Big sale this week",
"content": [
{
"type": "text/plain",
"value": "Don't miss our fruit sale this week!"
},
{
"type": "text/html",
"value": "
Don't miss our fruit sale this week!
"
}
]
}
```
When a property is defined both globally and in a personalization object, the value in the personalization object is used.
### Field types
Detailed definitions of each field can be found in the [API reference](/api-reference/send/send-an-email#body-personalizations).
The `to` field is the only required property within each personalization object.
* `personalizations`: Array of objects (recipient-specific data)
* `from`: Object (sender information)
* `subject`: String
* `content`: Array of objects (email body parts)
Each `content` object contains:
* `type`: String (e.g., "text/plain", "text/html")
* `value`: String (actual content)
Each `EmailAddress` object (sender or recipient information) contains:
* `name`: String, optional. Display name as raw, unencoded text (e.g., `John Doe`, `张三`).
* `email`: String (e.g., `foo@example.com`).
Always provide the `name` field as raw, unencoded text to prevent double-encoding issues.
Special characters and non-ASCII text will be automatically handled for email compatibility.
# Inbound email and replies
Source: https://docs.mailchannels.com/email-api/inbound-email-replies
Understand how to handle replies and inbound email when sending outbound messages with MailChannels Email API.
MailChannels Email API is focused on outbound email delivery. It sends messages, reports delivery events through webhooks, and helps protect your sending domains. It does not provide an inbound message content API for receiving, parsing, or storing replies.
## Supported reply patterns
Use one of these patterns when recipients need to reply:
| Pattern | Use it when |
| - | - |
| Real mailbox | A person or team should read and answer replies in an inbox. |
| Shared support address | Replies should create or update support tickets through your help desk. |
| Dedicated inbound provider | Your application needs to parse inbound message content, attachments, or reply threads. |
| No-reply sender plus `reply_to` | The visible sender should be product-owned, but replies should route elsewhere. |
## Configure `reply_to`
Set `reply_to` in your send request when replies should go to a different mailbox than the `from` address.
```json theme={null}
{
"from": {
"email": "notifications@example.com",
"name": "Example App"
},
"reply_to": {
"email": "support@example.com",
"name": "Example Support"
}
}
```
## What webhooks do not include
MailChannels webhooks report delivery events such as processing, delivery, bounces, drops, complaints, and unsubscribe activity. They are not inbound message webhooks and do not include reply content.
If your product needs reply parsing, store the outbound `request_id` and message metadata in your application, then connect replies through your mailbox or inbound provider.
# Nuxt community integration
Source: https://docs.mailchannels.com/email-api/integrations/nuxt
Find the community-maintained Nuxt module for the MailChannels Email API and the official Node.js SDK guides.
Use [nuxt-mailchannels in the Nuxt Modules directory](https://nuxt.com/modules/mailchannels) to integrate the MailChannels Email API into a Nuxt application. The module is maintained by [Yizack Rangel](https://github.com/Yizack/nuxt-mailchannels) and uses the official MailChannels Node.js SDK.
## Before you start
Complete the account, API-key, and sending-domain setup in the [Node.js quickstart](/email-api/javascript/quickstart#prerequisites).
The module requires Nuxt 3 or later and a running Nuxt server. Its server utilities are not available in a purely static deployment produced by `nuxt generate`. Keep your API key in server-side runtime configuration.
## Install and configure the module
Follow the module maintainer's [installation and configuration guide](https://github.com/Yizack/nuxt-mailchannels#quick-setup) for the current setup instructions, server utilities, and examples. The package is published as [nuxt-mailchannels on npm](https://www.npmjs.com/package/nuxt-mailchannels).
This is a community-maintained module. Report module-specific problems in its [issue tracker](https://github.com/Yizack/nuxt-mailchannels/issues).
## Use the SDK directly
If you prefer to call the official SDK from your own server routes, use the [Nuxt guide in the Node.js SDK repository](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-js/src/main/docs/guides/frameworks/nuxt.md). See the [Node.js quickstart](/email-api/javascript/quickstart) for the SDK's account setup and first-send example.
# Send governed emails automatically with MailChannels + Zapier
Source: https://docs.mailchannels.com/email-api/integrations/zapier
Connect MailChannels to Zapier so trusted app events can trigger governed, domain-authorized email sends.
The MailChannels Zapier integration lets you send domain-authorized email through the MailChannels Email API whenever a trusted event fires in another app, such as a form submission, new CRM contact, order update, or support workflow. You build the workflow once in Zapier's visual editor, and MailChannels handles governed delivery every time the trigger fires.
The MailChannels app on Zapier is currently in **Beta**. It works reliably, but you may see the Beta label when searching for it in the Zapier app directory.
## What you need before you start
* A **MailChannels account** - sign up at [mailchannels.com](https://www.mailchannels.com/pricing/#for_devs).
* A **Zapier account** - free or paid, available at [zapier.com](https://zapier.com).
* A **domain you own** - MailChannels sends email on behalf of your domain and requires a one-time DNS setup described below.
## Setting up the integration
Zapier uses your API key to authenticate with MailChannels on your behalf.
1. Log in to the [MailChannels Console](https://console.mailchannels.net).
2. Go to **Account Settings**.
3. Under **API Keys**, click **Create API Key**.
4. Give it a description (e.g., `Zapier`) and set the **Scope** to `api`.
5. Copy the key and store it somewhere safe. You will not be able to view it again.
While you are in the console, note your **Account ID** shown in the upper-right corner. You may need it during domain setup.
MailChannels uses **Domain Lockdown** to authorize the domain your emails will come from. You configure this once in your domain's DNS settings so your Zapier workflow cannot send from an unmanaged domain.
Add a TXT record at your domain registrar with the following values:
| Field | Value |
| - | - |
| **Host** | `_mailchannels.` |
| **Value** | `v=mc1; auid=` |
DNS changes typically take a few minutes to a few hours to propagate.
Your domain must also have a valid **SPF record** that includes MailChannels, and at least one **A** or **MX** record. Without these, MailChannels may not deliver mail for that domain.
Set up **DKIM** for your domain in the MailChannels Console under **Settings > DKIM**. DKIM signing helps protect sender reputation and reduces the chance your emails land in spam.
1. In Zapier, click **+ Create** > **Zaps**.
2. Give your Zap a name, such as `Send Welcome Email via MailChannels`.
3. Navigate to the **Action** step and search for **MailChannels**.
4. Select **MailChannels** from the results.
5. For **Action Event**, select **Send Email**.
6. In the **Account** section, click **Sign in**.
7. Enter your MailChannels API key when prompted, then confirm.
MailChannels is now connected to your Zapier account. This connection is reusable, so you only need to authenticate once.
After authenticating, click **Continue** to reach the email configuration screen. Fill in the following fields:
| Field | Description |
| - | - |
| **From (Email Address)** | The address you are sending from. It must use the domain you authorized in step 2, e.g. `hello@acme.com` |
| **From (Display Name)** | *(Optional)* The name recipients see, e.g. `Acme Support` |
| **To (Email Address)** | The recipient's email. Click the field to insert a dynamic value from your trigger |
| **To (Display Name)** | *(Optional)* The recipient's name, also available as a dynamic field |
| **Subject** | Your subject line. This supports dynamic values |
| **Body (HTML)** | The email body. This supports HTML and dynamic values for personalization |
In any field, click inside it to browse and insert data from your trigger step, such as the submitter's name, email address, or order number. This is what makes each email feel personally addressed without extra work.
**Example HTML body:**
```html theme={null}
Hi {{first_name}},
Thanks for signing up! We're excited to have you on board.
- The Acme Team
```
Once all fields are filled in, click **Continue**, then **Test Step**. Zapier sends a real test email using your sample trigger data. Check the inbox to confirm it arrived.
The trigger is the event in another app that starts the automation. Go to the **Trigger** step at the top of your Zap and configure it.
1. Search for your app and select it.
2. Choose the event that should start the email, e.g. `New Submission`.
3. Connect your account and follow the prompts.
4. Click **Test Trigger** to pull in sample data. This data will be available to personalize your email.
Common trigger apps and events:
* **Form submissions** - Typeform, Google Forms, Gravity Forms
* **New contacts** - HubSpot, Salesforce, Pipedrive
* **New orders** - Shopify, WooCommerce
* **Spreadsheet rows** - Google Sheets, Airtable
* **Scheduled time** - Schedule by Zapier (great for recurring emails)
Once the test is successful:
1. Click **Publish** to turn the Zap on.
2. Your automation is now live. Every time the trigger fires, MailChannels sends the email automatically.
## Common use cases
**Welcome email on form signup** - Trigger: new Typeform or Google Forms submission. Action: MailChannels sends a personalized welcome email to the submitter.
**Order confirmation** - Trigger: new order in Shopify or WooCommerce. Action: MailChannels sends a confirmation with the customer's order details.
**CRM follow-up** - Trigger: new contact added in HubSpot or Pipedrive. Action: MailChannels sends a personalized intro email from your sales team.
**Scheduled digest** - Trigger: Schedule by Zapier (e.g., every Monday at 9 am). Action: MailChannels sends a weekly summary to your team or list.
**Internal team alert** - Trigger: new row added to a Google Sheet. Action: MailChannels emails your team with the new row's details.
## Troubleshooting
**I can't find MailChannels in Zapier's app search.**
The integration is currently in Beta. Search for `MailChannels` directly, or use the direct link: [zapier.com/apps/mailchannels/integrations](https://zapier.com/apps/mailchannels/integrations).
**My test email wasn't delivered.**
* Make sure the From address uses the same domain you authorized in step 2.
* Confirm your Domain Lockdown TXT record has propagated.
* Verify your domain has a valid SPF record and at least one A or MX record.
**Email went to spam.**
* Set up DKIM in the MailChannels Console under **Settings > DKIM**.
* Confirm your SPF record is valid.
* Consider adding a DMARC record. See the [MailChannels DMARC guide](https://docs.mailchannels.net/email-api/advanced/dmarc).
**Zapier shows an authentication error.**
Your API key may have been deleted or rotated. Generate a new one in the MailChannels Console, then reconnect in Zapier under **My Apps > MailChannels > Reconnect**.
# Node.js quickstart
Source: https://docs.mailchannels.com/email-api/javascript/quickstart
Send an email with the MailChannels Email API using Node.js.
Send an email in a few minutes. This guide walks through sending your first request to the MailChannels Email API.
The Node.js SDK is open source. Browse the [source code](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-js/).
## Prerequisites
Before your first request, complete the following steps.
[Sign up](https://dash.mailchannels.com) if you don't already have an account.
Open [Account > API Keys](https://dash.mailchannels.com/account/api-keys) and click **Create API Key**.
Add a descriptive label and set the scope to `api`. Copy the key; you won't be able to see it again.
Treat your API key like a password. Don't commit it to source control or expose it in client-side code.
For each domain you'll send from, add a `TXT` record on the `_mailchannels` subdomain authorizing your account to send.
Replace `examplecorp` with your MailChannels account ID (visible in the panel footer when you log in):
```
_mailchannels.example.com TXT "v=mc1 auth=examplecorp"
```
See the [Domain Lockdown guide](/email-api/domain-lockdown) for details.
Add MailChannels to the SPF record on each sending domain so recipient servers accept your mail:
```
example.com TXT "v=spf1 include:relay.mailchannels.net ~all"
```
If you already have an SPF record, merge in `include:relay.mailchannels.net` rather than creating a second record.
A domain must have only one SPF record.
Your sending domain must resolve. It needs a valid `A` or `MX` record. Without one, mail from the domain is rejected with
`550 5.1.2 [SDNF] Sender Domain Not Found`. Most registered domains already have these records. Confirm yours with the
[check-domain tool](https://dash.mailchannels.com/domain-health). See [Troubleshooting](/email-api/troubleshooting) for details.
## Install the Node.js SDK
```javascript Node.js theme={null}
# npm
npm i mailchannels-sdk
# yarn
yarn add mailchannels-sdk
# pnpm
pnpm add mailchannels-sdk
```
## Send your first email
To send an email, provide the recipients, sender, subject, and message content. The example below sends an HTML email to a
single recipient.
Until you add a payment method, sending is restricted to addresses belonging to verified [Users](https://dash.mailchannels.com/account/users)
**on your account**. Add a user, click the link in the verification email, then use that address as the `to` email below.
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY, FROM_EMAIL, TO_EMAIL } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
if (!FROM_EMAIL) throw new Error("Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)");
if (!TO_EMAIL) throw new Error("Set TO_EMAIL");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.emails.send({
from: { email: FROM_EMAIL, name: "Your Name" },
to: { email: TO_EMAIL, name: "Recipient" },
subject: "Hello from MailChannels",
text: "Hello! This is a plain-text fallback for clients that don't render HTML.",
html: '
',
});
if (error) {
console.error("Send failed:", error);
process.exit(1);
}
console.log(data);
```
Set the following before running:
* `MAILCHANNELS_API_KEY` — the API key generated above.
* `FROM_EMAIL` — an address on the domain you configured with Domain Lockdown and SPF above (e.g. `hello@example.com` if
you used `example.com` in the Domain Lockdown and SPF DNS records).
* `TO_EMAIL` — a recipient inbox you can check, e.g. the address you use to sign up for MailChannels.
## Next steps
* Read the [webhooks guide](/email-api/webhooks) to learn how to get real-time notifications about email delivery,
bounces, and complaints.
* Explore the [API reference](/email-api/api-reference-introduction) to see what else you can do with the API.
# Laravel
Source: https://docs.mailchannels.com/email-api/laravel
Send email through Laravel Mail using the MailChannels driver.
The `mailchannels/mailchannels-php` package includes a Laravel service provider that registers `mailchannels` as a
[Mail](https://laravel.com/docs/13.x/mail) driver.
Laravel's `Mail` facade is built on Symfony Mailer under the hood,
and uses the exact same transport as the [Symfony integration](/email-api/symfony).
This plugin is optional and lives alongside the rest of the PHP SDK. The plugin itself can only send mail. It
doesn't implement the full API surface (sub-accounts, keys, webhooks, etc.). For those features, use the SDK's
`Client` directly.
## Prerequisites
Before you send your first message, follow the [PHP quickstart](/email-api/php/quickstart#prerequisites) prerequisite section.
This will walk you through creating an account, generating an API key, and adding the required Domain
Lockdown and SPF DNS records.
## Installation
```bash theme={null}
composer require mailchannels/mailchannels-php
```
The SDK also needs a PSR-18 HTTP client and PSR-17 factories. Symfony HTTP Client + Nyholm PSR-7 is a good fit here,
since Laravel Mail is built on Symfony Mailer (Guzzle works too, but install one or the other, not both):
```bash theme={null}
composer require symfony/http-client nyholm/psr7
```
### Configuring Laravel
Add a `mailchannels` mailer in `config/mail.php`:
```php config/mail.php theme={null}
return [
// ...
'mailers' => [
// ...
'mailchannels' => [
'transport' => 'mailchannels',
],
],
];
```
Add it to `config/services.php`:
```php config/services.php theme={null}
return [
// ...
'mailchannels' => [
'api_key' => env('MAILCHANNELS_API_KEY'),
'base_url' => env('MAILCHANNELS_API_URL'), // optional; omit for the default
],
];
```
Set in your environment or `.env` file:
```bash .env theme={null}
MAIL_MAILER=mailchannels
MAILCHANNELS_API_KEY=your-api-key
```
## Sending mail
Once configured, send mail through the standard `Mail` facade. No MailChannels-specific code is required:
```php theme={null}
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
Mail::to('recipient@example.com')->send(new WelcomeEmail());
```
See Laravel's docs on [generating mailables](https://laravel.com/docs/13.x/mail#generating-mailables)
for everything else `Email` supports (multiple recipients, reply-to, attachments and so on).
Anything built with a Laravel [Mailable](https://laravel.com/docs/13.x/mail#generating-mailables) (`from`,
`to`/`cc`/`bcc`, `subject`, `text`/`html` bodies, attachments) can be sent through this transport.
Only one personalization is ever built per message. If you need to send multiple individualized emails in one request,
call the SDK's `Client` directly instead of going through `Mail`. See
[How emails are structured](/email-api/how-emails-are-structured) for more information on personalizations.
### Control headers
A Laravel `Mailable` has no equivalent for some MailChannels-specific features. Reach those instead through custom
headers prefixed with `x-mailchannels-`. The `x-mailchannels-*` headers are stripped before the message is sent, so
recipients never see them.
Set these (and any other custom headers) from a `Mailable`'s `headers()`
method:
```php theme={null}
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Headers;
class WelcomeEmail extends Mailable
{
public function headers(): Headers
{
return new Headers(
text: [
'X-Misc-Header' => 'welcome-2026',
'x-mailchannels-transactional' => 'true',
],
);
}
}
```
If the same custom header is added twice, only the **last** value is used.
Boolean headers accept only the literal strings `"true"` / `"false"`; anything else throws
`Symfony\Component\Mailer\Exception\TransportException`. An empty value is treated as unset (falls back to the default).
| Header | Description |
| - | - |
| `x-mailchannels-send-async` | Queue the message (`true`, the default) instead of sending it synchronously (`false`). Equivalent to calling the SDK's `queue()` vs. `send()`. Queued sends report delivery status via [webhooks](/email-api/webhooks) rather than in the response. |
| `x-mailchannels-click-tracking-enable` | Enable click tracking. |
| `x-mailchannels-click-tracking-domain` | Custom domain for click-tracking links. See [Custom tracking](/email-api/custom-tracking). |
| `x-mailchannels-open-tracking-enable` | Enable open tracking. |
| `x-mailchannels-open-tracking-domain` | Custom domain for open-tracking links. See [Custom tracking](/email-api/custom-tracking). |
| `x-mailchannels-unsubscribe-domain` | Custom domain for unsubscribe links. See [Custom tracking](/email-api/custom-tracking). |
| `x-mailchannels-transactional` | Whether the mail is transactional or non-transactional (marketing, etc.). Affects List-Unsubscribe handling, see [Unsubscribe](/email-api/unsubscribe). |
| `x-mailchannels-dkim-domain` | Domain to use for DKIM signing. |
| `x-mailchannels-dkim-selector` | Selector to use for DKIM signing. |
| `x-mailchannels-dkim-private-key` | Private key to use for DKIM signing. |
| `x-mailchannels-campaign-id` | Campaign that this message is part of. |
Each of these corresponds directly to a field on the [send endpoint](/api-reference/send/send-an-email). See that
reference for full semantics.
This transport only supports DKIM via the `x-mailchannels-dkim-*` headers above.
If a `DKIM-Signature` header is already present on the message, the message is rejected, since the provided signature
won't match the message the API constructs.
See [DKIM signing and key management](/email-api/configuring-dkim)
for information on how MailChannels handles signing and keys.
Laravel's own mail queueing (`ShouldQueue` mailables, `Mail::queue()`) is unrelated to the `x-mailchannels-send-async`
control header:
The former controls *when your application* hands the message to the transport;
the latter controls whether *MailChannels* processes it synchronously or asynchronously once it arrives.
You can use them independently or together.
## Error handling
Any SDK exception raised while sending (authentication, validation, rate limiting, server errors) is caught and
re-thrown as `Symfony\Component\Mailer\Exception\TransportException`.
Access the original exception via `$e->getPrevious()` to inspect it:
```php theme={null}
use Symfony\Component\Mailer\Exception\TransportException;
try {
Mail::to($recipient)->send(new WelcomeEmail());
} catch (TransportException $e) {
$previous = $e->getPrevious();
if ($previous instanceof \MailChannels\Exception\RateLimitException) {
// handle rate limit
} elseif ($previous instanceof \MailChannels\Exception\ValidationException) {
// handle validation error
} else {
// handle other transport errors
}
}
```
## Limitations
* **One personalization per message.** No per-recipient template data or per-recipient header overrides through this
transport.
* **No Mustache templating.** This transport has no way to use mailchannels' mustache template rendering. If you need
to do so, use Laravel's [Blade Template Support](https://laravel.com/docs/13.x/blade), or use the SDK's `Client`
directly; see [Templates](/email-api/templates) for how.
* **DKIM only via the MailChannels API**, as described above.
## Next steps
* Read the [webhooks guide](/email-api/webhooks) to learn how to get real-time notifications about email
delivery, bounces, and complaints.
* Explore the [API reference](/email-api/api-reference-introduction) to see what else you can do with the API.
# Local development
Source: https://docs.mailchannels.com/email-api/local-development
Set up a safe local workflow for testing MailChannels Email API sends, webhooks, and secrets.
Use a local workflow that exercises the real API without exposing secrets or surprising real recipients.
## Environment variables
Store credentials outside source control:
```bash theme={null}
MAILCHANNELS_API_KEY=xxx
```
Add `.env` to `.gitignore` and commit a `.env.example` with placeholder values only.
## Test sending
Start with one recipient you control. Use a subject prefix such as `[dev]` or `[staging]` so test mail is easy to identify.
Before sending a new template, use the `dry-run` option to render the message without sending it.
## Local webhook testing
During development:
1. Run your webhook receiver locally.
2. Expose it through a temporary HTTPS tunnel.
3. Register the tunnel URL as your webhook endpoint.
4. Run webhook validation.
5. Store the raw test event and verify signature validation before adding business logic.
## Avoid common mistakes
* Do not put API keys in browser JavaScript.
* Do not commit `.env` files, webhook signing keys, or private DKIM keys.
* Do not test with large recipient lists.
* Do not use production customer addresses for template experiments.
* Do not assume a successful API response means inbox placement. Use webhooks and mailbox checks.
# Metrics
Source: https://docs.mailchannels.com/email-api/metrics
Learn how to query engagement, delivery, and sending volume metrics.
## Introduction
The Metrics API provides analytics and visibility into your email sending performance. By leveraging these endpoints, you
can monitor performance indicators, track user engagement, analyze delivery outcomes, and gain valuable insights into
recipient behavior patterns.
MailChannels groups metrics into five categories:
* **[Engagement](/email-api/metrics-engagement)**: opens and clicks.
* **[Performance](/email-api/metrics-performance)**: processed, delivered, and bounced counts.
* **[Volume](/email-api/metrics-volume)**: processed, delivered, and dropped counts.
* **[Recipient Behavior](/email-api/metrics-recipient-behavior)**: unsubscribe activity.
* **[Sender Breakdown](/email-api/metrics-senders)**: aggregate delivery performance grouped by campaign or sub-account.
## Common parameters
All metric queries accept the same time filter:
* `start_time` and `end_time`: ISO 8601 timestamps that restrict the query to a time range. If omitted, `start_time` defaults
to one month ago and `end_time` defaults to the current time.
Engagement, performance, volume, and recipient behavior additionally accept:
* `campaign_id`: restrict results to a single campaign.
* `interval`: controls bucket granularity for the time series. Supported values: `hour`, `day` (default), `week`, `month`.
Sender breakdown is an aggregate, not a time series, and accepts a different set of parameters. See [sender breakdown](/email-api/metrics-senders)
for details.
Engagement metrics require open and click tracking to be enabled, which is not available in the free tier.
# Engagement metrics
Source: https://docs.mailchannels.com/email-api/metrics-engagement
Learn how to track email opens and link clicks across your campaigns.
## What engagement metrics measure
Engagement metrics report how recipients interact with your mail. They cover two tracked actions:
* `open`: the number of times the email was opened.
* `click`: the number of clicks on tracked links in HTML messages.
Each event count is paired with the number of recipients who could have performed the action, so you can compute open and click rates:
* `open_tracking_delivered`: the number of recipients of delivered HTML messages where open tracking was enabled in the
send request.
* `click_tracking_delivered`: the number of recipients of delivered HTML messages containing tracked links, where click
tracking was enabled in the send request.
Messages sent without the corresponding tracking enabled do not contribute to either count.
Use these metrics to measure campaign reach, identify content that resonates, and compare engagement across
send times or audience segments.
## Example query
The example below requests daily engagement totals for a single campaign in January 2026.
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve daily open and click counts for a single campaign.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
curl -G "https://api.mailchannels.net/tx/v1/metrics/engagement" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
--data-urlencode "start_time=2026-01-01T00:00:00Z" \
--data-urlencode "end_time=2026-01-31T23:59:59Z" \
--data-urlencode "campaign_id=newsletter_jan_2026" \
--data-urlencode "interval=day"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.metrics.engagement({
startTime: "2026-01-01T00:00:00Z",
endTime: "2026-01-31T23:59:59Z",
});
if (error) {
console.error("Get engagement metrics failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
# Retrieve daily open and click counts for a single campaign.
response = mailchannels.Metrics.engagement(
start_time="2026-01-01T00:00:00Z",
end_time="2026-01-31T23:59:59Z",
campaign_id="newsletter_jan_2026",
interval="day",
)
print(response)
```
```php PHP theme={null}
metrics->engagement(
startTime: '2026-01-01T00:00:00Z',
endTime: '2026-01-31T23:59:59Z',
campaignId: 'newsletter_jan_2026',
interval: 'day',
);
print_r($response->toArray());
```
Open and click tracking is not available in our free tier.
# Performance metrics
Source: https://docs.mailchannels.com/email-api/metrics-performance
Learn how to monitor delivery success and bounce rates.
## What performance metrics measure
Performance metrics focus on the outcome of each attempt to deliver a message. They report three counts:
* `processed`: messages accepted by MailChannels for delivery.
* `delivered`: messages successfully handed off to the recipient's mail server.
* `bounced`: messages the recipient's mail server permanently rejected (hard bounces).
Use these metrics to track delivery success rates, surface bounce patterns, and watch for changes that may indicate
deliverability or reputation issues.
## Example query
The example below requests delivery counts from a chosen start time onward, with the default `interval` (`day`).
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve processed, delivered, and bounced counts from a chosen start time.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
curl -G "https://api.mailchannels.net/tx/v1/metrics/performance" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
--data-urlencode "start_time=2026-01-01T00:00:00Z"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.metrics.performance({
startTime: "2026-01-01T00:00:00Z",
});
if (error) {
console.error("Get performance metrics failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
# Retrieve processed, delivered, and bounced counts from a chosen start time.
response = mailchannels.Metrics.performance(
start_time="2026-01-01T00:00:00Z",
)
print(response)
```
```php PHP theme={null}
metrics->performance(
startTime: '2026-01-01T00:00:00Z',
);
print_r($response->toArray());
```
For a similar view that reports messages dropped before delivery instead of bounces, see [volume metrics](/email-api/metrics-volume).
# Recipient behavior metrics
Source: https://docs.mailchannels.com/email-api/metrics-recipient-behavior
Learn how to track unsubscribe activity across your MailChannels Email API campaigns.
## What recipient behavior metrics measure
Recipient behavior metrics report actions that signal recipient sentiment toward your mail. They currently cover
unsubscribe activity:
* `unsubscribed`: recipients who unsubscribed.
* `unsubscribe_delivered`: recipients of delivered messages that include an unsubscribe link or unsubscribe header. Because the unsubscribe feature requires exactly one recipient per message, this also equals the total number of delivered messages with unsubscribe enabled.
You can use the paired values to compute an unsubscribe rate. A rising rate can indicate content fatigue, frequency
issues, or an audience mismatch.
## Example query
The example below requests weekly unsubscribe activity for a single campaign.
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve weekly unsubscribe activity for a single campaign.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
curl -G "https://api.mailchannels.net/tx/v1/metrics/recipient-behaviour" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
--data-urlencode "campaign_id=weekly_newsletter" \
--data-urlencode "interval=week"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.metrics.recipientBehaviour({
startTime: "2026-01-01T00:00:00Z",
endTime: "2026-01-31T23:59:59Z",
});
if (error) {
console.error("Get recipient behaviour metrics failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
# Retrieve weekly unsubscribe activity for a single campaign.
response = mailchannels.Metrics.recipient_behaviour(
campaign_id="weekly_newsletter",
interval="week",
)
print(response)
```
```php PHP theme={null}
metrics->recipientBehaviour(
campaignId: 'weekly_newsletter',
interval: 'week',
);
print_r($response->toArray());
```
# Sender breakdown metrics
Source: https://docs.mailchannels.com/email-api/metrics-senders
Learn how to compare delivery performance across your largest campaigns and sub-accounts.
## What sender breakdown metrics measure
Sender breakdown metrics return aggregate delivery counts grouped by sender. Each entry includes the sender `name`
along with `processed`, `delivered`, `dropped`, and `bounced` totals over the queried window.
Choose how to group results with the `sender_type` path parameter:
* `campaigns`: group results by campaign.
* `sub-accounts`: group results by sub-account.
Use these metrics to identify your largest senders, flag campaigns or sub-accounts with deliverability issues, and
spot sub-accounts that are approaching their sending limits.
Senders with no processed messages in the queried window are excluded from the response.
## Parameters
Unlike the other metric categories, sender breakdown returns an aggregate (not a time series) and does not accept
`campaign_id` or `interval`. The supported parameters are:
* `start_time` and `end_time`: ISO 8601 timestamps that restrict the query to a time range. If omitted, `start_time` defaults
to one month ago and `end_time` defaults to the current time.
* `limit`: maximum number of senders to return per page (default `10`, max `1000`).
* `offset`: number of senders to skip from the start of the result set (default `0`).
* `sort_order`: `asc` or `desc`. Sorting is applied to the total number of messages (`processed` + `dropped`). Defaults to `desc`.
## Example query
The example below requests the top 50 campaigns by total messages (`processed` + `dropped`) for January 2026, sorted
in descending order.
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve the top 50 campaigns by total messages (processed + dropped) for January 2026.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
curl -G "https://api.mailchannels.net/tx/v1/metrics/senders/campaigns" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
--data-urlencode "start_time=2026-01-01T00:00:00Z" \
--data-urlencode "end_time=2026-01-31T23:59:59Z" \
--data-urlencode "limit=50" \
--data-urlencode "offset=0" \
--data-urlencode "sort_order=desc"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.metrics.senders("campaigns", {
startTime: "2026-01-01T00:00:00Z",
endTime: "2026-01-31T23:59:59Z",
});
if (error) {
console.error("Get senders metrics failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
# Retrieve the top 50 campaigns by total messages (processed + dropped) for January 2026.
response = mailchannels.Metrics.senders(
"campaigns",
start_time="2026-01-01T00:00:00Z",
end_time="2026-01-31T23:59:59Z",
limit=50,
offset=0,
sort_order="desc",
)
print(response)
```
```php PHP theme={null}
metrics->senders(
senderType: 'campaigns',
startTime: '2026-01-01T00:00:00Z',
endTime: '2026-01-31T23:59:59Z',
limit: 50,
offset: 0,
sortOrder: 'desc',
);
print_r($response->toArray());
```
# Volume metrics
Source: https://docs.mailchannels.com/email-api/metrics-volume
Learn how to track overall sending volume and dropped messages for your MailChannels Email API account.
## What volume metrics measure
Volume metrics give you a high-level view of throughput. They report three counts:
* `processed`: messages accepted by MailChannels for delivery.
* `delivered`: messages successfully handed off to the recipient's mail server.
* `dropped`: messages that MailChannels could not process for delivery.
Use these metrics to track sending volume over time, detect spikes or drop-offs, and surface configuration issues
that are causing messages to be dropped before they reach the recipient.
## Example query
The example below requests daily processing and delivery counts for the first week of January 2026.
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve processed, delivered, and dropped counts for the first week of January 2026.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
curl -G "https://api.mailchannels.net/tx/v1/metrics/volume" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
--data-urlencode "start_time=2026-01-01T00:00:00Z" \
--data-urlencode "end_time=2026-01-07T23:59:59Z"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.metrics.volume({
startTime: "2026-01-01T00:00:00Z",
endTime: "2026-01-07T23:59:59Z",
});
if (error) {
console.error("Get volume metrics failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
# Retrieve processed, delivered, and dropped counts for the first week of January 2026.
response = mailchannels.Metrics.volume(
start_time="2026-01-01T00:00:00Z",
end_time="2026-01-07T23:59:59Z",
)
print(response)
```
```php PHP theme={null}
metrics->volume(
startTime: '2026-01-01T00:00:00Z',
endTime: '2026-01-07T23:59:59Z',
);
print_r($response->toArray());
```
For a similar view that reports messages bounced by the recipient's mail server instead, see
[performance metrics](/email-api/metrics-performance).
# Migrate from Amazon SES to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-amazon-ses
Learn how to migrate an Amazon SES sending integration to MailChannels sender-risk infrastructure.
## Core difference
Amazon SES is an AWS-native set of regional email primitives. It gives you credentials, identities, configuration sets, event destinations, templates, account suppressions, and optional receipt rules, but sender governance remains something you design around those primitives. MailChannels is a transactional email delivery API for platforms with many downstream senders. It adds API credentials, JSON payloads, DKIM, Domain Lockdown, HTTPS webhooks, explicit suppressions, and controls that help detect risky senders, contain abuse, and keep good mail flowing.
## Feature mapping
| Amazon SES concept | MailChannels concept | Migration approach |
| - | - | - |
| `SendEmail` or `SendRawEmail` | `/send` payload | Replace AWS SDK calls with MailChannels API calls. |
| Email identities | `from` identities | Preserve sender addresses and align them with MailChannels domain authentication. |
| Domain identities | DKIM and Domain Lockdown | Recreate domain authentication and publish MailChannels DNS records. |
| Configuration sets | Runtime settings or account boundaries | Translate event, tracking, and routing intent into application settings or sub-account choices. |
| Event destinations | MailChannels webhooks | Replace SNS, EventBridge, Firehose, or CloudWatch flows with HTTPS webhook consumers where appropriate. |
| Email templates | Application-managed Mustache content | Export templates and move rendering ownership outside SES. |
| Account suppression list | MailChannels suppression list | Recreate suppressions with explicit transactional and non-transactional policy. |
| Receipt rule sets | No MailChannels Email API equivalent | Replace inbound receiving and routing with another architecture. |
## Send payload notes
The migration is usually a code-level integration rewrite because SES is often called through an AWS SDK rather than a provider-neutral HTTP client.
| Concern | Amazon SES behavior | MailChannels behavior |
| - | - | - |
| Authentication | AWS credential chain and IAM policy | MailChannels API key authentication |
| Regionality | SES resources are regional | MailChannels account or sub-account configuration is not an SES region |
| Send API | AWS SDK operations or raw MIME | JSON `/send` or `/send-async` payloads |
| Events | AWS-native destinations | MailChannels HTTPS webhooks |
| Inbound | SES receipt rules | External inbound service |
## Template migration
SES templates should be exported and moved into application-owned storage. If the existing application calls SES with template names and template data, replace those calls with application-side template selection and MailChannels `dynamic_template_data`.
## Suppression policy
Recommended defaults:
| Amazon SES source | MailChannels target |
| - | - |
| Account-level bounces and complaints | Suppress as both `transactional` and `non-transactional`. |
| Region-specific suppression exports | Merge carefully and deduplicate before importing or recreating in MailChannels. |
| Business preference unsubscribes stored outside SES | Preserve in the source preference system and map to non-transactional suppression when appropriate. |
## Event migration
SES event destinations frequently feed AWS-native systems. A MailChannels migration changes both transport and event schema. Plan this as a downstream integration rewrite, not just a webhook URL swap.
## Inbound migration
SES receipt rules do not map to MailChannels Email API resources. Replace inbound flows with a dedicated receiving provider, an application-managed mailbox workflow, or another AWS-native receiving path that remains separate from MailChannels sending.
## Cutover checklist
* Inventory every SES region that sends or receives mail.
* Replace AWS SDK send calls with MailChannels API calls.
* Recreate DKIM and domain authentication through MailChannels.
* Move SES templates into application-owned storage.
* Replace event destinations with MailChannels webhook consumers where needed.
* Rebuild inbound receipt-rule workflows outside MailChannels.
* Merge regional suppression lists intentionally.
# Migrate from Brevo to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-brevo
Learn how to migrate a Brevo sending integration to MailChannels sender-risk infrastructure.
## Core difference
Brevo combines transactional sending, SMTP templates, senders, domains, webhooks, contact lists, campaigns, and blacklist concepts. MailChannels is built for platforms where many downstream senders share infrastructure and reputation. The migration should move send calls to MailChannels, but it should also map sender identity, domain authorization, sub-account boundaries, suppressions, and containment paths so one sender's behavior does not become everyone's delivery problem.
## Feature mapping
| Brevo concept | MailChannels concept | Migration approach |
| - | - | - |
| Transactional email API | `/send` payload | Rewrite sends to MailChannels JSON payloads and authentication. |
| SMTP templates | Application-managed Mustache content | Export templates and move rendering ownership outside Brevo. |
| Senders | `from` identities | Preserve sender addresses and align them with MailChannels-authenticated domains. |
| Sending domains | DKIM and Domain Lockdown | Recreate domain authentication in MailChannels and publish DNS. |
| Transactional webhooks | MailChannels webhooks | Recreate delivery-event consumers for the MailChannels schema. |
| Marketing webhooks | External marketing workflow plus MailChannels webhooks | Keep marketing preferences outside MailChannels and use MailChannels events only for delivery behavior. |
| Blocked contacts | Suppression list | Map transactional blocks to transactional and non-transactional suppressions. |
| Blacklisted contacts | Suppression list | Map marketing blacklists to non-transactional suppressions by default. |
| Contact lists and email campaigns | External audience and campaign workflow | Replace with an external list manager or campaign system. |
## Send payload notes
Brevo's transactional API and MailChannels both support API-based delivery, but their payloads and template assumptions differ.
| Concern | Brevo behavior | MailChannels behavior |
| - | - | - |
| Authentication | Brevo API key | MailChannels API key authentication |
| Template reference | SMTP template ID and params | Application-selected content plus `dynamic_template_data` |
| Sender controls | Sender and domain resources | Authenticated domain and `from` address in payload |
| Suppression categories | Blocked transactional contacts and marketing blacklist | `transactional` and `non-transactional` suppression types |
| Campaigns | Brevo contact lists and campaigns | External workflow using MailChannels as delivery API |
## Template migration
Export Brevo SMTP templates and identify every application path that references a Brevo template ID. Replace those IDs with application-owned template names, files, or records. Convert Brevo-specific variables to the data shape your MailChannels send payload will provide.
## Suppression policy
Recommended defaults:
| Brevo source | MailChannels target |
| - | - |
| Blocked transactional contacts | Suppress as both `transactional` and `non-transactional`. |
| Marketing blacklists | Suppress as `non-transactional` unless policy requires a total block. |
| Campaign unsubscribes | Preserve as marketing preference data and non-transactional suppression. |
## Marketing migration
MailChannels Email API does not replace Brevo contact lists, segmentation, campaigns, or campaign analytics. Keep audience management and campaign scheduling in a separate system, and use MailChannels as the governed delivery layer for permission-based messages that should move through shared-sender controls.
## Cutover checklist
* Replace Brevo send endpoint and authentication.
* Move SMTP templates into application-owned storage.
* Convert template params into `dynamic_template_data`.
* Create and publish MailChannels DKIM records.
* Recreate transactional webhooks for MailChannels event payloads.
* Export or rebuild contact lists and campaign workflows externally.
* Split blocked and blacklisted contacts into explicit suppression policy.
# Migrate from Mailgun to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-mailgun
Learn how to migrate a Mailgun sending integration to MailChannels sender-risk infrastructure.
## Core difference
Mailgun is strongly domain-centric. Sending, tracking, templates, webhooks, suppressions, and routes are often configured per domain. MailChannels is centered on governing many downstream senders through account and sub-account credentials, JSON send payloads, DKIM authentication, Domain Lockdown, per-message tracking settings, webhooks, and account or sub-account suppression policy.
## Feature mapping
| Mailgun concept | MailChannels concept | Migration approach |
| - | - | - |
| Domain-scoped send API | `/send` payload | Replace Mailgun form-data calls with MailChannels JSON payloads. |
| Sending domains | DKIM and Domain Lockdown | Recreate authentication with MailChannels DKIM records and DNS publication. |
| Account and domain templates | Application-managed Mustache content | Export templates and move storage or versioning outside MailChannels. |
| Per-domain tracking | Per-message `tracking_settings` | Convert domain defaults into application send-time decisions. |
| Account and domain webhooks | MailChannels webhooks | Recreate webhook endpoint enrollment and update event consumers. |
| Domain suppressions | MailChannels suppression list | Collapse domain-scoped suppression behavior into account or sub-account policy. |
| Subaccounts | MailChannels sub-accounts | Map subaccounts directly when tenant boundaries should remain separate. |
| Inbound routes | No MailChannels Email API equivalent | Replace inbound forwarding or webhook routing with another receiving service. |
## Send payload notes
Mailgun send calls commonly use multipart form fields. MailChannels expects JSON. Build a translation layer around the application-level message model rather than copying request fields directly.
| Concern | Mailgun behavior | MailChannels behavior |
| - | - | - |
| Request shape | Form-data fields and domain-specific endpoint | JSON payload sent to `/send` or `/send-async` |
| Domain selection | Often implicit in endpoint domain | Explicit sender domain and DKIM configuration |
| Recipient variables | Mailgun recipient variables | `dynamic_template_data` inside MailChannels personalizations |
| Tracking | Domain-level defaults | Per-message `tracking_settings` |
| Webhooks | Account or domain scoped | MailChannels webhook endpoint with different event schema |
## Template migration
Mailgun templates can exist at account or domain scope. MailChannels does not provide the same stored-template control plane, so templates should move into application code or an external template store. If a Mailgun template has domain-specific variants, preserve those variants explicitly in the new template naming or configuration model.
## Suppression policy
Recommended defaults:
| Mailgun source | MailChannels target |
| - | - |
| Complaints and permanent bounces | Suppress as both `transactional` and `non-transactional`. |
| Unsubscribes | Suppress as `non-transactional` unless policy requires blocking all mail. |
| Domain-specific suppressions | Decide whether to preserve isolation with MailChannels sub-accounts or centralize suppression at the account level. |
## Inbound migration
Mailgun inbound routes do not map to MailChannels Email API resources. Inventory routes early and replace them with a dedicated inbound mail service, application mailbox processing, or another webhook-based receiving architecture.
## Cutover checklist
* Replace Mailgun form-data send calls with MailChannels JSON requests.
* Move domain-specific tracking defaults into application send settings.
* Export and relocate templates.
* Create and publish MailChannels DKIM records.
* Recreate webhook consumers for MailChannels events.
* Decide whether Mailgun subaccounts or domains need MailChannels sub-account isolation.
* Replace inbound routes before changing production MX or routing behavior.
# Migrate from Mailjet to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-mailjet
Learn how to migrate a Mailjet sending integration to MailChannels sender-risk infrastructure.
## Core difference
Mailjet combines transactional sending with sender identity management, stored templates, contact lists, and campaign workflows. MailChannels focuses on governed delivery for platforms with many senders: authenticated API delivery, runtime personalization, webhooks, DKIM, Domain Lockdown, suppression handling, and sender boundaries. Audience storage and campaign orchestration need a separate replacement.
## Feature mapping
| Mailjet concept | MailChannels concept | Migration approach |
| - | - | - |
| Send API v3.1 | `/send` payload | Rewrite transactional sends to MailChannels JSON payloads and authentication. |
| Messages and variables | `personalizations` and `dynamic_template_data` | Preserve recipient-specific merge values, but validate the new payload shape. |
| Stored templates | Application-managed Mustache content | Export templates and manage them outside MailChannels. |
| Sender identities | `from` identities | Keep valid sender addresses while shifting durable trust to domain authentication. |
| Sender domains | DKIM and Domain Lockdown | Recreate domain authentication with MailChannels DNS records. |
| Event callback URLs | MailChannels webhooks | Recreate callbacks and update consumers for MailChannels event schemas. |
| Campaign-excluded contacts | Suppression list | Treat as non-transactional suppressions by default. |
| Contact lists and campaigns | External audience and campaign workflow | Replace with an external list manager or campaign system. |
## Send payload notes
Mailjet's v3.1 API supports nested messages and variables. In MailChannels, model each recipient through `personalizations` and place merge values in `dynamic_template_data`.
| Concern | Mailjet behavior | MailChannels behavior |
| - | - | - |
| Authentication | Mailjet API key and secret | MailChannels API key authentication |
| Sender setup | Sender identity and sender domain controls | Authenticated sender domain with DKIM and Domain Lockdown |
| Variables | Mailjet variables | Mustache data in `dynamic_template_data` |
| Tracking and callbacks | Event callback URLs and account settings | MailChannels webhook plus per-message tracking settings |
| Campaigns | Mailjet campaign product | External workflow with MailChannels as delivery API |
## Template migration
Treat Mailjet templates as content assets to export, review, and re-home. Confirm whether each template depends on Mailjet-specific syntax or metadata and convert it to the rendering approach used by the new application.
## Suppression policy
Recommended defaults:
| Mailjet source | MailChannels target |
| - | - |
| Campaign exclusions | Suppress as `non-transactional`. |
| Hard bounces or complaints from delivery history | Suppress as both `transactional` and `non-transactional` when available. |
| List-level unsubscribe intent | Preserve as non-transactional preference data in the external audience system. |
## Marketing migration
MailChannels Email API does not replace Mailjet contact lists, newsletters, or campaign scheduling. Export list membership and campaign logic into an external audience workflow, then send permission-based messages through MailChannels when they need governed delivery and sender-risk controls.
## Cutover checklist
* Replace Mailjet authentication and send endpoint.
* Translate message variables into `dynamic_template_data`.
* Move templates out of Mailjet.
* Publish and validate MailChannels DKIM records.
* Recreate event callbacks as MailChannels webhooks.
* Move contact-list and campaign workflows outside MailChannels.
* Apply a non-transactional-first policy for campaign exclusions.
# Migrate from Postmark to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-postmark
Learn how to migrate a Postmark sending integration to MailChannels sender-risk infrastructure.
## Core difference
Postmark is organized around servers and message streams. MailChannels is organized around sender governance for platforms with many senders: account or sub-account credentials, authenticated sending domains, runtime send payloads, webhooks, suppression types, and reputation boundaries. The key migration decision is whether Postmark server and stream boundaries should become MailChannels sub-accounts, application metadata, or a simpler shared account model.
## Feature mapping
| Postmark concept | MailChannels concept | Migration approach |
| - | - | - |
| Server-scoped send API | `/send` payload | Replace server token sends with MailChannels API key sends. |
| Servers | Account or sub-account boundaries | Preserve server isolation only when it matters for credentials, reporting, or tenancy. |
| Message streams | Transactional flag and application metadata | Map transactional versus broadcast intent into send classification and suppression policy. |
| Server templates | Application-managed Mustache content | Export templates and move storage outside Postmark. |
| Sender signatures | `from` identities | Preserve sender addresses and align them with authenticated domains. |
| Sender domains | DKIM and Domain Lockdown | Recreate DNS authentication through MailChannels. |
| Webhooks and legacy hook URLs | MailChannels webhooks | Recreate event delivery and update payload consumers. |
| Message-stream suppressions | MailChannels suppression list | Collapse stream-scoped suppressions into account or sub-account policy. |
| Inbound processing | No MailChannels Email API equivalent | Replace inbound processing with another receiving architecture. |
## Send payload notes
Postmark send calls often carry server and stream intent through the token, endpoint, or message stream field. MailChannels sends should make equivalent intent explicit in account selection, sub-account selection, send classification, and application metadata.
| Concern | Postmark behavior | MailChannels behavior |
| - | - | - |
| Authentication | Account token or server token | MailChannels API key authentication |
| Isolation | Server boundary | Account or sub-account boundary |
| Stream type | Transactional or broadcast streams | `transactional` send behavior and suppression policy |
| Templates | Server templates | Application-owned content plus runtime data |
| Inbound | Server inbound processing and triggers | External receiving service |
## Template migration
Export templates per Postmark server and preserve the server context in the new template naming scheme when needed. Replace Postmark template aliases or IDs in application code with application-owned template references.
## Suppression policy
Recommended defaults:
| Postmark source | MailChannels target |
| - | - |
| Hard bounces and spam complaints | Suppress as both `transactional` and `non-transactional`. |
| Broadcast-stream manual suppressions | Suppress as `non-transactional` unless policy requires blocking all mail. |
| Stream-scoped suppressions | Preserve isolation with sub-accounts only when stream boundaries are operationally important. |
## Inbound migration
Postmark inbound processing and inbound rule triggers do not have MailChannels Email API equivalents. Replace those flows before cutover with a dedicated inbound provider, mailbox polling, or an application webhook receiver backed by another receiving service.
## Cutover checklist
* Decide how Postmark servers map to MailChannels account boundaries.
* Replace server-token authentication with MailChannels API keys.
* Translate message stream intent into send classification and metadata.
* Move templates out of Postmark.
* Publish and validate MailChannels DKIM records.
* Recreate webhook consumers for MailChannels events.
* Replace inbound processing before production cutover.
# Migrate from Resend to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-resend
Learn how to migrate a Resend sending integration to MailChannels sender-risk infrastructure.
## Core difference
Resend provides a modern send API for a team's own mail, with domains, templates, contacts, broadcasts, audiences or segments, webhooks, and receiving-related event signals. MailChannels is built for platforms where users, tenants, customers, or agents are the senders. It adds authenticated outbound delivery, runtime personalization, DKIM, Domain Lockdown, webhooks, sender isolation, and explicit suppression types so you can contain abuse without taking down good senders. Resend broadcast and inbound receiving workflows need separate replacements.
## Feature mapping
| Resend concept | MailChannels concept | Migration approach |
| - | - | - |
| Email send API | `/send` payload | Rewrite requests to MailChannels authentication, endpoint, and payload structure. |
| Sending domains | DKIM and Domain Lockdown | Recreate authentication with MailChannels DNS records. |
| Stored templates | Application-managed Mustache content | Export templates and move rendering ownership outside Resend. |
| Webhooks | MailChannels webhooks | Recreate endpoint registration and update consumers for MailChannels event schemas. |
| Contact unsubscribes | Suppression list | Treat as non-transactional suppressions by default. |
| Broadcasts | External campaign workflow | Replace broadcast orchestration outside MailChannels. |
| Audiences or segments | External audience system | Export or rebuild audience membership in a separate list manager. |
| Receiving and `email.received` workflows | No MailChannels Email API equivalent | Replace inbound receiving with another service. |
## Send payload notes
Resend send requests are straightforward, but MailChannels uses its own personalization-centered payload and authentication.
| Concern | Resend behavior | MailChannels behavior |
| - | - | - |
| Authentication | Resend API key | MailChannels API key authentication |
| Request shape | Resend send payload | MailChannels `/send` or `/send-async` payload |
| Merge data | Template or application variables | `dynamic_template_data` in personalizations |
| Broadcasts | Resend-managed campaign sends | External campaign system using MailChannels for delivery |
| Inbound | Receiving-enabled domains and `email.received` events | External inbound service |
## Template migration
Export Resend templates and replace template references in application code with application-owned template names, files, or records. Keep the runtime variable contract explicit so the MailChannels payload provides every value required by the new renderer.
## Suppression policy
Recommended defaults:
| Resend source | MailChannels target |
| - | - |
| Contact unsubscribes | Suppress as `non-transactional`. |
| Complaints or hard-bounce style delivery failures | Suppress as both `transactional` and `non-transactional` when available. |
| Audience or segment preferences | Preserve in the external audience system and mirror to non-transactional suppression where appropriate. |
## Marketing migration
MailChannels Email API does not provide a Resend broadcast or audience store. Move broadcast planning, segmentation, approval, and scheduling into an external workflow. Use MailChannels as the governed delivery layer for permission-based messages that should move through shared-sender controls.
## Inbound migration
Resend receiving and `email.received` workflows do not map to MailChannels Email API resources. Replace them with a dedicated inbound mail service or application-owned receiving architecture before changing production routing.
## Cutover checklist
* Replace Resend endpoint and authentication.
* Translate send payloads into MailChannels personalizations.
* Move templates out of Resend.
* Publish and validate MailChannels DKIM records.
* Recreate webhooks for MailChannels event payloads.
* Replace broadcasts and audiences externally.
* Replace receiving workflows before production cutover.
# Migrate from SendGrid to MailChannels Email API
Source: https://docs.mailchannels.com/email-api/migration-center/migrate-from-sendgrid
Learn how to migrate a SendGrid sending integration to MailChannels sender-risk infrastructure.
## Core difference
SendGrid is broad and mature, with JSON send payloads, personalizations, headers, attachments, reply-to values, template-like data, event webhooks, marketing resources, unsubscribe groups, and branded links. MailChannels is built for platforms where many downstream senders share infrastructure and reputation. The migration should move send calls, but it should also map tenant boundaries, domain authorization, suppressions, webhooks, and containment paths so risky senders can be isolated without disrupting legitimate mail.
## Feature mapping
| SendGrid concept | MailChannels concept | Migration approach |
| - | - | - |
| Mail Send API | `/send` payload | Rewrite the send call to MailChannels authentication, endpoint, and payload conventions. Personalizations and attachments usually map cleanly. |
| Personalizations | `personalizations` | Preserve recipient-specific variables and addressing, but verify field names and nesting. |
| Dynamic template data | `dynamic_template_data` and Mustache content | Keep runtime merge data, but move template rendering into application-controlled content. |
| Dynamic transactional templates | Application-managed content | Export and review templates, then store them in application code or a separate template system. |
| Verified senders | `from` identities plus authenticated domains | Keep valid `from` addresses, but rely on MailChannels domain authentication for durable production sending. |
| Authenticated domains | DKIM and Domain Lockdown | Create MailChannels DKIM records, publish DNS, and validate before cutover. |
| Link branding | MailChannels tracking behavior | Review tracking links and deliverability requirements. Do not assume SendGrid-style branded-link resources exist. |
| Subusers | MailChannels sub-accounts | Map subusers to sub-accounts when isolation, credentials, billing, or reporting boundaries need to remain separate. |
| Unsubscribe groups | Suppression list plus `transactional` flag | Collapse group-specific semantics into MailChannels transactional versus non-transactional suppression policy. |
| Event Webhooks | MailChannels webhooks | Recreate endpoint registration and update consumers for MailChannels event schemas and signing behavior. |
| Marketing lists and Single Sends | External audience and campaign workflow | Export audience data and replace campaign orchestration outside the MailChannels Email API. |
## Send payload notes
SendGrid integrations often require less restructuring than other providers because MailChannels also centers the send request on message content plus `personalizations`. During migration, compare these areas carefully:
| Concern | SendGrid behavior | MailChannels behavior |
| - | - | - |
| API authentication | SendGrid API key bearer token | MailChannels API key authentication |
| Endpoint | SendGrid mail-send endpoint | MailChannels `/send` or `/send-async` |
| Merge data | Dynamic template data attached to personalizations | `dynamic_template_data` used with Mustache-rendered content |
| Tracking | Account, template, or request-level settings | Per-message `tracking_settings` |
| Transactional versus marketing | Often modeled through products and unsubscribe groups | Explicit send classification and suppression behavior |
## Template migration
Do not treat SendGrid dynamic templates as a target-side object copy. MailChannels sending can use runtime dynamic data, but template storage and versioning should move to application code, a CMS, or another template-management service. Preserve template IDs in your migration notes only long enough to connect old application paths to their new content source.
## Suppression policy
Recommended defaults:
| SendGrid source | MailChannels target |
| - | - |
| Bounces, invalid emails, spam reports | Suppress as both `transactional` and `non-transactional`. |
| Global unsubscribes | Suppress as `non-transactional` unless business policy requires a total block. |
| Unsubscribe groups | Map to `non-transactional` suppressions unless a group clearly applies to transactional mail. |
## Marketing migration
MailChannels Email API does not provide a SendGrid Marketing Campaigns equivalent with managed audiences, Single Sends, and campaign scheduling. Move marketing audience storage and campaign orchestration into a separate system, then use MailChannels as the governed delivery layer for permission-based messages that should move through shared-sender controls.
## Cutover checklist
* Replace SendGrid endpoint and authentication.
* Translate payloads and verify personalization rendering.
* Move templates out of SendGrid.
* Create and publish MailChannels DKIM records.
* Recreate webhooks and update event consumers.
* Import or recreate suppressions with clear transactional policy.
* Replace Marketing Campaigns workflows externally.
* Run test sends for each sending domain before production cutover.
# Migration Center
Source: https://docs.mailchannels.com/email-api/migration-center/overview
Plan a migration from a developer email provider to MailChannels sender-risk infrastructure for platforms with many senders.
Use these guides to move sending workflows from another developer email provider to MailChannels Email API. The send call matters, but the larger migration decision is how you will govern many downstream senders without putting shared reputation at risk.
Each guide focuses on the operational differences that usually matter during a migration:
* Outbound send payload shape
* Sender identity and domain authorization
* Tenant isolation and sub-account mapping
* Policy, limits, and containment paths
* Template handling
* Webhook and event delivery
* Suppression and unsubscribe semantics
* Marketing-list or campaign replacement
* Inbound-mail feature gaps
## Migration inventory
Before you write code, inventory the sending features you use today:
| Current feature | MailChannels planning question |
| - | - |
| API keys, subusers, projects, servers, or tenants | Which boundaries need separate MailChannels sub-accounts, credentials, limits, suppressions, or webhooks? |
| Sending domains | Which domains need Domain Lockdown, SPF, DKIM, and DMARC records before production traffic moves? |
| Batch or bulk sends | Should the workflow use `/send-async`, webhooks, and per-sender limits to keep risk scoped? |
| Tags or metadata | Which sender, tenant, campaign, agent, or application IDs should you preserve for monitoring and remediation? |
| Suppressions and bounces | Which bounced or complained recipients should be migrated into your own suppression workflow? |
| Webhooks | Which event handlers need new MailChannels event mappings so delivery outcomes become operational signals? |
## Provider guides
Migrate SendGrid sending while replacing subuser, template, suppression, and marketing assumptions.
Migrate Mailgun domain-scoped sending into account, sub-account, and domain authorization boundaries.
Migrate Mailjet sending while moving campaigns and audience governance outside MailChannels.
Migrate Brevo senders, templates, blocklists, and campaigns into explicit sender controls.
Migrate Postmark servers and streams into MailChannels account, sub-account, or metadata boundaries.
Migrate SES identities, configuration sets, events, and regional assumptions into MailChannels controls.
Migrate Resend sends, domains, templates, audiences, and broadcasts into a governed delivery layer.
## Common MailChannels concepts
| MailChannels concept | Migration meaning |
| - | - |
| `/send` | Runtime API for governed transactional and personalized messages. |
| `/send-async` | Runtime API for larger or queued sends that need webhook-based outcome tracking. |
| `personalizations` | Recipient-specific addressing and merge data. |
| `dynamic_template_data` | Runtime data available to Mustache-rendered message content. |
| `transactional` | Send classification that affects suppression behavior. |
| `tracking_settings` | Per-message click, open, and unsubscribe tracking controls. |
| DKIM | Domain authentication that requires DNS publication. |
| Domain Lockdown | MailChannels control that authorizes which account can send for a protected domain. |
| Sub-accounts | Tenant isolation boundaries for credentials, limits, suppressions, webhooks, and suspension paths. |
| Webhooks | HTTPS event delivery that lets you monitor outcomes and remediate risky senders. |
| Suppression list | MailChannels recipient block list with transactional and non-transactional suppression types. |
## Recommended migration sequence
1. Inventory current sending domains, downstream sender identities, templates, webhooks, suppressions, and any marketing or inbound workflows.
2. Decide whether source-side tenants, servers, subusers, agents, or configuration groups should map to one MailChannels account or to separate MailChannels sub-accounts.
3. Rebuild application send code against MailChannels payloads and authentication.
4. Move provider-managed templates into application code or another template store.
5. Create MailChannels DKIM and Domain Lockdown records and plan DNS publication.
6. Recreate webhook consumers so delivery outcomes feed monitoring, suppression, and remediation workflows.
7. Import or recreate suppressions with explicit transactional versus non-transactional policy.
8. Replace campaign, audience, broadcast, cold outreach, or inbound-mail workflows outside the MailChannels Email API when the source platform provided those features.
9. Run parallel test sends, validate authentication, verify webhooks, and then schedule production cutover.
# Multi-tenancy overview
Source: https://docs.mailchannels.com/email-api/multi-tenancy
Understand the two isolation mechanisms available in Email API, and when to use each.
Email API provides two levels of isolation for separating email traffic: **sub-accounts** and **sender IDs**. They operate
at different layers and can be used independently or together.
| | Sub-account | Sender ID |
| - | - | - |
| Separate API keys | Yes | No |
| Separate sending limits | Yes | No |
| Separate suppression list | Yes | No |
| Separate webhooks | Yes | No |
| Separate usage reporting | Yes | No |
| Can be suspended | Yes | No |
| Reputation isolation | Yes | Yes |
## Sub-accounts
### What are sub-accounts?
Sub-accounts allow you to create and manage many distinct sending identities under your main account. This feature
is useful for agencies, resellers, or organizations that need to manage email sending for multiple clients or departments
while maintaining clear separation between them. Each sub-account has its own credentials, sending controls, suppression list,
webhook configuration, and usage reporting.
### When to use sub-accounts
Sub-accounts are useful when you need separate credentials, limits, or operational controls.
| Use case | Why use a sub-account |
| - | - |
| Agency or reseller sending for multiple clients | Keep each client isolated with its own credentials and usage. |
| Multi-tenant SaaS application | Limit the impact of one tenant's sending behavior on other tenants. |
| Separate business units or brands | Give each group independent API keys, SMTP passwords, webhooks, and suppressions. |
| Migration from providers with subusers, servers, or projects | Preserve existing tenancy boundaries while moving to MailChannels. |
### When not to use sub-accounts
Do not create a sub-account just to send from another domain. If the same application and team own the traffic, Domain
Lockdown, SPF, DKIM, and DMARC may be enough.
Every sub-account adds another operational boundary. Each one comes with separate credentials, suppressions,
webhooks, usage reporting, and limits. Those boundaries are helpful when different clients, tenants, teams, or
applications need to be managed independently. They add unnecessary overhead when the only difference is the domain in
the `from.email` address.
For example, a SaaS product might send billing mail from `billing.example.com`, product alerts from
`notifications.example.com`, and support updates from `support.example.com`. If the same engineering team owns all three
streams and they share the same operational process, create the required Domain Lockdown records and authentication
records for each domain instead of creating three sub-accounts. Use sub-accounts only when you need a separate owner,
credential set, suppression policy, webhook endpoint, usage limit, or incident response path.
### Sub-accounts and suppression lists
Each sub-account has its own suppression list, separate from the parent account's. A recipient on the parent
account's list is not blocked from receiving mail from a sub-account unless they are also on that sub-account's list.
To read or write a sub-account's suppression list, authenticate the request with the **sub-account's own API key** —
not the parent's. See the [suppression list documentation](/email-api/suppression-api-actions) for the available endpoints.
### Sub-accounts and delivery events
Each sub-account manages its own delivery-event webhook, separate from the parent account's. Events from a
sub-account's messages are delivered only to that sub-account's webhook; the parent account does not receive a copy.
To enroll, list, validate, or remove a sub-account's webhook, authenticate the request with the **sub-account's own
API key**, not the parent's. See the [webhooks documentation](/email-api/webhooks) for the available endpoints
and event schema.
## Sender IDs
### What is a sender ID?
A sender ID is a label that Email API assigns to each outbound message based on characteristics found in the message
headers. It acts as a **reputation boundary**: messages sharing the same sender ID are grouped together for delivery
reputation tracking, so poor sending behavior from one traffic stream does not automatically affect others.
Sender IDs are scoped to the account or sub-account that sends the message. If two sub-accounts happen to derive the
same sender ID value from their message headers, those values remain separate because they are always qualified by the
sub-account they belong to.
### What sender IDs provide
Sender IDs give you reputation isolation within a single set of credentials. You can separate the sending reputation of
distinct traffic streams, without creating a new sub-account for each stream.
Unlike sub-accounts, sender IDs do not provide separate API keys, sending limits, suppression lists, webhooks, or usage
reporting. They are a lighter-weight boundary suited to cases where operational separation is not required but reputation
isolation is still useful.
### When to use sender IDs
| Use case | Why use a sender ID |
| - | - |
| Separating transactional and promotional mail within one account | Protect transactional reputation from promotional sending risk, without splitting credentials. |
| Distinguishing mail streams within a sub-account | Identify and isolate a specific integration, product, or campaign at the reputation level. |
| Tracking reputation per sending use case for a single tenant | Get per-stream visibility without the overhead of managing multiple sub-accounts. |
## Choosing the right boundary
Use this guide to help decide which mechanism to use.
**Use a sub-account when:**
* Different customers, tenants, or teams need their own API keys or SMTP credentials.
* You need to enforce separate sending quotas or limits.
* Each sender needs its own suppression list and webhook endpoint.
* You need to be able to suspend or resume a tenant's sending independently.
**Use a sender ID when:**
* A single account or sub-account sends meaningfully different streams of mail, for example, transactional and promotional,
and you want to isolate their reputations.
* You want per-stream reputation tracking without the overhead of managing separate credentials, limits, and webhooks.
# Send email from OpenClaw agents using MailChannels
Source: https://docs.mailchannels.com/email-api/openclaw
Configure an OpenClaw AI agent to send and track email through MailChannels, with a dedicated inbox via AgentMail for inbound replies.
[OpenClaw](https://github.com/openclaw/openclaw) is an open-source AI agent framework that lets you build and run autonomous agents capable of executing tools and skills. One of the most common needs when running an agent is the ability to send email — for notifications, outreach, confirmations, and follow-ups. This guide walks you through connecting OpenClaw to the MailChannels Email API for outbound sending and delivery tracking, and optionally to AgentMail for giving your agent its own dedicated inbox.
## How the pieces fit together
* **OpenClaw** runs your agent and handles tool and skill execution.
* **MailChannels Email API** handles sending email and reports delivery events (delivered, bounced, dropped) back to your agent via webhooks.
* **AgentMail** (optional, but recommended) gives your agent a dedicated inbox — separate from your personal email — with support for real-time events and reply handling.
Each piece does one job, and your agent only gets the permissions it actually needs.
## What you need before you start
* A self-hosted OpenClaw installation.
* MailChannels Email API credentials: `MAILCHANNELS_API_KEY` and `MAILCHANNELS_ACCOUNT_ID` (your customer handle).
* DNS access for the domain you plan to send from.
* Optionally: a public HTTPS endpoint where MailChannels can POST delivery webhooks.
## Setting up the integration
The MailChannels skill for OpenClaw reads two environment variables:
* `MAILCHANNELS_API_KEY` — sent in the `X-Api-Key` header on every request.
* `MAILCHANNELS_ACCOUNT_ID` — your customer handle, visible in the [MailChannels Console](https://console.mailchannels.net).
There is also an optional override if you need a different base URL:
* `MAILCHANNELS_BASE_URL` — defaults to `https://api.mailchannels.net/tx/v1`.
Export them in your shell before launching OpenClaw:
```bash theme={null}
export MAILCHANNELS_API_KEY="YOUR_API_KEY"
export MAILCHANNELS_ACCOUNT_ID="YOUR_ACCOUNT_ID"
# Optional:
export MAILCHANNELS_BASE_URL="https://api.mailchannels.net/tx/v1"
```
Before letting your agent send email from your domain, configure **Domain Lockdown** via a DNS TXT record. This ensures only your MailChannels account can send as that domain.
For each domain you plan to send from, add a TXT record:
| Field | Value |
| - | - |
| **Host** | `_mailchannels.` |
| **Value** | `v=mc1; auid=` |
After adding the record, you can verify your full domain configuration (DKIM, SPF, and Domain Lockdown) using the `POST /check-domain` endpoint.
Your domain should also have a valid SPF record that authorizes MailChannels as a sender. Without SPF, deliverability will be poor and some receiving servers will reject your mail.
The MailChannels integration is published as an OpenClaw skill called `mailchannels-email-api`. Install it with:
```bash theme={null}
npx openclaw add @ttulttul/mailchannels
```
This skill handles two things: sending email through the MailChannels API, and ingesting signed delivery-event webhooks into your OpenClaw environment.
MailChannels exposes a `POST /send` endpoint. The minimum payload requires four fields: `personalizations` (who you are sending to), `from`, `subject`, and `content`.
Here is a `curl` example showing what happens under the hood:
```bash theme={null}
curl -sS -X POST "$MAILCHANNELS_BASE_URL/send" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"personalizations": [
{ "to": [ { "email": "you@example.com" } ] }
],
"from": { "email": "agent@your-domain.com", "name": "OpenClaw Agent" },
"subject": "Hello from OpenClaw via MailChannels",
"content": [
{ "type": "text/plain", "value": "This email was sent by my agent using the MailChannels Email API." }
]
}'
```
Once the skill is installed and your environment variables are set, you can skip `curl` entirely and just instruct your agent:
> Send an email to me using MailChannels with the subject "Test" and a one-line body confirming it worked.
The agent takes it from there.
In agent workflows, you usually do not want your agent blocking while an email is processed — especially when sending to multiple recipients.
Use `POST /send-async` instead of `POST /send`. This endpoint queues the message and returns immediately with a request ID. Delivery events still arrive via webhooks, so your agent retains full visibility and can move on to its next task without waiting.
Sending is only half the story. A genuinely useful agent can also answer: Did it deliver? Did it bounce? Should I retry or suppress that recipient?
**Register a webhook endpoint**
Use the following endpoints to manage your webhook subscriptions:
| Endpoint | Description |
| - | - |
| `POST /webhook` | Enroll for webhook notifications |
| `GET /webhook` | Retrieve enrolled webhooks |
| `DELETE /webhook` | Remove webhooks you no longer need |
| `POST /webhook/validate` | Test that your endpoint is reachable |
| `GET /webhook/public-key` | Retrieve the webhook signing key |
**Verify webhook signatures**
MailChannels signs every delivery-event webhook. Incoming requests include three headers: `Content-Digest`, `Signature-Input`, and `Signature`.
To verify them:
1. Parse `Signature-Input` — it includes `created`, `alg`, and `keyid`.
2. Reject anything with a stale timestamp.
3. Fetch the public key for the given `keyid` using `GET /webhook/public-key`.
4. Verify the Ed25519 signature per RFC 9421.
**Correlate events back to your agent's actions**
Webhook payloads include `email`, `customer_handle`, `timestamp`, `event`, and `request_id`. Persist the `request_id` from each send call so you can match it to delivery events and build a clean state machine: processed → delivered, soft-bounced, hard-bounced, or dropped.
This is what lets your agent make smart decisions — retry on a soft bounce, suppress on a hard bounce, escalate on repeated failures.
Everything above handles outbound email. Agents often also need to *receive* messages — replies, confirmations, OTP codes, inbound support requests.
**AgentMail** is an API-first inbox provider built specifically for AI agents. Install the official OpenClaw integration with either:
```bash theme={null}
openclaw skills install agentmail-to/agentmail-skills/agentmail
```
or via the ClawHub CLI:
```bash theme={null}
npx clawhub@latest install agentmail
```
Then add your AgentMail API key to `~/.openclaw/openclaw.json`:
```json theme={null}
{
"skills": {
"entries": {
"agentmail": {
"enabled": true,
"env": {
"AGENTMAIL_API_KEY": "your-api-key-here"
}
}
}
}
}
```
AgentMail supports programmatic inbox creation, including custom domains. Once configured, your agent can receive replies in a dedicated inbox, search and triage messages, and trigger workflows from inbound email — all without touching your personal mailbox.
## Ideas for what to build next
With MailChannels handling outbound and AgentMail handling inbound, your OpenClaw agent is ready for workflows that feel hands-free but stay operationally sound:
* **Daily briefings** — Your agent compiles calendar items, tasks, and news into a single email, sends it via MailChannels, and tracks delivery.
* **Support triage** — Inbound email arrives at AgentMail, the agent labels and routes it, then sends a reply (or drafts one for your approval) through MailChannels.
* **Deliverability-aware automation** — If a recipient hard-bounces, the agent automatically adds them to the suppression list and notifies your ops team.
## API quick reference
**Base URL:** `https://api.mailchannels.net/tx/v1`
| Category | Endpoint | Description |
| - | - | - |
| Sending | `POST /send` | Send an email synchronously |
| Sending | `POST /send-async` | Queue an email and return immediately |
| Webhooks | `POST /webhook` | Enroll for webhook notifications |
| Webhooks | `GET /webhook` | Retrieve enrolled webhooks |
| Webhooks | `DELETE /webhook` | Delete webhooks |
| Webhooks | `POST /webhook/validate` | Test your webhook endpoint |
| Webhooks | `GET /webhook/public-key` | Retrieve the webhook signing key |
| Domains | `POST /check-domain` | Check DKIM, SPF, and Domain Lockdown |
| Usage | `GET /usage` | Retrieve account usage stats |
| Suppression | `POST /suppression-list` | Add suppression entries |
| Suppression | `GET /suppression-list` | Retrieve the suppression list |
| Suppression | `DELETE /suppression-list/recipients/:recipient` | Remove a suppression entry |
# MailChannels Email API: transactional email for platforms
Source: https://docs.mailchannels.com/email-api/overview
Send transactional email from apps with many users, tenants, customers, or agents. MailChannels adds sender controls that help protect shared reputation.
MailChannels Email API is a transactional email delivery API for platforms with many senders. It helps you authorize downstream senders, contain abuse quickly, and keep legitimate mail flowing across shared reputation.
Most developer email tools are optimized for a company sending its own mail. MailChannels is built for apps where your users, tenants, customers, or agents send through your product.
## How it works
Your application calls the MailChannels API endpoint with the message details: sender, recipients, subject, and body. MailChannels validates the request against your domain configuration, evaluates sender-risk signals, applies the right controls, and delivers accepted messages to the recipient's mail server.
The API uses standard JSON over HTTPS, so you can integrate it from any language or platform that can make HTTP requests.
## Choose the right starting point
Use the path that matches what you are building:
| Goal | Start here |
| - | - |
| Send your first transactional email | [Quickstart](/email-api/curl/quickstart) |
| Send from Node.js | [Node.js quickstart](/email-api/javascript/quickstart) |
| Authorize a downstream sending domain | [Domain Lockdown](/email-api/domain-lockdown) |
| Improve domain authentication | [SPF, DKIM, and DMARC](/email-api/spf-dkim-dmarc) |
| Track delivery outcomes | [Webhooks](/email-api/webhooks) |
| Isolate tenants or traffic streams | [Multi-tenancy](/email-api/multi-tenancy) |
| Understand abuse controls | [Reputation and abuse control](/email-api/reputation-abuse-control) |
| Move from another provider | [Migration Center](/email-api/migration-center/overview) |
## Key benefits
**Authorize downstream senders.** Domain Lockdown™ ties each sending domain to your account so another customer cannot send through MailChannels as that domain.
**Detect risky senders.** Behavioral scoring, content signals, recipient feedback, and sender history help identify the source of risk before one sender's behavior becomes everyone's delivery problem.
**Contain abuse fast.** Sub-accounts, sending limits, suppressions, suspensions, and policy enforcement keep abuse scoped to the sender or tenant causing the problem.
**Keep good mail flowing.** Shared reputation stays protected so legitimate senders can continue delivering while your operators investigate and remediate the source of abuse.
**Operate with evidence.** Signed webhooks, delivery events, metrics, and troubleshooting data give abuse, support, and engineering teams the signals they need to act.
## What you need to get started
Before you can send your first email, you need three things:
1. A MailChannels account, created at [dash.mailchannels.net](https://dash.mailchannels.net)
2. An API key with the `api` scope, generated in the Console
3. A Domain Lockdown DNS TXT record for each domain you send from
The quickstart guide walks you through each of these steps and ends with a working example.
## Production checklist
Before you send production traffic, verify each item:
* Store your API key in a secrets manager or environment variable.
* Add a Domain Lockdown record for every domain used in the `from.email` field.
* Publish SPF for each sending domain.
* Configure DKIM signing and publish the DKIM DNS record.
* Add a DMARC record, starting with a monitoring policy such as `p=none`.
* Create a webhook endpoint and store delivery events for operational troubleshooting.
Sign up, create an API key, configure your domain, and send your first email in minutes.
Learn how API keys work and how to pass the `X-Api-Key` header in every request.
Add the DNS TXT record that authorizes your domain to send through MailChannels.
Review plans, usage limits, and billing details for the Email API.
# PHP quickstart
Source: https://docs.mailchannels.com/email-api/php/quickstart
Send an email with the MailChannels Email API using PHP.
Send an email in a few minutes. This guide walks through sending your first request to the MailChannels Email API.
The PHP SDK is open source. Browse the [source code](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-php/).
## Prerequisites
Before your first request, complete the following steps.
[Sign up](https://dash.mailchannels.com) if you don't already have an account.
Open [Account > API Keys](https://dash.mailchannels.com/account/api-keys) and click **Create API Key**.
Add a descriptive label and set the scope to `api`. Copy the key; you won't be able to see it again.
Treat your API key like a password. Don't commit it to source control or expose it in client-side code.
For each domain you'll send from, add a `TXT` record on the `_mailchannels` subdomain authorizing your account to send.
Replace `examplecorp` with your MailChannels account ID (visible in the panel footer when you log in):
```
_mailchannels.example.com TXT "v=mc1 auth=examplecorp"
```
See the [Domain Lockdown guide](/email-api/domain-lockdown) for details.
Add MailChannels to the SPF record on each sending domain so recipient servers accept your mail:
```
example.com TXT "v=spf1 include:relay.mailchannels.net ~all"
```
If you already have an SPF record, merge in `include:relay.mailchannels.net` rather than creating a second record.
A domain must have only one SPF record.
Your sending domain must resolve. It needs a valid `A` or `MX` record. Without one, mail from the domain is rejected with
`550 5.1.2 [SDNF] Sender Domain Not Found`. Most registered domains already have these records. Confirm yours with the
[check-domain tool](https://dash.mailchannels.com/domain-health). See [Troubleshooting](/email-api/troubleshooting) for details.
## Install the PHP SDK
```php PHP theme={null}
composer require mailchannels/mailchannels-php
# You also need a PSR-18 client and PSR-17 factories. Guzzle is a popular choice.
composer require guzzlehttp/guzzle
```
Using a PHP framework? See the [Laravel](/email-api/laravel), [Symfony](/email-api/symfony) integration guides.
## Send your first email
To send an email, provide the recipients, sender, subject, and message content. The example below sends an HTML email to a
single recipient.
Until you add a payment method, sending is restricted to addresses belonging to verified [Users](https://dash.mailchannels.com/account/users)
**on your account**. Add a user, click the link in the verification email, then use that address as the `to` email below.
```php PHP theme={null}
emails->send([
'from' => ['email' => $from_email, 'name' => 'Your Name'],
'to' => ['email' => $to_email, 'name' => 'Recipient'],
'subject' => 'Hello from MailChannels',
'text' => 'Hello! This is a plain-text fallback for clients that don\'t render HTML.',
'html' => '
',
]);
print_r($response->toArray());
```
Set the following before running:
* `MAILCHANNELS_API_KEY` — the API key generated above.
* `FROM_EMAIL` — an address on the domain you configured with Domain Lockdown and SPF above (e.g. `hello@example.com` if
you used `example.com` in the Domain Lockdown and SPF DNS records).
* `TO_EMAIL` — a recipient inbox you can check, e.g. the address you use to sign up for MailChannels.
## Next steps
* Read the [webhooks guide](/email-api/webhooks) to learn how to get real-time notifications about email delivery,
bounces, and complaints.
* Explore the [API reference](/email-api/api-reference-introduction) to see what else you can do with the API.
# Python quickstart
Source: https://docs.mailchannels.com/email-api/python/quickstart
Send an email with the MailChannels Email API using Python.
Send an email in a few minutes. This guide walks through sending your first request to the MailChannels Email API.
The Python SDK is open source. Browse the [source code](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-py/).
## Prerequisites
Before your first request, complete the following steps.
[Sign up](https://dash.mailchannels.com) if you don't already have an account.
Open [Account > API Keys](https://dash.mailchannels.com/account/api-keys) and click **Create API Key**.
Add a descriptive label and set the scope to `api`. Copy the key; you won't be able to see it again.
Treat your API key like a password. Don't commit it to source control or expose it in client-side code.
For each domain you'll send from, add a `TXT` record on the `_mailchannels` subdomain authorizing your account to send.
Replace `examplecorp` with your MailChannels account ID (visible in the panel footer when you log in):
```
_mailchannels.example.com TXT "v=mc1 auth=examplecorp"
```
See the [Domain Lockdown guide](/email-api/domain-lockdown) for details.
Add MailChannels to the SPF record on each sending domain so recipient servers accept your mail:
```
example.com TXT "v=spf1 include:relay.mailchannels.net ~all"
```
If you already have an SPF record, merge in `include:relay.mailchannels.net` rather than creating a second record.
A domain must have only one SPF record.
Your sending domain must resolve. It needs a valid `A` or `MX` record. Without one, mail from the domain is rejected with
`550 5.1.2 [SDNF] Sender Domain Not Found`. Most registered domains already have these records. Confirm yours with the
[check-domain tool](https://dash.mailchannels.com/domain-health). See [Troubleshooting](/email-api/troubleshooting) for details.
## Install the Python SDK
```python Python theme={null}
# pip
pip install mailchannels
# uv
uv add mailchannels
```
## Send your first email
To send an email, provide the recipients, sender, subject, and message content. The example below sends an HTML email to a
single recipient.
Until you add a payment method, sending is restricted to addresses belonging to verified [Users](https://dash.mailchannels.com/account/users)
**on your account**. Add a user, click the link in the verification email, then use that address as the `to` email below.
```python Python theme={null}
import os
import sys
import mailchannels
# The SDK reads MAILCHANNELS_API_KEY from the environment automatically.
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY before running")
from_email = os.environ.get("FROM_EMAIL")
to_email = os.environ.get("TO_EMAIL")
if not from_email:
sys.exit("Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)")
if not to_email:
sys.exit("Set TO_EMAIL")
response = mailchannels.Emails.send(
{
"from": {"email": from_email, "name": "Your Name"},
"to": [{"email": to_email, "name": "Recipient"}],
"subject": "Hello from MailChannels",
"text": "Hello! This is a plain-text fallback for clients that don't render HTML.",
"html": (
"
'
),
}
)
print(response)
```
Set the following before running:
* `MAILCHANNELS_API_KEY` — the API key generated above.
* `FROM_EMAIL` — an address on the domain you configured with Domain Lockdown and SPF above (e.g. `hello@example.com` if
you used `example.com` in the Domain Lockdown and SPF DNS records).
* `TO_EMAIL` — a recipient inbox you can check, e.g. the address you use to sign up for MailChannels.
## Next steps
* Read the [webhooks guide](/email-api/webhooks) to learn how to get real-time notifications about email delivery,
bounces, and complaints.
* Explore the [API reference](/email-api/api-reference-introduction) to see what else you can do with the API.
# Reputation and abuse control
Source: https://docs.mailchannels.com/email-api/reputation-abuse-control
Learn how MailChannels evaluates outbound email traffic at granular levels to stop abuse while protecting legitimate senders.
MailChannels protects outbound email reputation with a multi-stage filtering pipeline built for transactional email
from dynamic, customer-controlled sources. Messages can originate from applications, agents, CMS plugins, SaaS
tenants, sub-accounts, scripts, and other automated systems. The pipeline evaluates identity, infrastructure,
content, recipient patterns, and sending behavior before choosing how a message should be handled.
This page explains the architecture at a high level. It does not expose proprietary thresholds, scoring formulas,
model features, or threat intelligence sources.
## Granular identity tracking
Before MailChannels evaluates message content, it first determines who is responsible for the traffic. The platform
normalizes multiple signals into sender-level tracking context so abuse can be attributed more precisely than the
visible `from.email` address.
For Email API traffic, that context can include:
* The parent account.
* The sub-account, when a sub-account API key is used.
* The sending domain authorized by Domain Lockdown.
* The `campaign_id`, when included in the send request.
* Envelope and header details that help correlate related traffic.
This lets MailChannels separate legitimate traffic from suspicious traffic at narrow boundaries. For example, a
reseller can isolate each client with sub-accounts, then use `campaign_id` to distinguish campaigns or segments
inside a client sub-account. If one campaign starts producing abnormal bounce or complaint patterns, that traffic
can be understood in context without treating every message from the reseller as identical.
## Policy and infrastructure checks
MailChannels validates sender and account context before and during delivery. These checks help prevent spoofing,
unauthorized sending, quota abuse, and infrastructure patterns commonly associated with compromised systems.
Examples include:
* Confirming that the sender is authorized through Domain Lockdown.
* Evaluating SPF, DKIM, and DMARC posture for sending domains.
* Checking account, sub-account, plan, and usage constraints.
* Comparing sender, domain, IP, and URL signals with internal and external reputation data.
* Applying additional scrutiny to new domains, new sender identities, or traffic patterns without an established
history.
These controls help stop abuse before it harms shared infrastructure or recipient trust.
## Content and payload analysis
Each message is evaluated for signs of spam, phishing, malware, credential theft, and other abusive behavior.
MailChannels uses layered scanning rather than relying on one signal.
The platform can evaluate:
* Message body and attachment characteristics.
* URLs and domains found in the message.
* Header structure and message formatting.
* Sender and reply-to alignment.
* Patterns commonly produced by compromised scripts or automated abuse tools.
* Similarities between messages that may be part of the same campaign, even when they use different sender addresses.
This layered approach lets MailChannels distinguish a normal transactional receipt from a credential phishing
attempt, a malware attachment, or a sudden burst of low-quality bulk mail.
## Behavioral profiling over time
Abuse is often visible only when messages are viewed as a stream. MailChannels evaluates sender behavior across
short and longer time windows so it can detect changes that a single-message scan may miss.
Behavioral signals can include:
* Message volume and sending velocity.
* Recipient validity and bounce patterns.
* Complaint and unsubscribe behavior.
* Repeated delivery failures.
* Sudden changes in content, domains, URLs, or recipient lists.
* New sender identities that ramp volume before building a positive history.
This is especially important for shared platforms and SaaS products. One tenant, client, script, or campaign can
be handled differently from the rest of the account when its behavior diverges from normal traffic.
## Recipient list and hygiene signals
Recipient quality is a major reputation signal. Poor list hygiene can look similar to abuse, especially when a
sender hits many invalid addresses, spam traps, or unrelated recipient patterns in a short time.
MailChannels evaluates recipient behavior and list structure to identify:
* Purchased or scraped lists.
* Dictionary-style address generation.
* High invalid-recipient rates.
* Repeated hard bounces.
* Complaint-heavy recipient groups.
* Suppression-list conflicts.
Use [suppressions and unsubscribe handling](/email-api/suppressions-unsubscribes) to keep bad recipients out of future
sends and to honor recipient preferences.
## Routing and enforcement
After MailChannels combines identity, policy, content, recipient, and behavioral signals, the platform chooses an
enforcement outcome. The goal is to protect legitimate senders and recipient networks while containing suspicious
or abusive traffic as narrowly as possible.
Possible outcomes include:
| Outcome | What it means |
| - | - |
| Accept | The message is queued for delivery. |
| Defer | Sending is temporarily slowed or retried later when the traffic needs more control. |
| Reject | The message is blocked because it violates policy or reputation safeguards. |
| Drop | The message is stopped before delivery and reported through delivery events when applicable. |
| Route with additional controls | Traffic may be handled differently based on risk, sender history, or reputation context. |
These decisions are dynamic. A clean sender with stable behavior should not be treated the same way as a newly
observed sender with a sudden spike in invalid recipients, suspicious URLs, or complaint-heavy traffic.
## What this means for Email API users
For the specific things to do (and avoid) as a sender, see
[sending policies and best practices](/email-api/sending-policies).
# Security
Source: https://docs.mailchannels.com/email-api/security
Protect MailChannels Email API credentials, webhook consumers, and message data in production systems.
Treat your email integration as production infrastructure. It can send messages to customers, expose personal data in logs,
and affect your sender reputation.
## API keys
* Store API keys in a secrets manager or environment variables.
* Use separate keys for development, staging, and production.
* Use one key per application or service.
* Rotate keys on a schedule and immediately after suspected exposure.
* Revoke unused keys.
* Never expose API keys in client-side code, mobile apps, screenshots, logs, or support tickets.
## Webhook security
* Verify every webhook signature before trusting the payload.
* Reject signatures outside a short freshness window.
* Store the raw body before parsing.
* Make processing idempotent because replay and duplicate delivery can happen during recovery workflows.
* See the [webhook](/email-api/webhooks) documentation for more details on webhooks and best practices.
## Data handling
Email content and metadata can contain personal data. Avoid logging full message bodies, private DKIM keys, API keys,
unsubscribe links, and sensitive recipient data. When support needs examples, provide `request_id`, timestamps, and sender
and recipient domain.
## Transport security
MailChannels sends outbound email over TLS when the receiving mail server supports it.
## Security contact
If you believe you found a security issue, contact MailChannels support with a clear description, reproduction steps,
affected endpoints, and any relevant timestamps.
# Send vs send async
Source: https://docs.mailchannels.com/email-api/send-vs-send-async
Learn the differences between sending emails synchronously and asynchronously.
## Synchronous vs asynchronous sending
There are two ways to send an email: synchronously and asynchronously. When you send an email synchronously, your application
waits for the API to process the request and return a response. For a single email, this is usually pretty quick. But for
larger sends, the processing time can take much longer.
When you send an email asynchronously, your application makes a request and immediately receives a response. In the background,
the API processes the request. Your application does not wait. In order to track the status of the send, you will receive
webhooks for all delivery status updates (dropped, processed, delivered, hard-bounced, etc.). These webhooks are identical
to the webhooks you receive for synchronous sends.
## When to use Send vs Send Async
Synchronous sending should only be used when the user needs feedback on the status of the send before proceeding, and the
number of emails being sent is small. Our evaluation process can take several seconds per message.
Asynchronous sending should be used for most requests. It is important to have webhooks set up to receive delivery status
updates. You can view webhook events on the [webhooks page](https://dash.mailchannels.com/webhooks).
For more information on webhooks, see the [webhooks documentation](/email-api/webhooks).
## Examples
Send synchronously:
```bash cURL theme={null}
#!/usr/bin/env bash
# Send an email synchronously
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST https://api.mailchannels.net/tx/v1/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <emails->send([
'from' => ['email' => $from_email, 'name' => 'Your Name'],
'to' => ['email' => $to_email, 'name' => 'Recipient'],
'subject' => 'Hello from MailChannels (sync)',
'text' => 'This message was sent with emails->send. The caller waited for the response.',
]);
print_r($response->toArray());
```
Send asynchronously:
```bash cURL theme={null}
#!/usr/bin/env bash
# Send an email asynchronously.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY before running}"
: "${FROM_EMAIL:?Set FROM_EMAIL (must be on a Domain-Lockdown-authorized domain)}"
: "${TO_EMAIL:?Set TO_EMAIL}"
curl -X POST https://api.mailchannels.net/tx/v1/send-async \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d @- <queue posts to /tx/v1/send-async. Delivery events are reported via webhooks.
$response = $client->emails->queue([
'from' => ['email' => $from_email, 'name' => 'Your Name'],
'to' => ['email' => $to_email, 'name' => 'Recipient'],
'subject' => 'Hello from MailChannels (async)',
'text' => 'This message was sent with emails->queue. Delivery status will arrive via webhook.',
]);
print_r($response->toArray());
```
# Sending policies and best practices
Source: https://docs.mailchannels.com/email-api/sending-policies
Follow these practices so your traffic stays in good standing with MailChannels and reaches the inbox.
How to prevent blocks and get your traffic accepted. For the controls behind these rules, see
[reputation and abuse control](/email-api/reputation-abuse-control).
## Reading a rejection
Rejections take the shape `550 5.7.1 [CODE] reason.` The bracketed code tells you which control fired.
These codes are designed to help you understand why a message was rejected. They are not part of the SMTP standard and can
change at any time as we update our policies. Below are some examples of common rejection codes and their meanings.
## Authenticate every sending domain
* Configure [Domain Lockdown](/email-api/domain-lockdown) for every domain you send from.
* Publish [SPF, DKIM, and DMARC](/email-api/spf-dkim-dmarc) records before you scale.
* Keep `from.email`, `reply_to`, and the DKIM signing domain aligned.
* Never send on behalf of domains you do not control.
Rejections when authentication is wrong:
```
550 5.7.1 This sender is not authorized to send from example.com
550 5.7.1 [BFD] Sender prohibited by SPF
550 5.7.1 [ESA] Sender blocked.
```
## Segment your traffic
* Use a separate [sub-account](/email-api/multi-tenancy) for each client, tenant, or application.
* Use consistent headers within each mail stream. MailChannels derives a `sender-id` from message headers, so consistency
allows each stream to build its own reputation history.
* Set `campaign_id` per send for campaign-level metrics, webhook correlation, and isolation.
* Use distinct domains or subdomains for transactional and marketing mail.
## Send only to recipients who asked for mail
* Send only to recipients with explicit consent. Do not buy, scrape, or rent lists.
* Do not send speculatively to generated or role addresses (`info@`, `sales@`, sequential local parts).
* Validate addresses at signup. Drop typos and disposable domains.
Rejections when list hygiene slips:
```
550 5.7.1 [BR] Our system has detected an excessively high number of invalid recipients originating from your account.
550 5.7.1 [UNS] Recipient has unsubscribed from your list.
550 5.7.1 [RB] This recipient email address has been blocked.
```
## Keep message content honest
* Use a stable `from.email` per mail stream. Do not rotate sender addresses to evade filters.
* Use HTTPS links to domains you control. Avoid URL shorteners and redirector chains.
* Avoid executables, scripts, and password-protected archives as attachments.
* Match the HTML and plain-text bodies. Do not hide content in one.
Rejections from content and header scans:
```
550 5.7.1 [CS] Our system has detected that this message is a virus.
550 5.7.1 [CS] Message blocked. If this is a false positive, please report this to your hosting service provider.
550 5.7.1 [CSR] Account blocked. See
550 5.7.1 [FEB] Your message could not be delivered because it contained an attachment type that is not allowed.
550 5.7.1 [SHF] Message rejected.
550 5.7.1 [XM] Blocked.
```
## Ramp new traffic gradually
* Warm new domains over days or weeks. Start low, grow as engagement stays healthy.
* Treat a new sub-account, `campaign_id`, or recipient segment the same way.
* Keep daily volume consistent. Unexplained spikes attract scrutiny.
Velocity and quota rejections:
```
550 5.7.1 [STF] Sending too fast.
550 5.7.1 [NS] Our system has detected an unusual amount of unsolicited email originating from your address.
550 5.7.1 This sender has exceeded their daily limit for email api requests.
451 4.7.1 Sending quota exceeded.
```
## Process feedback signals
* Subscribe to [webhooks](/email-api/webhook-event-types) and process them in your sending pipeline.
* Watch [metrics](/email-api/metrics) for bounce rate, complaint rate, and engagement trends.
# Sending via SMTP
Source: https://docs.mailchannels.com/email-api/smtp-sending
Learn how to send emails via SMTP.
## Overview
Along with the HTTP Email API, you can also send mail via SMTP. This guide walks through sending your first email using
the MailChannels SMTP API.
## Prerequisites
Open [Account > SMTP Passwords](https://dash.mailchannels.com/account/smtp-credentials)
and click **Create SMTP Credentials**.
Treat your SMTP password like a password. Don't commit it to source control or expose it in client-side code.
Add MailChannels to the SPF record on each sending domain so recipient servers accept your mail. See the [SPF page](/email-api/spf-dkim-dmarc)
for more details.
[Domain Lockdown](/email-api/domain-lockdown) is required for SMTP sending as well.
Configure your MTA to relay through `smtp.mailchannels.net` on port `587` with TLS. Use your MailChannels account ID as
the username and the SMTP password you created as the password.
## Configuring your MTA
The [mail server setup guide](/outbound/configure-mail-server) will help you to configure your MTA to send via MailChannels.
## SMTP vs Email API
MailChannels does not DKIM sign messages sent via SMTP. However, DKIM signatures on mail sent via SMTP will not be broken.
Messages sent via SMTP do not produce webhooks.
# Handling soft-bounced events
Source: https://docs.mailchannels.com/email-api/soft-bounced-events
Learn about the best practices for responding to soft-bounced webhook events.
## What is a soft-bounced event?
A soft-bounced event is generated when a recipient's server temporarily rejects an email (returning a 4xx SMTP status code).
The event data includes the specific reason for the bounce, for example, if there's a connection timeout or a full mailbox.
MailChannels will automatically retry delivery for soft-bounced messages for up to six hours, or until the message is successfully delivered.
If the message cannot be delivered within this retry window, it is treated as a permanent failure and won't be retried.
You will receive a [hard-bounced event](/email-api/hard-bounced-events) for the message if this happens.
Note that you may receive multiple soft-bounced events for a single message. The retry period may change in the future.
## What to do if you receive a soft-bounced event
If you receive a soft-bounced event, you most likely don't need to take any action, as MailChannels will automatically retry delivery for soft-bounced messages.
However, you can investigate the reason for the bounce using the `status` code and `reason` fields.
For more in-depth information on SMTP response codes and their meanings, see our [SMTP response code documentation](https://blog.mailchannels.com/understanding-smtp-response-codes/).
## Fields for soft-bounced events
* `email`: Sender email address.
* `customer_handle`: Your account ID.
* `timestamp`: Unix timestamp of the event.
* `event`: The type of event, `soft-bounced` in this case.
* `recipients`: List of recipient email addresses that couldn't receive the message.
* `status`: The SMTP status code received for the soft bounce.
* `reason`: A human-readable explanation of why the message bounced.
* `campaign_id` *(optional)*: The campaign ID associated with the message, if applicable.
* `request_id` *(optional)*: A unique identifier generated to track the original HTTP request. This will be blank if the message is sent via SMTP instead of the API.
* `smtp_id`: A unique identifier used to track the message, matching the `Message-Id` header.
## Example soft-bounced event
```json theme={null}
{
"email": "sender@example.com",
"customer_handle": "examplecorp",
"timestamp": 1700000000,
"event": "soft-bounced",
"recipients": [
"recipient@example.net",
"recipient2@example.net"
],
"status": "421",
"reason": "Service not available",
"campaign_id": "spring-sale-2026",
"request_id": "4b06083a23724f279c8d11a370e641f8",
"smtp_id": ""
}
```
# SPF, DKIM, and DMARC
Source: https://docs.mailchannels.com/email-api/spf-dkim-dmarc
Learn how SPF, DKIM, and DMARC records authenticate your email and improve deliverability.
## SPF (Sender Policy Framework)
SPF is an email authentication standard that allows domain owners to specify which mail servers are authorized to send emails
on their behalf. An SPF record is a DNS TXT record that lists the IP addresses and/or hostnames that may send mail for a domain.
**How it works:** When a receiving mail server gets a message, it looks up the SPF record published in the DNS of the
sender's `MAIL FROM` domain. Then, the connecting IP address is compared against the authorized senders listed in that record.
If the IP matches, SPF passes; if it doesn't, SPF fails.
**Why it matters:** SPF makes it harder for attackers to spoof your domain by sending mail from unauthorized servers.
**If your domain has no SPF record:** Create a DNS TXT record with the following value, replacing `example.com` with your
domain:
```
v=spf1 include:relay.mailchannels.net ~all
```
**If your domain has an SPF record:** Add `include:relay.mailchannels.net` to the existing record. For example, if your
current record is `v=spf1 include:example.com ~all`, update it to:
```
v=spf1 include:example.com include:relay.mailchannels.net ~all
```
## DKIM (DomainKeys Identified Mail)
DKIM is an email authentication standard that uses public key cryptography to sign emails from your domain.
**How it works:** The sending mail server adds a `DKIM-Signature` header to each outgoing message, containing a
cryptographic signature generated from selected headers and the message body using a private key. The receiving server
fetches the matching public key from a DNS TXT record at `._domainkey.` and uses it to verify the
signature. A valid signature proves that the message was authorized by the domain owner and that the signed content has
not been altered in transit.
**Why it matters:** It verifies that the email came from the specified domain, and prevents tampering with certain headers
and the message body.
## DMARC (Domain-based Message Authentication, Reporting, and Conformance)
DMARC is an authentication standard that tells recipients what to do in the case of an email that fails SPF and DKIM checks.
It provides senders with a way to publish policies in their DNS records that specify how to handle unauthenticated emails.
**How it works:** DMARC specifies two things: *alignment* and *policy*. Alignment requires that the domain validated by
SPF or DKIM matches the domain in the `From:` header. This prevents a nefarious actor from passing SPF/DKIM for one domain,
and displaying another in the recipient's inbox. Policy, published as a DNS TXT record, tells receivers what to do with
messages that fail alignment.
**Why it matters:** SPF and DKIM on their own don't protect the `From:` address the recipient sees. DMARC closes that gap.
## Consequences of not configuring SPF, DKIM, and DMARC
Without SPF, DKIM, and DMARC, your emails will be marked as spam, or rejected entirely by receiving servers. This means
poor deliverability and a poor sender reputation.
Gmail requires DKIM for domains sending more than [5,000 messages per day](https://support.google.com/a/answer/81126).
[Yahoo](https://senders.yahooinc.com/best-practices/) and [Microsoft](https://techcommunity.microsoft.com/blog/microsoftdefenderforoffice365blog/strengthening-email-ecosystem-outlook%E2%80%99s-new-requirements-for-high%E2%80%90volume-senders/4399730)
have similar requirements.
## MailChannels' requirements
SPF, DKIM, and DMARC are not explicitly checked as part of the sending process. We will attempt to deliver your mail regardless
of your adherence to these standards.
**If you want your message to be received, you must have SPF, DKIM, and DMARC configured correctly.**
# Authenticating a sub-account
Source: https://docs.mailchannels.com/email-api/sub-accounts-authentication
Issue API keys and SMTP passwords for sub-accounts.
## How sub-account authentication works
Every sub-account has its own credentials, separate from the parent account. A request authenticated with a
sub-account API key is treated as that sub-account: it sends from the sub-account's quota, writes to the
sub-account's suppression list, and triggers the sub-account's webhooks.
Sub-accounts have access to the full API and Console. Sub-accounts cannot create sub-accounts.
To get the sub-account ready to send, create at least one API key. Use the parent API key when using the sub-account API
to create a sub-account API key.
Sub-accounts can manage their own [API keys](/email-api/authentication).
Or a parent account can manage sub-account keys using the [sub-account API](/email-api/authentication#sub-account-key-management).
# Creating a sub-account
Source: https://docs.mailchannels.com/email-api/sub-accounts-create
Create a sub-account under your MailChannels parent account.
## Before you start
Only a parent account can create sub-accounts; sub-accounts cannot create further sub-accounts.
## Create a sub-account
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY (a parent-account API key) before running}"
: "${COMPANY_NAME:?Set COMPANY_NAME (display name for the sub-account)}"
# SUB_ACCOUNT_HANDLE is optional. If unset, MailChannels generates a random handle.
HANDLE_FIELD=""
if [ -n "${SUB_ACCOUNT_HANDLE:-}" ]; then
HANDLE_FIELD=", \"handle\": \"$SUB_ACCOUNT_HANDLE\""
fi
curl -X POST https://api.mailchannels.net/tx/v1/sub-account \
-H "Content-Type: application/json" \
-H "X-Api-Key: $PARENT_API_KEY" \
-d "{ \"company_name\": \"$COMPANY_NAME\"$HANDLE_FIELD }"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, COMPANY_NAME, SUB_ACCOUNT_HANDLE } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!COMPANY_NAME) throw new Error("Set COMPANY_NAME (display name for the sub-account)");
const mailchannels = new MailChannels(PARENT_API_KEY);
// SUB_ACCOUNT_HANDLE is optional. If omitted, MailChannels generates a random handle.
const { data, error } = await mailchannels.subAccounts.create(COMPANY_NAME, SUB_ACCOUNT_HANDLE);
if (error) {
console.error("Create sub-account failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
company_name = os.environ.get("COMPANY_NAME")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not company_name:
sys.exit("Set COMPANY_NAME (display name for the sub-account)")
mailchannels.api_key = parent_api_key
# SUB_ACCOUNT_HANDLE is optional. If omitted, MailChannels generates a random handle.
response = mailchannels.SubAccounts.create(
company_name=company_name,
handle=os.environ.get("SUB_ACCOUNT_HANDLE"),
)
print(response)
```
```php PHP theme={null}
subAccounts->create(
companyName: $company_name,
handle: getenv('SUB_ACCOUNT_HANDLE') ?: null,
);
print_r($response->toArray());
```
Set the following before running:
* `PARENT_API_KEY` — a parent-account API key with the `Sending Email` scope.
* `COMPANY_NAME` — a display name for the sub-account, between 3 and 128 characters. Shown on the unsubscribe page when using
Unsubscribe Links.
* `SUB_ACCOUNT_HANDLE` — optional, between 3 and 128 characters, consisting of lowercase letters and digits. If omitted,
MailChannels generates a random handle. The handle is the identifier used in every subsequent sub-account API call.
A successful call returns a response with the new sub-account's `handle`, `company_name`, and `enabled` state.
# Sub-account sending limits
Source: https://docs.mailchannels.com/email-api/sub-accounts-limits
Learn how to manage sending limits for sub-accounts.
## How sub-account limits work
A sub-account limit represents the maximum number of messages that can be sent from that sub-account during a billing period.
By default, sub-accounts have no limit; they can send as many messages as the parent account's limit allows.
Sub-account limits can be set to any non-negative integer and their sum might exceed the parent account's overall limit.
The parent account's limit is a hard ceiling; once the parent account's limit is reached, all subsequent sends from the
parent account and all sub-accounts are blocked until the next billing period.
### Example
Parent account `P` has two sub-accounts, `SUB_A` and `SUB_B`:
| Account | Sending limit |
| - | - |
| `P` | 100,000 |
| `SUB_A` | 70,000 |
| `SUB_B` | 70,000 |
The sub-account limits sum to 140,000, which is more than `P`'s overall limit of 100,000. That means the parent limit,
not the sub-account limits, will be the ceiling once total sends across the parent and sub-accounts reach 100,000.
Suppose `SUB_A` sends 70,000 messages and `SUB_B` has not sent any:
* `SUB_A` has hit its own 70,000 limit and is blocked from sending further.
* `P` has used 70,000 of its 100,000 overall capacity, leaving 30,000.
* `SUB_B` can still send up to 30,000 messages, even though its own limit is 70,000. The parent's remaining capacity
is now the binding ceiling.
Once `SUB_B` sends those 30,000 messages, `P` reaches its 100,000 overall limit and no further sends are accepted from
`P`, `SUB_A`, or `SUB_B` until the next billing period.
The parent account is responsible for picking sub-account limits that match how much capacity each sub-account is expected
to use.
## Set a limit
Limits can be managed programmatically, or via the [sub-accounts page](https://dash.mailchannels.com/sub-accounts).
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY (a parent-account API key) before running}"
: "${SUB_ACCOUNT_HANDLE:?Set SUB_ACCOUNT_HANDLE (the sub-account to set a limit on)}"
: "${SEND_LIMIT:?Set SEND_LIMIT (maximum sends per billing period, integer >= 0)}"
curl -X PUT \
"https://api.mailchannels.net/tx/v1/sub-account/$SUB_ACCOUNT_HANDLE/limit" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $PARENT_API_KEY" \
-d "{ \"sends\": $SEND_LIMIT }"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, SUB_ACCOUNT_HANDLE, SEND_LIMIT } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!SUB_ACCOUNT_HANDLE) throw new Error("Set SUB_ACCOUNT_HANDLE (the sub-account to set a limit on)");
if (!SEND_LIMIT) throw new Error("Set SEND_LIMIT (maximum sends per billing period, integer >= 0)");
const mailchannels = new MailChannels(PARENT_API_KEY);
const { success, error } = await mailchannels.subAccounts.setLimit(SUB_ACCOUNT_HANDLE, {
sends: Number(SEND_LIMIT),
});
if (!success) {
console.error("Set sub-account limit failed:", error);
process.exit(1);
}
console.log({ success });
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
handle = os.environ.get("SUB_ACCOUNT_HANDLE")
send_limit = os.environ.get("SEND_LIMIT")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not handle:
sys.exit("Set SUB_ACCOUNT_HANDLE (the sub-account to set a limit on)")
if not send_limit:
sys.exit("Set SEND_LIMIT (maximum sends per billing period, integer >= 0)")
mailchannels.api_key = parent_api_key
response = mailchannels.SubAccounts.Limits.set(handle, sends=int(send_limit))
print(response)
```
```php PHP theme={null}
subAccounts->limits->set($sub_account_handle, sends: (int) $send_limit);
print_r($response->toArray());
```
Set the following before running:
* `PARENT_API_KEY` — a parent-account API key.
* `SUB_ACCOUNT_HANDLE` — the sub-account being limited.
* `SEND_LIMIT` — maximum sends per billing period (integer, `0` or greater).
Setting `sends` to `0` effectively pauses the sub-account without suspending it.
## Get the current limit
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY (a parent-account API key) before running}"
: "${SUB_ACCOUNT_HANDLE:?Set SUB_ACCOUNT_HANDLE (the sub-account to read the limit for)}"
curl "https://api.mailchannels.net/tx/v1/sub-account/$SUB_ACCOUNT_HANDLE/limit" \
-H "X-Api-Key: $PARENT_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, SUB_ACCOUNT_HANDLE } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!SUB_ACCOUNT_HANDLE) throw new Error("Set SUB_ACCOUNT_HANDLE (the sub-account to read the limit for)");
const mailchannels = new MailChannels(PARENT_API_KEY);
const { data, error } = await mailchannels.subAccounts.getLimit(SUB_ACCOUNT_HANDLE);
if (error) {
console.error("Get sub-account limit failed:", error);
process.exit(1);
}
// A value of -1 means no explicit limit; the sub-account shares the parent's allocation.
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
handle = os.environ.get("SUB_ACCOUNT_HANDLE")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not handle:
sys.exit("Set SUB_ACCOUNT_HANDLE (the sub-account to read the limit for)")
mailchannels.api_key = parent_api_key
# A value of -1 means the sub-account has no explicit limit and is bounded
# only by the parent account's overall limit.
response = mailchannels.SubAccounts.Limits.retrieve(handle)
print(response)
```
```php PHP theme={null}
subAccounts->limits->retrieve($sub_account_handle);
print_r($response->toArray());
```
A response of `sends: -1` means no explicit limit has been set. The sub-account is bounded only by the parent account's
limit.
## Remove a limit
Removing the limit lets the sub-account use any remaining capacity in the parent account's allocation.
```bash cURL theme={null}
#!/usr/bin/env bash
set -u
: "${PARENT_API_KEY:?Set PARENT_API_KEY (a parent-account API key) before running}"
: "${SUB_ACCOUNT_HANDLE:?Set SUB_ACCOUNT_HANDLE (the sub-account whose limit you want to clear)}"
curl -X DELETE \
"https://api.mailchannels.net/tx/v1/sub-account/$SUB_ACCOUNT_HANDLE/limit" \
-H "X-Api-Key: $PARENT_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { PARENT_API_KEY, SUB_ACCOUNT_HANDLE } = process.env;
if (!PARENT_API_KEY) throw new Error("Set PARENT_API_KEY (a parent-account API key) before running");
if (!SUB_ACCOUNT_HANDLE) throw new Error("Set SUB_ACCOUNT_HANDLE (the sub-account whose limit you want to clear)");
const mailchannels = new MailChannels(PARENT_API_KEY);
const { success, error } = await mailchannels.subAccounts.deleteLimit(SUB_ACCOUNT_HANDLE);
if (!success) {
console.error("Delete sub-account limit failed:", error);
process.exit(1);
}
console.log({ success });
```
```python Python theme={null}
import os
import sys
import mailchannels
parent_api_key = os.environ.get("PARENT_API_KEY")
handle = os.environ.get("SUB_ACCOUNT_HANDLE")
if not parent_api_key:
sys.exit("Set PARENT_API_KEY (a parent-account API key) before running")
if not handle:
sys.exit("Set SUB_ACCOUNT_HANDLE (the sub-account whose limit you want to clear)")
mailchannels.api_key = parent_api_key
response = mailchannels.SubAccounts.Limits.delete(handle)
print(response)
```
```php PHP theme={null}
subAccounts->limits->delete($sub_account_handle);
print_r($response->toArray());
```
# Managing suppressions
Source: https://docs.mailchannels.com/email-api/suppression-api-actions
Learn how to manage suppressions to protect recipients and your domain reputation.
Suppression list entries can be managed via the [suppression list page](https://dash.mailchannels.com/suppression-lists)
or the API. To create a suppression list entry for all sub-accounts, use an API key that belongs to the parent account,
and set `add_to_sub_accounts` to `true`. Otherwise, **suppression list entries belong to the account of the API key used
to create them.**
## Create a suppression entry
```bash cURL theme={null}
#!/usr/bin/env bash
# Create a suppression entry for a recipient.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY (parent or sub-account) before running}"
: "${RECIPIENT:?Set RECIPIENT to the email address to suppress}"
# SUPPRESSION_TYPES is optional, a comma-separated list (e.g. "transactional,non-transactional").
# If unset, MailChannels uses the default "non-transactional".
TYPES_FIELD=""
if [ -n "${SUPPRESSION_TYPES:-}" ]; then
IFS=',' read -ra TYPES_ARR <<< "$SUPPRESSION_TYPES"
TYPES_JSON=""
for t in "${TYPES_ARR[@]}"; do
TYPES_JSON+=",\"$t\""
done
TYPES_FIELD=", \"suppression_types\": [${TYPES_JSON#,}]"
fi
curl -X POST https://api.mailchannels.net/tx/v1/suppression-list \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY" \
-d "{ \"suppression_entries\": [{ \"recipient\": \"$RECIPIENT\"$TYPES_FIELD }] }"
```
```javascript Node.js theme={null}
// Create a suppression entry for a recipient.
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY, RECIPIENT, SUPPRESSION_TYPES } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY (parent or sub-account) before running");
if (!RECIPIENT) throw new Error("Set RECIPIENT to the email address to suppress");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
// SUPPRESSION_TYPES is optional, a comma-separated list (e.g. "transactional,non-transactional").
// If unset, MailChannels uses the default "non-transactional".
const entry = { recipient: RECIPIENT };
if (SUPPRESSION_TYPES) {
entry.types = SUPPRESSION_TYPES.split(",").map((t) => t.trim()).filter(Boolean);
}
const { success, error } = await mailchannels.suppressions.create({ entries: [entry] });
if (!success) {
console.error("Create suppression failed:", error);
process.exit(1);
}
console.log("Suppression created");
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY (parent or sub-account) before running")
recipient = os.environ.get("RECIPIENT")
if not recipient:
sys.exit("Set RECIPIENT to the email address to suppress")
# SUPPRESSION_TYPES is optional, a comma-separated list (e.g. "transactional,non-transactional").
# If unset, MailChannels uses the default "non-transactional".
entry: dict = {"recipient": recipient}
types_env = os.environ.get("SUPPRESSION_TYPES")
if types_env:
entry["suppression_types"] = [t.strip() for t in types_env.split(",") if t.strip()]
response = mailchannels.Suppressions.create([entry])
print(response)
```
```php PHP theme={null}
$recipient];
$types_env = getenv('SUPPRESSION_TYPES');
if ($types_env) {
$entry['suppression_types'] = array_values(array_filter(array_map('trim', explode(',', $types_env))));
}
$response = $client->suppressions->create([$entry]);
print_r($response->toArray());
```
Set the following before running:
* `MAILCHANNELS_API_KEY` — a parent or sub-account API key.
* `RECIPIENT` — the email address to suppress.
* `SUPPRESSION_TYPES` — optional, a comma-separated list of suppression types to apply. If omitted, the default is `non-transactional`.
If both `transactional` and `non-transactional` are included, the recipient will not receive any mail. If only `non-transactional`
is included, the recipient will still receive necessary transactional messages.
## List suppression entries
```bash cURL theme={null}
#!/usr/bin/env bash
# Retrieve suppression entries for the account.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY (parent or sub-account) before running}"
curl -X GET "https://api.mailchannels.net/tx/v1/suppression-list" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { data, error } = await mailchannels.suppressions.list();
if (error) {
console.error("List suppressions failed:", error);
process.exit(1);
}
console.log(data);
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY (parent or sub-account) before running")
# Retrieve suppression entries for the account.
response = mailchannels.Suppressions.list()
print(response)
```
```php PHP theme={null}
suppressions->list();
print_r($response->toArray());
```
Set `MAILCHANNELS_API_KEY` before running. The list endpoint also accepts optional `recipient`, `source`, `created_before`,
`created_after`, `limit`, and `offset` query parameters for filtering and pagination.
## Delete a suppression entry
```bash cURL theme={null}
#!/usr/bin/env bash
# Delete a suppression entry for a recipient.
set -u
: "${MAILCHANNELS_API_KEY:?Set MAILCHANNELS_API_KEY (parent or sub-account) before running}"
: "${RECIPIENT:?Set RECIPIENT to the email address whose suppression entry should be removed}"
# SOURCE is optional. Defaults to "api" if unset. Use "all" to delete every entry for the recipient.
SOURCE_QUERY=""
if [ -n "${SOURCE:-}" ]; then
SOURCE_QUERY="?source=$SOURCE"
fi
curl -X DELETE "https://api.mailchannels.net/tx/v1/suppression-list/recipients/$RECIPIENT$SOURCE_QUERY" \
-H "X-Api-Key: $MAILCHANNELS_API_KEY"
```
```javascript Node.js theme={null}
import { MailChannels } from "mailchannels-sdk";
const { MAILCHANNELS_API_KEY, RECIPIENT } = process.env;
if (!MAILCHANNELS_API_KEY) throw new Error("Set MAILCHANNELS_API_KEY before running");
if (!RECIPIENT) throw new Error("Set RECIPIENT to the email address whose suppression entry should be removed");
const mailchannels = new MailChannels(MAILCHANNELS_API_KEY);
const { success, error } = await mailchannels.suppressions.delete(RECIPIENT);
if (!success) {
console.error("Delete suppression failed:", error);
process.exit(1);
}
console.log("Suppression deleted");
```
```python Python theme={null}
import os
import sys
import mailchannels
if not os.environ.get("MAILCHANNELS_API_KEY"):
sys.exit("Set MAILCHANNELS_API_KEY (parent or sub-account) before running")
recipient = os.environ.get("RECIPIENT")
if not recipient:
sys.exit("Set RECIPIENT to the email address whose suppression entry should be removed")
# SOURCE is optional. Defaults to "api". Pass "all" to delete every entry for the recipient.
source = os.environ.get("SOURCE")
response = mailchannels.Suppressions.delete(recipient, source=source)
print(response)
```
```php PHP theme={null}
suppressions->delete($recipient, $source);
print_r($response->toArray());
```
Set the following before running:
* `MAILCHANNELS_API_KEY` — the API key that owns the suppression entry.
* `RECIPIENT` — the email address whose suppression entry should be removed.
* `SOURCE` — optional, defaults to `api`. Pass `all` to delete every entry for the recipient regardless of source.
# Suppressions and unsubscribe handling
Source: https://docs.mailchannels.com/email-api/suppressions-unsubscribes
Learn how to use suppressions and unsubscribes to protect recipients and your domain reputation.
Suppression handling protects recipients and your domain reputation. Your application should decide when a recipient is
eligible for transactional mail, non-transactional mail, or no mail at all.
## Suppression types
MailChannels supports two suppression types:
| Type | Use it for |
| - | - |
| `transactional` | Block all mail to a recipient when you should not send any message, including operational mail. |
| `non-transactional` | Block marketing, lifecycle, newsletter, or promotional mail while still allowing necessary transactional messages. |
If you create a suppression entry without `suppression_types`, the default is `non-transactional`.
## Suppression sources
Suppression entries can come from:
* `api`
* `unsubscribe_link`
* `list_unsubscribe`
* `hard_bounce`
* `spam_complaint`
Use the source to separate recipient preference, automated bounce handling, and manual administrative actions.
## Non-transactional messages
Set `transactional` to `false` for non-transactional sends. Non-transactional messages must be DKIM signed and must have
exactly one recipient per personalization. MailChannels adds unsubscribe headers for non-transactional messages.
Do not mark marketing or promotional email as transactional to bypass suppression handling. That can create compliance
risk and damage sender reputation.
## Operational workflow
1. Import existing unsubscribes and hard bounces into the suppression list before cutover.
2. Store `hard-bounced`, `complained`, `unsubscribed`, and unsubscribe-related webhook events.
3. Update your application preference store from those events.
4. Check your own preference store before calling MailChannels.
5. Review suppression list entries by recipient or source when debugging.
# Symfony
Source: https://docs.mailchannels.com/email-api/symfony
Send email through Symfony Mailer using the MailChannels transport plugin.
The `mailchannels/mailchannels-php` package ships a [Symfony Mailer](https://symfony.com/doc/current/mailer.html) transport.
If you already have a Symfony application that builds `Symfony\Component\Mime\Email` objects and sends them through
`MailerInterface`, you can switch to MailChannels with just a few lines of configuration.
This plugin is optional and lives alongside the rest of the PHP SDK. The plugin itself can only send mail.
It doesn't implement the full API surface (sub-accounts, keys, webhooks, etc.). For those features, use the SDK's `Client` directly.
## Prerequisites
Before you send your first message, follow the [PHP quickstart](/email-api/php/quickstart#prerequisites) prerequisite section.
This will walk you through creating an account, generating an API key, and adding the required Domain
Lockdown and SPF DNS records.
## Installation
Install the SDK and the Symfony Mailer component:
```bash theme={null}
composer require mailchannels/mailchannels-php symfony/mailer
```
The SDK also needs a PSR-18 HTTP client and PSR-17 factories. Symfony HTTP Client + Nyholm PSR-7 is a natural fit for a
Symfony project (Guzzle works too, but install one or the other, not both):
```bash theme={null}
composer require symfony/http-client nyholm/psr7
```
### Configuring Symfony
Symfony discovers mailer transports through tagged services. Register the factory in `config/services.yaml`:
```yaml config/services.yaml theme={null}
services:
MailChannels\Plugins\Symfony\Transport\MailChannelsTransportFactory:
tags: ['mailer.transport_factory']
```
See Symfony's docs on [custom transport factories](https://symfony.com/doc/current/mailer.html#custom-transport-factories)
for background on how this mechanism works.
Set the `MAILER_DSN` environment variable, in your `.env` file or your environment:
```bash .env theme={null}
MAILER_DSN=mailchannels+api://${MAILCHANNELS_API_KEY}@default
```
* Scheme: `mailchannels` or `mailchannels+api` (both accepted).
* DSN user: your MailChannels API key.
* DSN host: leave as `default` for the production API endpoint.
See Symfony's [transport setup](https://symfony.com/doc/current/mailer.html#transport-setup) docs for more on the DSN
format and how it's typically wired up per-environment.
## Sending mail
Once configured, send mail through the standard `MailerInterface`. No MailChannels-specific code is required for a
basic send:
```php theme={null}
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class YourService
{
public function __construct(private readonly MailerInterface $mailer) {}
public function sendWelcomeEmail(): void
{
$email = (new Email())
->from('sender@example.com')
->to('recipient@example.com')
->subject('Welcome to MailChannels')
->text('Plain-text body.')
->html('