SMTP Authentication
PostMTA supports two authentication pathways: a REST API accessed with Bearer tokens (or OAuth 2.0 client credentials), and traditional SMTP submission with AUTH PLAIN. Every API request also validates that the key's workspace is active.
API Key Management
API keys are long-lived credentials scoped to a single workspace. They are prefixed with pmk_live_ (production) or pmk_test_ (sandbox). Keys are stored as a bcrypt hash — only the plaintext value is returned once at creation time.
Create an API key
curl -X POST https://api.postmta.com/v1/keys \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "webapp-production",
"permissions": [
"messages:send",
"messages:read",
"domains:read",
"webhooks:write"
]
}'Response:
{
"id": "key_01HX5K9P7N3M4Q6R8T2V4W6Y8",
"name": "webapp-production",
"key": "pmk_live_abc123xyz789def456...",
"permissions": ["messages:send", "messages:read", "domains:read", "webhooks:write"],
"workspace_id": "ws_01HX5K9P7N3M4Q6R8T2V4W6Y0",
"created_at": "2026-07-18T12:00:00Z",
"last_used_at": null
}List keys in a workspace
curl https://api.postmta.com/v1/keys \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response:
{
"keys": [
{
"id": "key_01HX5K9P7N3M4Q6R8T2V4W6Y8",
"name": "webapp-production",
"permissions": ["messages:send"],
"created_at": "2026-07-18T12:00:00Z",
"last_used_at": "2026-07-18T14:22:00Z"
}
],
"total": 1
}Revoke an API key
curl -X DELETE https://api.postmta.com/v1/keys/key_01HX5K9P7N3M4Q6R8T2V4W6Y8 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Returns 204 No Content on success. Revocation is immediate and irreversible.
Permission scopes
| Scope | Description |
|---|---|
messages:send | Inject outbound messages via REST or SMTP. |
messages:read | Read message status and event history. |
domains:read | List and inspect sending domains. |
domains:write | Add and verify new sending domains. |
webhooks:write | Register and manage webhook endpoints. |
keys:write | Create and revoke API keys (restricted). |
SMTP AUTH PLAIN
AUTH PLAIN is the simplest SMTP authentication mechanism. The client sends a base64-encoded credential string containing both the username (your account ID) and password (your API key) in a single round-trip.
Production port: 587 (STARTTLS required)
Legacy port: 25 (restricted — contact support to enable)
AUTH PLAIN command
The AUTH PLAIN credential format is:
\u0000USERNAME\u0000PASSWORDFor PostMTA, USERNAME is your account ID (e.g., acc_01HX5K9P7N3M4Q6R8T2V4W6Y0) and PASSWORD is your API key (e.g., pmk_live_abc123...). The entire string is base64-encoded before sending.
Manual encode example (what your SMTP library does):
# Plain credential string:
# \0acc_01HX5K9P7N3M4Q6R8T2V4W6Y0\0pmk_live_abc123xyz789def456...
# base64-encoded (Python):
import base64
cred = b'\0acc_01HX5K9P7N3M4Q6R8T2V4W6Y0\0pmk_live_abc123xyz789def456...'
print(base64.b64encode(cred).decode())
# Output: AGFjY18wMUg1UEs5UDdOM000UTZSOFQyVjRXNlY5MGhhYmMxMjN4eXo3OGRlZjQ1Ni4uLg==Full SMTP session with AUTH PLAIN:
220 mail.postmta.com ESMTP PostMTA ready
EHLO my-mail-server
250-mail.postmta.com
250-8BITMIME
250-AUTH PLAIN
250 STARTTLS
AUTH PLAIN AGFjY18wMUg1UEs5UDdOM000UTZSOFQyVjRXNlY5MGhhYmMxMjN4eXo3OGRlZjQ1Ni4uLg==
235 Authentication successfulFull SMTP send flow
After successful authentication, send an email using the standard SMTP envelope commands. All content must be UTF-8 encoded.
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"
List-Unsubscribe: <https://example.com/unsubscribe/alice@example.com>
<html>
<body>
<h1>Order Confirmed</h1>
<p>Hi Alice, your order #12345 has shipped.</p>
</body>
</html>
.
250 OK msg_id=msg_01HX5K9P7N3M4Q6R8T2V4W6Y8The server returns a msg_id on acceptance.
MAIL FROM address must belong to a verified sending domain on your account. Using an unverified domain results in a 550 5.7.1 Unverified sender domain rejection.OAuth 2.0 — Client Credentials Flow
For service-to-service integrations, OAuth 2.0 client credentials are the recommended approach. You exchange a client ID and client secret for a short-lived JWT access token.
Request an access token
curl -X POST https://auth.postmta.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=CLIENT_ID_FROM_DASHBOARD" \
-d "client_secret=CLIENT_SECRET_FROM_DASHBOARD"Response:
{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"workspace_id": "ws_01HX5K9P7N3M4Q6R8T2V4W6Y0"
}Use the access token as a Bearer token on the REST API:
curl https://api.postmta.com/v1/messages/msg_01HX5K9P7N3M4Q6R8T2V4W6Y8 \
-H "Authorization: Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."Workspace Membership
Every API key is bound to a single workspace. On every authenticated request, PostMTA verifies that:
- The API key is valid and has not been revoked.
- The key's workspace is active (not suspended or deleted).
- The key has the required permission scope for the requested operation.
If the workspace is suspended, the API returns 403 Forbidden with error code workspace_suspended. Contact support to resolve a suspended workspace.
{
"error": "workspace_suspended",
"message": "This workspace has been suspended. Contact hello@netwit.ca.",
"workspace_id": "ws_01HX5K9P7N3M4Q6R8T2V4W6Y0"
}Multi-workspace accounts can manage each workspace independently. Keys from one workspace cannot access another workspace's data, even if the account owns both.
Rate Limits
Rate limits protect the platform and ensure fair usage across all customers. Limits are enforced at two levels: per key and per workspace.
| Plan | Per-Key Limit | Per-Workspace Limit |
|---|---|---|
| Starter | 60 req/min | 300 req/min |
| Growth | 300 req/min | 1,500 req/min |
| Business | 1,000 req/min | 5,000 req/min |
| Enterprise | Custom | Custom |
When a rate limit is exceeded, the API returns 429 Too Many Requestswith a Retry-After header indicating seconds to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1752856860
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Retry after 30 seconds.",
"retry_after": 30
}SMTP rate limits
SMTP connections are limited to 100 messages per connection and50 concurrent connections per key. For high-volume sending, use connection pooling and keep connections alive with periodicNOOP commands.
Next Steps
- Sending Domains — add and verify the domains you send from
- Sending Email — REST and SMTP sending in depth
- Webhooks — receive real-time delivery events
- Custom Headers — List-Unsubscribe, X-Priority, and header injection protection