Send Transactional Email from n8n Workflows with MailAnvil
n8n is where a lot of teams run their operational glue: order webhooks in, Slack alerts out, database writes in between. Email usually belongs in that same chain — a customer signs up, an invoice becomes due, a payment lands — and the workflow that already knows about the event should be the thing that sends the notification.
You don't need a dedicated n8n "email node" for that. MailAnvil is a plain REST API, and n8n's HTTP Request node speaks plain REST better than almost any integration layer. This tutorial covers the whole path: credentials, a basic send, a webhook-triggered send with template variables, error handling, and delivery-status checks.
What you need
- An n8n instance (cloud or self-hosted — same everywhere)
- A MailAnvil account with a verified sending domain (app.mailanvil.com → Domains)
- A MailAnvil API key (keys start with
re_; treat them like passwords)
Step 1: Store the API key in n8n credentials
Never paste the key directly into node fields. In n8n:
- Go to Credentials → New → Header Auth
- Name:
Authorization - Value:
Bearer re_your_key_here
Save it as MailAnvil API. Every HTTP Request node in this tutorial references that credential, so rotating the key later is a one-place change.
Step 2: A basic send with the HTTP Request node
Add an HTTP Request node with these settings:
| Field | Value |
|---|---|
| Method | POST |
| URL | https://api.mailanvil.com/v1/send |
| Authentication | Generic Credential Type → Header Auth → MailAnvil API |
| Body Content Type | JSON |
Body (JSON, switch off "Send Body as JSON Parameters" and use raw JSON):
{
"from": "[email protected]",
"to": ["{{ $json.customer_email }}"],
"subject": "Your order is confirmed",
"html": "<p>Order <strong>{{ $json.order_id }}</strong> is confirmed. Total: {{ $json.total }}.</p>",
"text": "Order {{ $json.order_id }} is confirmed. Total: {{ $json.total }}."
}
n8n expressions ({{ }}) are evaluated before the request leaves, so the JSON body is already concrete when MailAnvil receives it.
A successful call returns 202 Accepted:
{
"id": "em_01JABC...",
"state": "queued"
}
202 means MailAnvil accepted the message into the queue — it is not yet a delivery confirmation. More on that in Step 5.
Step 3: Webhook-triggered send (the common pattern)
The realistic n8n shape is: a webhook fires, some nodes enrich the payload, then email goes out.
Webhook (POST /webhook/order-paid)
→ Set node: normalize fields
→ HTTP Request: MailAnvil send
→ IF node: response status == 202
true → Slack: "receipt sent"
false → Error path (Step 4)
Set node output example:
{
"customer_email": "[email protected]",
"order_id": "INV-2026-0042",
"total": "Rp 249.000",
"product": "Pro plan (annual)"
}
The HTTP Request node body stays the same as Step 2 — it just reads from the Set node output.
Step 4: Error handling — don't sleep through a 429
MailAnvil rate-limits with 429. n8n's HTTP Request node has a built-in Retry On Fail option — turn it on, set 3 tries, 5-second wait. For anything you cannot afford to lose, also add an On Error: Continue branch:
4xx(bad request, auth): a real bug — route to an alert channel, don't retry blindly5xx: transient — Retry On Fail covers it429: Retry On Fail, and slow down the workflow's input rate if it happens repeatedly
For low-volume transactional flows (receipts, OTPs, notifications), retry + alert is enough.
Step 5: Check delivery status
Because /v1/send returns immediately with queued, follow up with a status call:
- Method:
GET - URL:
https://api.mailanvil.com/v1/emails/{{ $json.id }}
Response states: queued, delivered, bounced, suppressed, failed.
In practice, don't poll inside the same workflow run. Add a second workflow on a Schedule Trigger (every 5–15 minutes) that queries recent message IDs and routes anything not delivered into a review list. For high-volume sending, use webhooks instead — MailAnvil can push state changes to your own endpoint, which is the right shape for automation platforms anyway.
Step 6: Templates keep the HTML out of n8n
Long HTML bodies inside a JSON node are painful to edit and impossible to review. Prefer stored templates:
- Upload the template once via
POST /v1/templates(or the dashboard) - In the send body, use
template_id+template_data:
{
"from": "[email protected]",
"to": ["{{ $json.customer_email }}"],
"template_id": "tpl_order_receipt",
"template_data": {
"order_id": "{{ $json.order_id }}",
"total": "{{ $json.total }}"
}
}
Now the email design lives in one place, and n8n workflows only pass variables.
Step 7 (optional): let an AI Agent node send the email
n8n's AI Agent nodes can call tools. MailAnvil also ships an MCP server at mcp.mailanvil.com/mcp, so an agent that supports MCP can discover and call the send tools directly — useful when the "who gets emailed and why" decision itself is AI-driven. For deterministic transactional flows (receipts, OTPs), the plain HTTP Request node is the better choice: fewer moving parts, fully testable.
Common mistakes
- Unverified
fromdomain. Every send 4xxes until the domain is verified (SPF/DKIM set up). Verify first, send second. - Hardcoding the key in the node. Anyone with workflow-view access sees it. Use credentials.
- Treating 202 as delivered. It's
queued. Check state before declaring success in a customer-facing flow. - HTML-only emails. Always send
textalongsidehtml— spam filters and plain-text clients both notice its absence.
That's the full loop: trigger → enrich → send → verify. From here, duplicate the HTTP Request node into your existing workflows; the credential is shared, so each new integration is about five minutes of work.