Sending Messages
Send email via SMTP or HTTP API.
Overview
Message injection is how you submit email for delivery through your unMTA cluster. You can inject messages using either SMTP or HTTP—choose whichever fits your application best.
Before sending email, you'll need:
- A verified domain — See Domains to add and verify your sending domain
- A credential — See Credentials to create SMTP/HTTP credentials
SMTP Injection
Connect to your cluster's SMTP endpoint using any standard SMTP client or library.
Connection Settings
| Setting | Value |
|---|---|
| Host | Your cluster's SMTP endpoint |
| Port | 25, 587, or 2587 |
| Encryption | STARTTLS required on all ports |
| Auth | SMTP AUTH PLAIN |
To find your cluster's SMTP hostname and HTTP endpoint, see Clusters.
Requirements
- STARTTLS required — Authentication only accepted over encrypted connection
- FROM header required — Used for DKIM signing
- Domain validation — MAIL FROM and FROM header domains must be verified and allowed for your credential
- Max message size — 10 MB
Example
Using swaks (Swiss Army Knife for SMTP):
swaks --to recipient@example.com \
--from sender@yourdomain.com \
--server your-cluster.unmta.net:587 \
--tls \
--auth-user your-credential \
--auth-password your-password \
--header "Subject: Test Message" \
--body "Hello from unMTA!"HTTP Injection
Submit messages via HTTP POST for applications that prefer REST APIs over SMTP.
Endpoint
POST https://your-cluster.unmta.net/api/inject/v1Authentication
HTTP Basic Auth using your credential username and password.
Request Format
Send a JSON object with:
| Field | Required | Description |
|---|---|---|
envelope_sender | Yes | Bounce address for delivery failures |
recipients | Yes | Array of recipient email addresses |
content | Yes | Message content object |
Content Object
| Field | Required | Description |
|---|---|---|
from | Yes | From header (used for DKIM signing) |
subject | No | Subject line |
text_body | No | Plain text body |
html_body | No | HTML body |
reply_to | No | Reply-To address |
headers | No | Additional headers as key-value object |
Include at least one of text_body or html_body. For best deliverability,
include both so recipients without HTML support see the plain text version.
Example
curl -X POST https://your-cluster.unmta.net/api/inject/v1 \
-u "username:password" \
-H "Content-Type: application/json" \
-d '{
"envelope_sender": "bounces@yourdomain.com",
"recipients": ["user@example.com"],
"content": {
"from": "sender@yourdomain.com",
"subject": "Hello from unMTA",
"text_body": "This is the plain text version.",
"html_body": "<html><body><p>This is the <b>HTML</b> version.</p></body></html>"
}
}'Response
On success, the API returns HTTP 200 with details about the injected message.
| Status | Meaning |
|---|---|
| 200 | Message accepted for delivery |
| 401 | Invalid credentials |
| 429 | Rate limited (too many failed auth attempts) |
Template Substitution
HTTP injection supports template substitution, allowing you to personalize messages for each recipient without making separate API calls.
Template Variables
Variables are populated from three sources, in order of precedence:
- Per-recipient substitutions —
recipients[].substitutions(highest priority) - Global substitutions —
substitutionsat the request level - Built-in variables —
nameandemailfrom each recipient
Substitution values can be any JSON type—strings, numbers, objects, or arrays.
Syntax
Use double curly braces to insert variables:
Hello {{ name }}, your order #{{ order_id }} has shipped!Template Dialects
Control templating behavior with the template_dialect field:
| Dialect | Description |
|---|---|
Jinja | MiniJinja engine—Jinja2-compatible syntax (default) |
Handlebars | Handlebars-compatible engine |
Static | No template expansion—send content as-is |
MiniJinja supports most common Jinja2 features including filters, conditionals, and loops, plus additional functions like now(), dateformat(), and pluralize().
Scope
Templates are applied to:
text_bodyhtml_bodyheaders
Attachments are not subject to template substitution.
Example
Send a personalized message to multiple recipients:
curl -X POST https://your-cluster.unmta.net/api/inject/v1 \
-u "username:password" \
-H "Content-Type: application/json" \
-d '{
"envelope_sender": "bounces@yourdomain.com",
"substitutions": {
"company": "Acme Corp"
},
"recipients": [
{
"email": "alice@example.com",
"name": "Alice",
"substitutions": { "order_id": "12345" }
},
{
"email": "bob@example.com",
"name": "Bob",
"substitutions": { "order_id": "67890" }
}
],
"content": {
"from": "orders@yourdomain.com",
"subject": "Your {{ company }} order has shipped",
"text_body": "Hi {{ name }},\n\nYour order #{{ order_id }} is on its way!\n\nThanks,\n{{ company }}"
}
}'This sends two personalized messages:
- Alice receives: "Hi Alice, Your order #12345 is on its way! Thanks, Acme Corp"
- Bob receives: "Hi Bob, Your order #67890 is on its way! Thanks, Acme Corp"
To send content containing literal curly braces without substitution, set
template_dialect to Static.
Special Headers
These headers control message behavior during injection. Include them in your message headers for SMTP, or in the headers object for HTTP injection.
X-Schedule
Schedule messages for future delivery by including an X-Schedule header with a JSON value specifying when delivery should occur.
Deferred delivery — Delay the first delivery attempt until a specific time:
X-Schedule: {"first_attempt":"2024-03-01T17:00:00-08:00"}Time-window delivery — Restrict delivery to specific days and times:
X-Schedule: {"dow":"Mon,Tue,Wed,Thu,Fri","tz":"America/New_York","start":"09:00:00","end":"17:00:00"}This example delivers only on weekdays between 9 AM and 5 PM Eastern time. Messages received outside this window are held until the next valid delivery time.
| Parameter | Description |
|---|---|
| first_attempt | RFC 3339 timestamp for earliest delivery (e.g., 2024-03-01T17:00:00-08:00) |
| dow | Days of week for delivery (e.g., Mon,Wed,Fri) |
| tz | IANA timezone name (e.g., America/Phoenix) |
| start | Window start time in HH:MM:SS format |
| end | Window end time in HH:MM:SS format |
Time-window parameters (dow, tz, start, end) must all be specified
together. You can combine them with first_attempt to delay until a specific
time and then apply window constraints.
X-Campaign
Assign messages to a campaign for logical grouping and management:
X-Campaign: welcome-seriesCampaigns let you organize related messages and manage them together:
- Promotional sends — Group messages from a marketing campaign
- Transactional categories — Separate order confirmations from shipping notifications
- A/B testing — Track different message variants independently
Messages with the same campaign are grouped in the queue view and can be suspended or bounced as a unit. When suspending or bouncing a queue, you can target a specific campaign to affect only those messages while leaving other traffic to the same domain unaffected.
If no campaign is specified, messages are grouped by destination domain only.
The X-Campaign header is removed from the message before delivery.
Rate Limiting
unMTA does not rate limit message injection. However, it does include brute-force protection to prevent credential stuffing attacks by temporarily blocking IPs with repeated failed authentication attempts.
How It Works
- Failed authentication attempts are tracked per IP address
- After the threshold is exceeded, the IP is temporarily blocked
- IPv6 addresses are grouped by prefix to prevent address rotation attacks
When Blocked
If you exceed the failed authentication threshold:
- SMTP:
421 4.7.0 Too many failed authentication attempts. Try again later. - HTTP:
429 Too Many RequestswithRetry-Afterheader
If you're blocked due to failed attempts, wait before trying again. The block expires automatically after a short period.
Message Validation
Both SMTP and HTTP injection apply these validations:
| Check | Description |
|---|---|
| Domain verified | Sender domain must be verified in your cluster |
| Domain authorized | Sender domain must be allowed for your credential |
| FROM header required | Message must include a FROM header for DKIM signing |
| Domain alignment | FROM header domain must match an allowed sending domain |
Messages failing validation are rejected with an appropriate error message.
Bounce Addresses
unMTA automatically generates bounce addresses for delivery tracking. When a message bounces, the bounce notification is sent to this generated address, allowing unMTA to correlate bounces with the original message.
The generated bounce address format is:
{message_id}@c-{cluster_id}.unmta.netYour original envelope sender is preserved in message metadata and included in event payloads.
Best Practices
Choose the Right Method
- SMTP — Best for applications with existing SMTP infrastructure, email clients, or when streaming large volumes
- HTTP — Best for REST API integrations, serverless environments, or when you need template substitution
Validate Before Sending
Ensure your sending domain is verified and your credential has access to it before attempting injection. Check the Domains page for domain status.
Handle Errors Gracefully
- 401, 429 errors — Check your credential username and password
- Domain errors — Verify the sending domain is verified and allowed for your credential
Use Campaigns for Organization
Assign campaigns to logically group related messages. This makes it easier to monitor delivery and take action on specific message types without affecting other traffic.