Email Delivery Pipeline
How Postkit processes and delivers your emails
Every email you send through Postkit passes through a multi-stage pipeline before reaching the recipient's inbox. Unlike services that keep their internals opaque, Postkit makes this process fully transparent so you can understand exactly what happens to your email at every step.
How an email moves through Postkit
The following diagram shows the complete journey of an email from your API request to the recipient's mailbox, including event tracking back to your application.
Your application receives an immediate 200 response with status queued as soon as the API Gateway accepts and validates the request. The actual delivery happens asynchronously through the pipeline.
Step by step
1. API request and validation
When you send a POST request to /v1/emails, the API Gateway performs several checks before accepting the message:
- Authentication -- Your API key (starting with
pk_live_) is verified against a SHA-256 hash. Valid keys are cached in Redis for fast subsequent lookups. - Field validation -- Required fields (
from,to,subject, and eitherhtmlortext) are validated. Email addresses are checked for format correctness. - Rate limiting -- Each organization has a rate limit (default: 10 requests per second). If exceeded, the API returns a
429status withRetry-AfterandX-RateLimit-Resetheaders. - Domain verification -- The sending domain in the
fromaddress must be verified in your account with valid DNS records. - Suppression check -- Recipient addresses are checked against your suppression list. Addresses that have previously hard-bounced or filed complaints are blocked from sending.
If all checks pass, the email is accepted for delivery.
2. Queueing via NATS JetStream
Accepted emails are published to the emails.send NATS JetStream stream. This is an append-only, persistent message log that guarantees your email will not be lost even if downstream services are temporarily unavailable.
The API Gateway returns a 200 response immediately after publishing:
{
"id": "em_a1b2c3d4e5",
"status": "queued"
}Your email now has a unique ID (prefixed with em_) that you can use to track its status through the rest of the pipeline.
3. Send Worker picks up the message
The Send Worker is a pull-based NATS consumer that retrieves messages from the emails.send stream. Key behavior:
- Pull-based consumption -- The worker actively pulls messages rather than having them pushed, giving it control over processing rate.
- Acknowledgment window -- Each message must be acknowledged within 30 seconds (
ack-wait: 30s). If the worker crashes or hangs, the message is automatically redelivered. - Maximum delivery attempts -- Messages are retried up to 3 times (
max-deliver: 3). After 3 failed attempts, the message moves to a dead-letter state.
When the Send Worker picks up a message, the email status changes to sending.
4. Postal SMTP engine delivers the email
The Send Worker hands the email to Postal, the SMTP engine that handles the final delivery:
- DKIM signing -- Postal signs every outbound email with your domain's DKIM private key, proving the message is genuinely from your domain.
- SMTP delivery -- Postal connects to the recipient's mail server (MX record) and delivers the message using the SMTP protocol.
- TLS encryption -- Connections use opportunistic TLS for transport-layer encryption.
Postal manages the low-level SMTP conversation, including handling temporary failures (4xx responses) with its own retry logic.
5. Delivery events flow back
After Postal attempts delivery, events flow back through the system:
- Delivery events are published to the
emails.eventsNATS JetStream stream. - The Webhook Worker (a separate pull-based consumer) picks up these events and delivers them to your configured webhook endpoints.
- The webhook worker has a longer acknowledgment window (
ack-wait: 60s) and more retry attempts (max-deliver: 8) since webhook delivery to external endpoints is more prone to temporary failures.
Events include delivery confirmations, bounces, complaints, and engagement tracking (opens and clicks if enabled).
Email status lifecycle
Every email progresses through a defined set of statuses. You can check the current status via the API or receive status changes through webhooks.
| Status | Meaning |
|---|---|
queued | Accepted by the API, waiting in the NATS stream |
scheduled | Accepted but held for future delivery (if scheduled_at was set) |
sending | Picked up by the Send Worker, delivery in progress |
sent | Successfully handed to the recipient's mail server |
delivered | Confirmed delivery (when the receiving server reports success) |
bounced | Delivery permanently failed (invalid address, full mailbox, etc.) |
complained | Recipient marked the email as spam |
canceled | A scheduled email was canceled before its send time |
The typical happy path is: queued -> sending -> sent -> delivered.
Retry behavior
The pipeline has two layers of retry logic:
NATS JetStream retries handle failures in the Send Worker itself (crashes, timeouts, connection issues). Messages are redelivered up to 3 times with a 30-second acknowledgment window.
Postal SMTP retries handle temporary SMTP failures from the recipient's mail server (4xx responses like "try again later"). Postal manages these retries independently with exponential backoff.
If all retries are exhausted, the email is marked as bounced and a bounce event is emitted to your webhook.
What's next?
- Sending Email guide for complete API usage including batch sending, scheduling, and attachments
- Webhooks guide for receiving delivery events in real time
- DKIM, SPF & DMARC for how email authentication works in the pipeline