Matrix: M22M23
Open in panel — /app/sending
Sending

Sending Email

PostMTA accepts outbound email via two pathways: an SMTP relay for traditional mail injection, and a REST API for programmatic, structured sending with built-in personalization and batch support.

SMTP Relay

The SMTP relay accepts standard SMTP submissions. It supports AUTH PLAIN, STARTTLS on ports 587 and 2525, and the full ESMTP command set.

PortEncryptionUse case
587STARTTLS requiredProduction submission
2525STARTTLS optionalLab and testing environments
25STARTTLS optionalLegacy — restricted, requires support opt-in

SMTP submission flow

220 mail.postmta.com ESMTP PostMTA ready
EHLO my-mail-server.example.com
250-mail.postmta.com
250-8BITMIME
250-AUTH PLAIN
250 STARTTLS
AUTH PLAIN AGFjY18wMUg1UEs5UDdOM000UTZSOFQyVjRXNlY5MGhhYmMxMjN4eXo3OGRlZjQ1Ni4uLg==
235 Authentication successful
MAIL FROM: <hello@mail.example.com>
250 OK
RCPT TO: <alice@example.com>
250 OK
DATA
354 Begin input; end with <CRLF>.<CRLF>
From: Example Inc <hello@mail.example.com>
To: Alice <alice@example.com>
Subject: Your order confirmation
Content-Type: text/html; charset="UTF-8"

<html>
<body>
<h1>Order Confirmed</h1>
<p>Hi Alice, your order #12345 has shipped.</p>
</body>
</html>
.
250 OK msg_id=msg_01HX5K9P7N3M4Q6R8T2V4W6Y8
QUIT
221 Bye

Connection pooling

For high-volume sending, maintain a pool of persistent SMTP connections rather than opening a new connection per message. PostMTA supports up to 50 concurrent connections per API key and up to 100 messages per connection.

# Keepalive with NOOP every 60 seconds to prevent connection timeout
NOOP
250 OK

REST API — Send a Message

The REST API accepts structured JSON payloads. The html andtext fields accept raw content — no base64 encoding required. All addresses must use the { "address": "..."} object form for full personalization support.

Basic single-recipient send

curl -X POST https://api.postmta.com/v1/messages/send \
  -H "Authorization: Bearer pmk_live_abc123xyz789..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": {
      "address": "hello@mail.example.com",
      "name": "Example Inc"
    },
    "to": [
      {
        "address": "alice@example.com",
        "name": "Alice"
      }
    ],
    "subject": "Your order confirmation",
    "html": "<h1>Order Confirmed</h1><p>Hi Alice, your order #12345 has shipped.</p>",
    "headers": {
      "X-Order-ID": "12345"
    }
  }'

Success response (HTTP 202):

{
  "message_id": "msg_01HX5K9P7N3M4Q6R8T2V4W6Y8",
  "status": "accepted",
  "accepted_count": 1,
  "rejected_count": 0,
  "created_at": "2026-07-18T12:00:00Z"
}

Providing both HTML and plain-text

curl -X POST https://api.postmta.com/v1/messages/send \
  -H "Authorization: Bearer pmk_live_abc123xyz789..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": {
      "address": "hello@mail.example.com"
    },
    "to": [
      {
        "address": "alice@example.com"
      }
    ],
    "subject": "Your order confirmation",
    "html": "<h1>Order Confirmed</h1><p>Hi Alice...</p>",
    "text": "Order Confirmed\n\nHi Alice, your order #12345 has shipped.",
    "headers": {
      "X-Order-ID": "12345"
    }
  }'
Always include a text version when possible. Some recipients and corporate filters prefer or require plain-text alternatives.

Batch Send — Multiple Recipients

Pass multiple recipients in the to array. Each recipient can carry its own substitutions object for per-recipient personalization.

Batch send example

curl -X POST https://api.postmta.com/v1/messages/send \
  -H "Authorization: Bearer pmk_live_abc123xyz789..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": {
      "address": "newsletter@mail.example.com",
      "name": "Example Newsletter"
    },
    "to": [
      {
        "address": "alice@example.com",
        "substitutions": {
          "name": "Alice",
          "url": "https://example.com/promo/alice"
        }
      },
      {
        "address": "bob@example.com",
        "substitutions": {
          "name": "Bob",
          "url": "https://example.com/promo/bob"
        }
      },
      {
        "address": "carol@example.com",
        "substitutions": {
          "name": "Carol",
          "url": "https://example.com/promo/carol"
        }
      }
    ],
    "subject": "Your monthly update",
    "html": "<p>Hi <name}, check out <a href=\"<url}\">today\'s featured deal</a>.</p>"
  }'

One API call generates three individual messages. Each recipient receives the same campaign with their own substituted values.

Batch response

{
  "message_id": "msg_01HX5K9P7N3M4Q6R8T2V4W6Y8",
  "status": "accepted",
  "accepted_count": 3,
  "rejected_count": 0,
  "recipients": [
    {
      "address": "alice@example.com",
      "message_id": "msg_01HX5K9P7N3M4Q6R8T2V4W6Y8",
      "status": "accepted"
    },
    {
      "address": "bob@example.com",
      "message_id": "msg_01HX5K9P7N3M4Q6R8T2V4W6YA",
      "status": "accepted"
    },
    {
      "address": "carol@example.com",
      "message_id": "msg_01HX5K9P7N3M4Q6R8T2V4W6YC",
      "status": "accepted"
    }
  ],
  "created_at": "2026-07-18T12:00:00Z"
}

Personalization with Handlebars Substitutions

PostMTA uses Handlebars-style template substitutions in subject,html, text, and most header values. Anysubstitutions key on a recipient is automatically available.

Supported substitution syntax

SyntaxExampleNotes
<variableName><name>Inline HTML-safe substitution.
<variableName|fallback><name|Friend>Default if value is null/empty.
<unescaped:variableName><unescaped:htmlContent>Raw (no HTML escaping).

Substitution example

{
  "from": { "address": "support@example.com" },
  "to": [
    {
      "address": "alice@example.com",
      "substitutions": {
        "firstName": "Alice",
        "accountAge": "3 years",
        "accountUrl": "https://example.com/account/alice"
      }
    }
  ],
  "subject": "Your account summary — <firstName>",
  "html": "<p>Hi <firstName>,\n\nYou\'ve been a member for <accountAge>.\n\n<a href=\"<accountUrl>\">View your account</a>.</p>"
}
Do not use <unescaped: substitutions with untrusted user input. Always sanitize content from external sources to prevent XSS in email clients.

Custom Headers

Pass custom headers in the headers object of the JSON payload. Standard headers like From, To, and Subjectare set automatically from the envelope fields and should not be duplicated in headers.

Adding custom headers via API

{
  "from": { "address": "hello@mail.example.com" },
  "to": [ { "address": "alice@example.com" } ],
  "subject": "Your order confirmation",
  "html": "<p>Order confirmed.</p>",
  "headers": {
    "X-Order-ID": "12345",
    "X-Campaign-ID": "summer-sale",
    "X-Priority": "3",
    "List-Unsubscribe": "<https://example.com/unsubscribe/alice@example.com>"
  }
}

For SMTP sends, add headers directly in the message body before the blank line that separates headers from the message body.

Attachments

Attachments are passed as an array in the attachments field. The content must be base64-encoded. Maximum attachment size is 25 MB per message.

Attaching a PDF invoice

curl -X POST https://api.postmta.com/v1/messages/send \
  -H "Authorization: Bearer pmk_live_abc123xyz789..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": { "address": "billing@example.com" },
    "to": [ { "address": "alice@example.com" } ],
    "subject": "Invoice #12345",
    "html": "<p>Please find your invoice attached.</p>",
    "attachments": [
      {
        "filename": "invoice-12345.pdf",
        "content_type": "application/pdf",
        "content": "JVBERi0xLjcKJeLjz9M..."
      }
    ]
  }'

Constraints

Scheduled Sending

Set a send_at timestamp to defer delivery. The message is accepted immediately and queued for delivery at the specified time. Times are interpreted as UTC in ISO 8601 format.

curl -X POST https://api.postmta.com/v1/messages/send \
  -H "Authorization: Bearer pmk_live_abc123xyz789..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": { "address": "hello@mail.example.com" },
    "to": [ { "address": "alice@example.com" } ],
    "subject": "Appointment reminder",
    "html": "<p>Reminder: your appointment is tomorrow at 10am.</p>",
    "send_at": "2026-07-20T09:00:00Z"
  }'
Scheduled messages can be cancelled before send_at by callingDELETE /v1/messages/:id.

Idempotency

For critical sends (e.g., order confirmations), pass an Idempotency-Keyheader to safely retry without creating duplicate messages. The key must be unique per message; if the same key is submitted twice within 24 hours, the second submission returns the original message's result.

curl -X POST https://api.postmta.com/v1/messages/send \
  -H "Authorization: Bearer pmk_live_abc123xyz789..." \
  -H "Idempotency-Key: order-12345-confirmation-001" \
  -H "Content-Type: application/json" \
  -d '{
    "from": { "address": "hello@mail.example.com" },
    "to": [ { "address": "alice@example.com" } ],
    "subject": "Order #12345 confirmed",
    "html": "<p>Your order has been confirmed.</p>"
  }'

Send Limits and Rate Throttling

PostMTA enforces sending limits per plan tier. Exceeding the limit returns429 Too Many Requests. The Retry-After header tells you how many seconds to wait.

PlanMessages/minuteMessages/day (soft cap)
Starter6010,000
Growth300100,000
Business1,000Unlimited
EnterpriseCustomUnlimited

For SMTP, rate limiting is applied per connection. Use concurrent connections to achieve your target throughput.

Next Steps