MailcrateDocs

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:

POST/public/sending/mailscope · sending:write

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:

Terminal
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:

FieldWhat it does
contactEmailFinds the contact with this address, or creates it if there is none.
contactIdFinds 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:

SourceSend it withGood for
TemplatetemplateId + dataMost transactional email. Designed in Mailcrate, filled with your data at send time.
Transactional messagetransactionalMessageId + dataThe same, plus a page in the app with its own stats and delivery history.
Inline HTMLhtml + subject + fromAddressHTML 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.

FieldTemplateTransactional messageInline HTML
dataYesYesIgnored
subject, preHeaderTextTemplateTemplateYes (subject required)
fromAddressOptional overrideTemplateRequired
replyTo, bccTemplateTemplateYes
ccIgnoredIgnoredYes
attachmentsYesYesYes

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:

Response
{
  "id": "6f1c2a9e-3b7d-4c55-9a1e-2d8f0b4c7e11",
  "status": "created",
  "lane": "transactional"
}
FieldMeaning
idThe delivery id. Look it up with GET /public/delivery/<id> (scope delivery:read) and its events with GET /public/delivery/<id>/events.
statusWhere the email stood when the response was written. suppressed means it will not be sent.
laneThe lane the send used.
suppressionReasonPresent when status is suppressed. See Lanes.
approvalUrlPresent 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:

StatusMessageWhat to do
400A field failed validation, for example html must be provided when transactionalMessageId and templateId are missing.Fix the request. errors names each field.
400Sender identity is not verified.Finish verifying the sender under Settings → Mailing.
400Attachment at index 0 …Check the attachment's size, type and base64.
403sending-pausedSending is paused on the workspace. Contact support.
404Email template "…" not found.Check the slug or id, and that the key's workspace has the template.
404Sender identity not found.The fromAddress, or the template's sender, is not a sender identity in this workspace.
404Contact not found and email not provided to create a new contact.Send contactEmail, or a contactId that exists.
409CONTACT_EXTERNAL_ID_MISMATCHThe email belongs to a contact with a different external id.
429daily-send-limit-reached / monthly-send-limit-reached, or RATE_LIMITEDWait 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.

On this page