postkit
Concepts

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 either html or text) 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 429 status with Retry-After and X-RateLimit-Reset headers.
  • Domain verification -- The sending domain in the from address 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.events NATS 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.

StatusMeaning
queuedAccepted by the API, waiting in the NATS stream
scheduledAccepted but held for future delivery (if scheduled_at was set)
sendingPicked up by the Send Worker, delivery in progress
sentSuccessfully handed to the recipient's mail server
deliveredConfirmed delivery (when the receiving server reports success)
bouncedDelivery permanently failed (invalid address, full mailbox, etc.)
complainedRecipient marked the email as spam
canceledA 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?