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.
| Port | Encryption | Use case |
|---|---|---|
| 587 | STARTTLS required | Production submission |
| 2525 | STARTTLS optional | Lab and testing environments |
| 25 | STARTTLS optional | Legacy — 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 ByeConnection 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 OKREST 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"
}
}'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
| Syntax | Example | Notes |
|---|---|---|
<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>"
}<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
- Maximum 10 attachments per message.
- Maximum total message size (including attachments) is 25 MB.
- Attachment content must be base64-encoded in the JSON payload.
- Supported content types: PDF, ZIP, CSV, PNG, JPG, GIF, TXT, and most common MIME types.
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"
}'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.
| Plan | Messages/minute | Messages/day (soft cap) |
|---|---|---|
| Starter | 60 | 10,000 |
| Growth | 300 | 100,000 |
| Business | 1,000 | Unlimited |
| Enterprise | Custom | Unlimited |
For SMTP, rate limiting is applied per connection. Use concurrent connections to achieve your target throughput.
Next Steps
- SMTP Authentication — AUTH PLAIN, OAuth 2.0, and workspace membership
- Sending Domains — SPF, DKIM, DMARC, and subdomain delegation
- Bounce Classification — hard bounces, soft bounces, and retry schedules
- Custom Headers — List-Unsubscribe, X-Priority, RFC 7230 compliance
- Webhooks — real-time delivery events for sent messages