Transactional email
Transactional email is the email your app sends. It goes out from a subdomain of your domain, such as notify.example.com, and replies go to a Shipmail mailbox you choose. The Transactional section of the dashboard shows what happened to each message.
Last updated
How it differs from sending from a mailbox
When your app sends through the API from a mailbox address, each email lands in that mailbox's Sent folder next to your team's conversations. A transactional sender keeps your transactional email apart:
- It sends from its own subdomain, so receiving providers can tell your transactional email from the mail your team writes. Adding it does not change mail routing on your main domain. Sending health and limits apply across your organization.
- Sent copies are kept in separate transactional email storage, not in the mailbox. They do not appear in its folders, search or email apps.
- Replies arrive in the mailbox as normal email, in their own conversations.
- Activity lists accepted sends with their delivery status and history.
A sender needs no extra mailbox and costs nothing extra. Solo includes 3,000 transactional emails a month, Pro 15,000, Team 30,000 and Scale 50,000, then $0.30 per 1,000, with no tiers. One transactional email is one recipient and up to 1 MB including attachments and encoding. A larger email counts once per 1 MB, rounded up. The largest allowed is 10 MB. Mailbox emails and newsletters are counted apart. Extra sending is off until the owner allows it in Billing; until then, these emails are refused at the included amount until next month. An organization created before these prices were published keeps its current rule until its owner allows extra sending: transactional email shares the plan's monthly sends.
Set up a sender
Owners and Admins with access to all mailboxes can add sending subdomains. Owners and Admins can manage the senders of the mailboxes they can read. Both happen under Transactional in the dashboard. Before you start, you need a domain already verified in Shipmail and an active mailbox for replies.
Choose a sending subdomain
Pick one of your verified domains and a subdomain label, such as
notify. It has to be a subdomain, never the domain itself. Mail routing on your main domain does not change.Add DNS records
The dashboard lists the three records, with steps for your DNS provider. If your DNS provider supports one-click setup, you'll see an Add records at [your provider] button. You can also open the records and add them yourself.
Share a link creates a link to the DNS records that stays valid for 14 days. Send it to whoever manages your DNS.
Shipmail keeps checking in the background, so you can leave the page. DNS usually updates in a few minutes, sometimes up to an hour. Once every record is verified, setup moves to the next step.
Choose your From address
Choose the From address on the subdomain, the sender name recipients see, and the mailbox that gets replies. If you have several active mailboxes, select one before creating the sender. That mailbox's address becomes the sender's Reply-To.
Send a test email
Send a test to me sends a short email from the selected sender to your own account address. The dialog shows both addresses before you send. It takes the same path as your app's email and counts as one live send. Then connect your app.
A sender named Acme on notify.example.com, with replies going to the support mailbox, sends as Acme <notifications@notify.example.com> with Reply-To support@example.com.
To get code for your app, choose Connect beside the sender. Its example is filled in with that sender and its reply mailbox. A test from the dashboard only proves the sending path works. Your app still has to send through the API itself.
DNS records
The examples below use notify.example.com. Copy the exact values from the dashboard. Your root domain's MX records are not touched.
| Type | Host | Value | What it does |
|---|---|---|---|
| TXT | shipmail._domainkey.notify.example.com | The DKIM key shown in the dashboard | Signs your transactional email so receiving servers can check it came from you. |
| MX | mail.notify.example.com | 10 feedback-smtp.eu-central-1.amazonses.com | Receives bounce reports for your transactional email. It is not an inbox, and replies never arrive here. |
| TXT | mail.notify.example.com | v=spf1 include:amazonses.com -all | Authorizes Shipmail to send with this subdomain as the return path. |
Shipmail does not ask for a DMARC record on the subdomain. A subdomain follows the DMARC policy of its parent domain unless it publishes its own, so the policy you already have on your root domain keeps working.
If a verified sending subdomain loses verification, the organization owners get an alert. Restore its DNS records and verify it again before you send. You get one alert per outage, however many checks fail. Earlier sends stay in Activity.
Remove a sending subdomain
Owners and Admins with access to all mailboxes can remove a sending subdomain, for example one added with a typo. Open its menu under Domains on the Transactional page and choose Remove domain. During setup, choose Remove beside the subdomain, before you add the corrected one. Shipmail stops sending from it and deletes its sender identity and DKIM key. You can then delete its DNS records at your DNS provider, and add the subdomain again later.
A domain that has from addresses cannot be removed; contact support to remove it. If removal stops part-way, the domain shows that it did not finish; remove it again to complete it.
Replies
Every email from a sender carries the Reply-To of its mailbox, so a recipient's reply goes to that mailbox. Shipmail moves the reply into a Transactional folder as it arrives and marks it Transactional in the message list. Automatic replies, such as out-of-office messages, arrive already read. Replies that look like spam go to Junk.
Replies from people notify you like Inbox mail. Turn this off in Settings > Notifications, under Transactional email. Automatic replies never notify.
If one of your mailbox rules moves a reply, it stays where the rule put it.
Each send's page in Activity lists its replies, with a link to the conversation.
The sending subdomain has no mailbox. Email sent straight to notifications@notify.example.com bounces back to its sender.
Replies arrive through Reply-To. The sender address itself can't receive email, so don't publish it where people might write to it.
Send through the API
Use POST /v1/messages with an API key that can send from the sender's mailbox. Existing keys work, so there is nothing to rotate. Set from to the sender address.
curl -X POST https://shipmail.to/api/v1/messages \
-H "Authorization: Bearer sm_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "notifications@notify.example.com",
"to": ["customer@example.com"],
"subject": "Your order has shipped",
"text": "Your order is on its way."
}'A sender changes a few fields of the send request:
fromis the bare sender address. Shipmail adds the sender name. A different name inName <address>form is refused.mailbox_idis optional. If you send it, it must be the sender's mailbox.reply_tois optional and set for you. If you send it, it must be the sender's Reply-To address.- Sends go out immediately.
scheduled_atandtrack_reply: trueare refused. - Attach files with
attachments. An email can be up to 10 MB, attachments and encoding included.staged_attachment_idsis not supported yet. - The returned message has
conversation_idandthread_idset to null, because transactional email is not part of a mailbox conversation. POST /v1/messages/:id/replydoes not accept a transactional email. Send a new message instead.
A test API key simulates the send. Nothing is delivered, and the send appears under Test in Activity. Test keys also accept sandbox_outcome to simulate a delivery, a bounce or a complaint. See Sandbox mode.
A retry with the same Idempotency-Key returns the message from the first request, even if the sender was turned off in between.
To connect your app with a coding agent, paste this prompt into Claude Code, Codex, Cursor or another agent in your app's repository. If your app sends through another provider today, follow Move transactional email instead.
Send this app's transactional email through Shipmail.
Read https://shipmail.to/docs/features/transactional-email and https://shipmail.to/llms.txt first.
1. Add a sendEmail helper that calls POST https://shipmail.to/api/v1/messages.
2. Read the API key from SHIPMAIL_API_KEY and the sender address from EMAIL_FROM.
3. Set from to the bare sender address.
4. When Shipmail refuses a send, throw an error that includes details[].code.
5. Use the helper everywhere the app sends email.
6. Run the tests and list what you changed.Framework guides: Next.js, Express, Cloudflare Workers, Django, FastAPI, Flask, Rails and Laravel.
Templates
A template stores a subject and body with variables such as {{name}}, so your code sends a template ID and the values for one recipient instead of the whole email. Owners and admins manage templates under Transactional, then Templates.
- The editor shows a live preview. Give each variable a fallback to make it optional; a variable without one must be passed on every send.
- Save draft keeps your changes without affecting sends. Publish makes the draft the next version, and sends use the published version.
- Version history lists every published version. Restore copies one back into the draft; publish it to send it again.
- The template page shows its ID and a send snippet to copy into your code.
{
"from": "hello@example.com",
"to": ["ada@example.com"],
"template": { "id": "tpl_123", "variables": { "name": "Ada" } }
}A send with a template needs only messages:send, and the sender rules above still apply. Values are HTML-escaped in the HTML body. See the Templates API for the endpoints, variables, versions and errors.
Errors
Errors use the standard error envelope. The reason is in details[].code. Branch on the code, not on the message.
| Code | Status | When |
|---|---|---|
| transactional_sender_unavailable | 403 | The sender is turned off, the API key cannot send from the sender's mailbox, or mailbox_id names a different mailbox. Every case returns the same response. |
| transactional_sender_immediate_only | 422 | The request includes scheduled_at. |
| transactional_sender_no_reply_tracking | 422 | The request sets track_reply to true. |
| transactional_sender_no_signature | 422 | The request sets add_signature to true. |
| transactional_sender_reply_to | 422 | reply_to is set to an address other than the sender's Reply-To. |
| transactional_sender_staged_attachments | 422 | The request includes staged_attachment_ids. Send the files in attachments instead. |
| transactional_sender_display_name | 422 | from carries a display name that differs from the sender's name. |
| recipient_disposable | 422 | The sender refuses temporary email addresses and a recipient uses one. One entry per address. |
| application_daily_limit | 429 | The organization reached its transactional daily limit: the safety limit or its plan-share floor, or an approved limit. |
| application_unique_recipients | 429 | Transactional email reached its hourly limit of different recipients. |
| sending_capacity | 429 | Shipmail is at its sending capacity for transactional email. Try again in a few minutes. |
| application_storage_full | 429 | The sender's mailbox has no room left in its transactional email storage for this message. |
| transactional_monthly_limit | 403 | The organization is metered in emails, the month's included amount is reached, and extra sending is off or cannot be billed. |
| transactional_payment_failed | 403 | The organization is metered in emails, the included amount is reached, and an invoice for extra email is unpaid. |
| transactional_budget_reached | 403 | Extra email reached the month's spending limit. Raise the limit in Settings > Billing. |
| transactional_reply_unsupported | 422 | POST /v1/messages/:id/reply was called on a transactional email. |
A 409 conflict with no code means the Idempotency-Key belongs to a message this API key cannot see. Use a new key.
Limits
Email from a transactional sender, through the API or an SMTP credential, has its own daily limit. Email your app sends from a mailbox address keeps the mailbox rules in the sending policy.
Safety limit
On a paid plan, outside a trial, your organization starts with a safety limit of 1,000 recipients a day. It counts accepted recipients (To, Cc and Bcc), and all your senders share it: creating another sender does not reset it.
Once a day, Shipmail raises the limit to twice your highest clean day of the last 7 days, when those 7 days had:
- at least 500 recipients in all
- at least 3 days above half the limit
- hard bounces under 2%
- spam complaints under 0.08%, counted on the 7 days that ended 2 days earlier, so late complaints arrive
Automatic raises stop at 25,000 a day. Above that, a person approves the limit. The limit never goes down on its own. A bad day holds it: a day with hard bounces at 2% or more, or a delayed day with complaints at 0.08% or more, once the week had at least 500 recipients. The limit stays where it is, raises stop, and a person reviews your sending before clearing the hold.
Plan floor
Your plan keeps its earlier share as a floor: 80% of the plan's daily limit (mailbox emails a month divided by 30), counted in sends. A send goes through while either the safety limit or the floor has room.
| Plan | Mailbox emails a month | Daily floor |
|---|---|---|
| Solo | 20,000 | 533 |
| Pro | 40,000 | 1,067 |
| Team | 120,000 | 3,200 |
| Scale, 13 mailboxes | 130,000 | 3,467 |
| Solo (legacy) | 30,000 | 800 |
| Pro (legacy) | 100,000 | 2,667 |
| Team (legacy) | 500,000 | 13,333 |
Approvals, trials and throttling
- Approval: an active sending approval replaces the safety limit and the floor with its own daily limit.
- Trials: during a trial, and on the free plan, transactional email may use at most 80% of the organization's daily limit, including the warm-up for new organizations and new sending domains, and counts toward that daily limit. The trial's 300 sends still apply.
- Throttling: high bounce or complaint rates halve both the safety limit and the floor.
- Hourly: different recipients per hour, up to half the daily limit, using the larger of the two rules. It is never below 200 and never above the daily limit.
On a paid plan, transactional email does not use the organization's daily limit for mailbox sending, and the warm-up for new organizations and new sending domains does not apply to it. It counts toward your included transactional emails, not your mailbox emails. An organization created before these prices were published keeps its current rule until its owner allows extra sending: transactional email shares the plan's monthly sends. The daily count resets at midnight UTC and the hourly count at the start of each UTC hour.
Shipmail also keeps a share of its own sending capacity for mailbox email. When transactional email across all customers reaches it, a send is refused with 429 sending_capacity. Try again in a few minutes.
Moving from another provider
If your app already sends more than the safety limit elsewhere, open the Transactional page and request a migration review with your last 30 days: volume, bounce and complaint rates, sending domains and expected peak. A person answers within one business day. The answer is a sending approval, and the limit approved may be lower than the one you ask for.
The Transactional page shows your monthly plan allowance, daily capacity, and hourly recipient count. Each counter shows its own reset time. If Shipmail can't read a counter, the page says its usage is unavailable.
Storage
Each mailbox that receives replies for a sender includes transactional email storage for sent copies. It is not charged separately, does not use a mailbox slot on your plan, and does not take space from the mailbox itself.
Shipmail keeps each sent copy for 30 days, then deletes its body and attachments. Delivery details, history and references stay. The storage is 1 GB for every 300 emails of your transactional daily limit (the safety limit or its plan floor, or an approved limit), and never less than 10 GB, so it grows with your limit. Organizations created before 30-day retention shipped in November 2026 keep 2 GB and their copies until they delete them; contact support to switch to 30-day retention. Copies sent before the switch keep the old rule.
Senders linked to the same reply mailbox share this allocation. The Transactional page groups their storage usage by reply mailbox. The copies are not stored in that mailbox's folders.
When the storage is full, new sends are refused with application_storage_full until you free space. To free space, open a send in Activity and choose Delete content. This deletes the body and attachments for good. Delivery details, history and references stay. You can delete the content once the send was sent or failed, not while it is still processing.
Activity
Activity lists accepted transactional sends with their recipient, subject, full sender, status, reference and time. Filter by sender, recipient, status and period, and switch between live and test sends. Members see Activity for the mailboxes they can access. Open a send to see its content, its event history and the reason for a failure.
Use Exact lookup to find a message ID or client reference. The lookup keeps the selected live or test mode and your mailbox access. Select Issues to see bounced, complained, failed, suppressed and uncertain sends together.
Refresh Activity to fetch the latest results. Open a send to move between the current results or copy its link. Delivery history includes the date of each event. Recipient outcomes use the latest delivery update for each recipient.
The customer panel beside a conversation also lists the last five sends to the sender, with the outcome for their address. See Reply times and customers.
Some requests fail before Shipmail creates a message. These do not appear in Activity. Open Developer > Requests to inspect recorded refusals for limits, storage, sending restrictions or an unavailable sender. Request logs count recorded attempts. Shipmail samples repeated rate-limit refusals so a retry loop does not fill the log. Several retries of one request are not several failed emails. The logs are best effort and do not replace your app's error handling. Shipmail keeps request logs for 30 days.
Sends blocked because a recipient is on your suppression list show in Activity as Suppressed. If an earlier email to that address bounced, an administrator can remove the address from the suppression list on the send's page, or from the email itself, when that address writes to you.
| Status | Meaning |
|---|---|
| Queued | Shipmail accepted the email and is processing it. |
| Not confirmed | The email was handed off, but the handoff has not been confirmed yet. It may still be delivered. Do not send it again until the status settles. |
| Sent | The email was handed off for delivery. |
| Delivered | The receiving server accepted the email. This does not confirm that it reached the inbox rather than a spam folder. Email to a Shipmail mailbox is delivered when it lands in that mailbox. |
| Bounced | The receiving server rejected the email. |
| Spam complaint | The recipient marked the email as spam. |
| Suppressed | Not sent, because the recipient is on your suppression list after an earlier bounce or complaint. |
| Failed | The email was not sent. |
For Owners and Admins with access to all mailboxes, the Transactional page also shows your sending reputation: whether Shipmail is limiting your sending, and your bounce and spam complaint rates for the last 7 days against the limits in the sending policy. The rates count all email your organization sends, not only transactional email.
Turn a sender off
Owners and Admins can turn a sender off and back on for the mailboxes they can read from the Transactional page. New sends stop immediately: API sends from its address are refused with transactional_sender_unavailable. Sends the API accepted in the minute before can still go out. Replies to earlier email still reach the mailbox. The address stays reserved for the sender, and Shipmail never sends from it as an ordinary mailbox address.
Turning a sender off stops only that sender. Your other senders and your team's mailboxes keep sending. If an API key leaks, turn off the senders it can send from, then revoke the key in Developer > API keys.
Change where replies go
Open the sender's menu on the Transactional page and choose Change where replies go. Pick another mailbox. The sender keeps its address, display name and settings, and new sends carry the new mailbox's Reply-To. You need to be able to manage both mailboxes.
Sends made before the change stay with the old mailbox, with their copies and activity. API sends that pass the old mailbox's mailbox_id, or use an API key limited to the old mailbox, are refused with transactional_sender_unavailable until you update the app. Sends that set reply_to to the old mailbox's address are refused with transactional_sender_reply_to. Set it to the new address or leave it out. Other sends can be refused for up to a minute while the change applies. Once nothing else depends on the old mailbox, you can delete it, but that also deletes the earlier sends and their activity.
Refuse temporary email addresses
Open a sender's menu on the Transactional page, choose Settings, and turn on Refuse temporary email addresses. Owners and Admins can change it for the mailboxes they can read. It is off for a new sender.
When it is on, Shipmail checks the domain of every recipient in to, cc and bcc against a public list of throwaway email domains, such as Mailinator, including their subdomains. If any recipient matches, the whole send is refused with a 422 and one details[] entry per address, with field set to recipients and code set to recipient_disposable. Nothing is sent and nothing is added to Activity, so a retry with the same Idempotency-Key sends once the setting is off. Sends in test mode are checked too.
The setting applies to that sender only. Your mailboxes and your other senders are not checked.
SMTP
Apps that send over SMTP, such as Supabase Auth, can send as a sender without code changes. Give the app an SMTP credential: open the sender's menu on the Transactional page and choose SMTP credentials, or use POST /v1/smtp-credentials with the mailbox_credentials:write scope. Shipmail shows the password once. Owners and Admins who manage the sender's mailbox can create and revoke credentials, after a fresh sign-in.
| Server | mx1.shipmail.to |
| Port | 465 with TLS, or 587 with STARTTLS |
| Username | The credential's username, smtp-…@txsmtp.shipmail.to |
| Password | Shown once, when you create the credential |
| Sender | The sender address, for example notifications@notify.example.com |
A message sent with a credential is handled like an API send from the sender:
- The credential sends only as its sender. The envelope sender (
MAIL FROM) and theFromheader must both be the sender address; anything else is refused with550. The credential cannot read mail or send as any other address, including the sender's mailbox. - It goes through the same checks, limits, suppression list, storage and Activity as
POST /v1/messages, and counts as transactional email. - Shipmail sets the sender name and Reply-To. A name your app sets is replaced, and a different
Reply-Tois refused. - Recipients come from the SMTP envelope. Addresses in To and Cc keep their place, and the other envelope recipients are sent as Bcc. Each message needs at least one To recipient, and the API's limits on recipients, size and attachments apply.
- A message your app submits again with the same
Message-IDis sent once. - A limit that lifts with time, such as the daily limit, answers
451so your app retries later. A message that can never be sent answers550with the reason. - A sender can have 5 credentials. Revoking one stops its logins at once. A sender that is turned off keeps its credentials, but they cannot send.
- A credential acts for the person who created it. Once they leave the organization or lose access to the sender's mailbox, its messages are refused; create a new one.
- Inline images referenced with
cid:arrive as plain attachments. Link images hosted on the web instead, as most sign-in templates do.
Supabase Auth. In the Supabase dashboard, open Authentication, then the SMTP settings, and turn on custom SMTP. Set the sender email to the sender address, the host to mx1.shipmail.to, the port to 465, and the username and password to those of the credential. For local development with the Supabase CLI, set the same values in supabase/config.toml:
[auth.email.smtp]
enabled = true
host = "mx1.shipmail.to"
port = 465
user = "smtp-smtpc_...@txsmtp.shipmail.to"
pass = "env(SHIPMAIL_SMTP_PASSWORD)"
admin_email = "notifications@notify.example.com"
sender_name = "Acme"To check a credential from a terminal, send yourself a message with curl:
cat > message.txt <<'MAIL'
From: notifications@notify.example.com
To: you@example.com
Subject: SMTP test
It works.
MAIL
curl --ssl-reqd --url smtps://mx1.shipmail.to:465 \
--user 'smtp-smtpc_...@txsmtp.shipmail.to:YOUR_PASSWORD' \
--mail-from notifications@notify.example.com \
--mail-rcpt you@example.com \
--upload-file message.txt