unMTA

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

SettingValue
HostYour cluster's SMTP endpoint
Port25, 587, or 2587
EncryptionSTARTTLS required on all ports
AuthSMTP 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/v1

Authentication

HTTP Basic Auth using your credential username and password.

Request Format

Send a JSON object with:

FieldRequiredDescription
envelope_senderYesBounce address for delivery failures
recipientsYesArray of recipient email addresses
contentYesMessage content object

Content Object

FieldRequiredDescription
fromYesFrom header (used for DKIM signing)
subjectNoSubject line
text_bodyNoPlain text body
html_bodyNoHTML body
reply_toNoReply-To address
headersNoAdditional 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.

StatusMeaning
200Message accepted for delivery
401Invalid credentials
429Rate 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:

  1. Per-recipient substitutionsrecipients[].substitutions (highest priority)
  2. Global substitutionssubstitutions at the request level
  3. Built-in variablesname and email from 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:

DialectDescription
JinjaMiniJinja engine—Jinja2-compatible syntax (default)
HandlebarsHandlebars-compatible engine
StaticNo 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_body
  • html_body
  • headers

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.

ParameterDescription
first_attemptRFC 3339 timestamp for earliest delivery (e.g., 2024-03-01T17:00:00-08:00)
dowDays of week for delivery (e.g., Mon,Wed,Fri)
tzIANA timezone name (e.g., America/Phoenix)
startWindow start time in HH:MM:SS format
endWindow 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-series

Campaigns 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 Requests with Retry-After header

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:

CheckDescription
Domain verifiedSender domain must be verified in your cluster
Domain authorizedSender domain must be allowed for your credential
FROM header requiredMessage must include a FROM header for DKIM signing
Domain alignmentFROM 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.net

Your 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.

On this page