Sending Transactional Email
Send password resets, receipts and other one-to-one email from your application, with every option on a send explained by task.
Transactional email is mail your application sends to one person because of something they did. Everything on this page goes through one endpoint:
A send answers four questions: who gets it, what they get, which lane it travels on, and how to retry safely. The sections below take them in that order. If you have not sent anything yet, start with the Quickstart.
A complete example
This sends the password-reset template to Ada on the transactional lane, and is safe to retry:
curl -X POST https://api.mailcrate.io/public/sending/mail \
-H "Authorization: Api-Key $MAILCRATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contactEmail": "ada@example.com",
"contactId": "user_8231",
"templateId": "password-reset",
"lane": "transactional",
"data": {
"resetUrl": "https://app.example.com/reset/8f2c",
"expiresInMinutes": 30
},
"idempotencyKey": "password-reset-8f2c"
}'Who: the recipient
Every email is sent to a contact, so it shows up on that person's timeline in Mailcrate. Name the contact in one of two ways:
| Field | What it does |
|---|---|
contactEmail | Finds the contact with this address, or creates it if there is none. |
contactId | Finds the contact by Mailcrate id, or by your own id for them (the contact's external identifier). Never creates one on its own. |
Send both when you have both. Mailcrate looks the contact up by contactId first, then by email, and creates it with your id attached if neither matches. That keeps the contact linked to the user in your system, even if they change their address later. If the email already belongs to a contact with a different external identifier, the send is refused with 409 and code CONTACT_EXTERNAL_ID_MISMATCH.
Add contactProperties to update the contact in the same call. Each key is written to the contact's custom properties, and the template can read them as {{ contact.properties.<key> }}:
{
"contactEmail": "ada@example.com",
"contactProperties": { "firstName": "Ada", "plan": "pro" },
"templateId": "welcome",
"lane": "transactional"
}What: the content
A send takes its content from exactly one of three sources. Pick one:
| Source | Send it with | Good for |
|---|---|---|
| Template | templateId + data | Most transactional email. Designed in Mailcrate, filled with your data at send time. |
| Transactional message | transactionalMessageId + data | The same, plus a page in the app with its own stats and delivery history. |
| Inline HTML | html + subject + fromAddress | HTML your own system already rendered. |
Templates
templateId is the template's slug (such as password-reset) or its id. data is an object whose keys the template reads as variables: {"resetUrl": "…"} fills {{ resetUrl }}. The template supplies the subject, preview text, sender, reply-to and BCC from its settings. Send with a Template walks through building one.
Transactional messages
A transactional message is a named email in the app (Transactional in the sidebar) with a template attached. Sending through one works like sending the template, with three differences:
- It defaults to the transactional lane, so you cannot forget
lane. - Its deliveries are grouped on the message's page, with delivery and engagement stats.
- The email never carries an unsubscribe link:
{{ unsubscribe_url }}stays empty.
{
"contactEmail": "ada@example.com",
"transactionalMessageId": "3de943a1-4861-4e44-9f67-88b30caf7a6f",
"data": { "resetUrl": "https://app.example.com/reset/8f2c" }
}The message id is on its API Integration tab. See Transactional Messages for setting one up in the app.
Inline HTML
Send your own HTML when another system renders the email. subject and fromAddress are required, preHeaderText is optional:
{
"contactEmail": "ada@example.com",
"fromAddress": "Acme Billing <billing@acme.com>",
"subject": "Your invoice for October",
"preHeaderText": "Invoice INV-2041, due November 14",
"html": "<p>Hi Ada, your invoice is attached.</p>",
"lane": "transactional"
}Inline HTML is sent exactly as you send it. Merge tags like {{ name }} are not filled in and data is ignored, so render the personal parts yourself.
Which fields apply to which source
Not every field works with every source. A field marked "template" comes from the template's settings, and the value in the request is ignored.
| Field | Template | Transactional message | Inline HTML |
|---|---|---|---|
data | Yes | Yes | Ignored |
subject, preHeaderText | Template | Template | Yes (subject required) |
fromAddress | Optional override | Template | Required |
replyTo, bcc | Template | Template | Yes |
cc | Ignored | Ignored | Yes |
attachments | Yes | Yes | Yes |
fromAddress must be one of your workspace's verified sender identities. It can carry a display name ("Acme <hello@acme.com>"), but the name shown to recipients is the sender identity's own.
Which lane
Set "lane": "transactional" on every email the recipient asked for. A send by templateId or html without it uses the marketing lane, which skips anyone who unsubscribed from your marketing. Lanes explains the difference in full.
To let people opt out of one kind of optional mail without unsubscribing from everything, add subscriptionGroupId. Recipients who opted out of that group are skipped on either lane.
Retrying safely
Networks fail, and a retry must not send a customer two password resets. Add an idempotencyKey, any string up to 255 characters that identifies this one email in your system:
{ "idempotencyKey": "password-reset-8f2c" }A second request with the same key, within the same workspace, sends nothing and returns the delivery from the first request. Concurrent duplicates are handled too: if two requests with one key race, both get the same delivery back.
Build the key from something that is unique to the email, such as the reset token or the order id. Do not use a random value generated per attempt, because then every retry gets a new key.
Attachments
Attach up to 20 files, base64-encoded:
{
"attachments": [
{
"filename": "invoice-2041.pdf",
"contentType": "application/pdf",
"content": "JVBERi0xLjcKJeLjz9MK…"
}
]
}Each file can be up to 10 MB and all of them together up to 25 MB, measured after decoding. Executable types (.exe, .bat, .js, .msi and similar) are refused. contentType defaults to application/octet-stream.
Holding a send for approval
Set "requiresApproval": true to have a person check an email before it goes out, for example one written by an AI agent. The response then carries an approvalUrl; the email waits with status pending-approval until someone opens that link and approves it.
Replying to inbound mail
When Mailcrate receives email for you, reply to a received message by passing its id as inReplyToInboundMessageId. The reply is threaded under the original in the recipient's mail client, and their next reply comes back to Mailcrate.
The response
A 200 means Mailcrate accepted the request and created a delivery:
{
"id": "6f1c2a9e-3b7d-4c55-9a1e-2d8f0b4c7e11",
"status": "created",
"lane": "transactional"
}| Field | Meaning |
|---|---|
id | The delivery id. Look it up with GET /public/delivery/<id> (scope delivery:read) and its events with GET /public/delivery/<id>/events. |
status | Where the email stood when the response was written. suppressed means it will not be sent. |
lane | The lane the send used. |
suppressionReason | Present when status is suppressed. See Lanes. |
approvalUrl | Present when you set requiresApproval. |
To follow an email after the response, subscribe a webhook to the delivery events (delivery.sent, delivery.delivered, delivery.bounced, delivery.complained, delivery.suppressed) instead of polling.
Errors
Errors use the standard error envelope. The ones a send can produce:
| Status | Message | What to do |
|---|---|---|
400 | A field failed validation, for example html must be provided when transactionalMessageId and templateId are missing. | Fix the request. errors names each field. |
400 | Sender identity is not verified. | Finish verifying the sender under Settings → Mailing. |
400 | Attachment at index 0 … | Check the attachment's size, type and base64. |
403 | sending-paused | Sending is paused on the workspace. Contact support. |
404 | Email template "…" not found. | Check the slug or id, and that the key's workspace has the template. |
404 | Sender identity not found. | The fromAddress, or the template's sender, is not a sender identity in this workspace. |
404 | Contact not found and email not provided to create a new contact. | Send contactEmail, or a contactId that exists. |
409 | CONTACT_EXTERNAL_ID_MISMATCH | The email belongs to a contact with a different external id. |
429 | daily-send-limit-reached / monthly-send-limit-reached, or RATE_LIMITED | Wait for retryAfterSeconds or Retry-After. See Rate Limits. |
A 400 or 404 will fail the same way every time, so do not retry it unchanged. A 429 or a 5xx is worth retrying with the same idempotencyKey.
Sending many at once
To send up to 500 emails in one request, use Batch Sending.