← Back to Self-host SendGrid

Migrate from SendGrid to Postal

Transactional email API · full migration guide

SendGrid → Postal is a code change plus a deliverability project. The code change is small: swap your `POST https://api.sendgrid.com/v3/mail/send` calls for `POST https://postal.your-domain.com/api/v1/send/message` with Postal's `X-Server-API-Key` header — the JSON body is similar, just renamed fields. The deliverability project is the actual work: SendGrid's IP reputation is shared and warmed; your Postal install is sending from a fresh box with no history. Most teams run Postal as the application gateway and chain it to Amazon SES or a similar relay so SES carries the IP reputation and Postal handles the routing, retries, suppression list, and analytics.

Prerequisites

  • A self-hosted Postal instance — see /sendgrid/ for the 30-minute docker-compose recipe and ~$5-10/mo VPS sizing (Postal + MariaDB + RabbitMQ).
  • DNS control on your sending domain — you'll add SPF, DKIM (Postal generates the keys), DMARC, and rDNS records.
  • An upstream relay (recommended): Amazon SES at ~$0.10/1k emails. Configure SES first, verify your domain, request production access (the SES sandbox limits sends to verified addresses).
  • Application access — you'll be modifying the email-sending code path. Have a code review / rollback plan.
  • A test domain or non-critical email path to validate the cutover before touching production transactional mail.

Step 1 — Export from SendGrid

  1. Inventory your SendGrid usage

    SendGrid dashboard → Email API → Integration. Note: which API endpoints you call (`/v3/mail/send` is the common one; webhooks for events; subuser API for multi-tenant), volume per day, sender domains/identities, and any IP allowlist requirements your senders have.

  2. Export the suppression list

    SendGrid → Suppressions → Global Unsubscribes / Bounces / Spam Reports / Blocks / Invalid. Each list has a CSV export. Pull all four. This is the data you must preserve — re-sending to a previously-bounced or unsubscribed address is the fastest path to spam-folder hell.

  3. Export dynamic templates

    SendGrid → Email API → Dynamic Templates. There's no bulk export — for each template, click into it and copy the HTML and the test data. Save as `templates/<template-name>.html` in version control. Postal does not have a dynamic templates surface (it sends raw HTML), so templating moves to your application or a tool like Maizzle.

  4. Document API key scopes

    SendGrid → Settings → API Keys. List each key, its scope, and which application uses it. Postal's API keys are simpler — one key per 'mail server' (Postal's term for a sender configuration).

Step 2 — Import into Postal

  1. Stand up Postal and configure DNS

    Run the Postal docker-compose. Visit the Postal UI, create the first admin account. Settings → Organisation → New Organisation. New Mail Server → set sending domain. Postal generates a DKIM key — copy it to your DNS as a TXT record at `postal._domainkey.your-domain.com`. Add SPF (`v=spf1 include:postal.your-domain.com ~all` if Postal sends directly, or include your relay's SPF), DMARC (`v=DMARC1; p=none; rua=mailto:dmarc@your-domain.com`), and rDNS via your VPS provider.

  2. Configure the upstream relay (recommended path)

    In Postal: Mail Server → Settings → Outgoing → choose SMTP Relay. Enter SES SMTP credentials (`email-smtp.us-east-1.amazonaws.com`, port 587, STARTTLS). Test send. Postal now hands outbound mail to SES, which delivers from its warmed IPs.

  3. Generate Postal API credentials

    Postal → Mail Server → Credentials → New Credential → API Key. Save the key. Each credential has a scope — typically one per application or per environment.

  4. Replace the SendGrid SDK with Postal's API

    Code change. SendGrid's `POST /v3/mail/send` JSON body uses `personalizations[].to[].email`, `from.email`, `subject`, `content[].value`. Postal's `POST /api/v1/send/message` uses `to`, `from`, `subject`, `html_body`, `plain_body`, with the `X-Server-API-Key` header. Most languages' HTTP libraries can swap with a 20-line change. Some teams prefer to keep using SendGrid's SDK in their code and point it at a SendGrid-compatible proxy (Postal does not natively offer one; this is an Inbucket / MailSlurper pattern).

  5. Import the suppression list

    Postal → Mail Server → Suppression List → Import. Upload the SendGrid CSVs (you may need to merge into one column of email addresses — Postal accepts a simple line-per-email format). Verify the count matches before sending production mail.

  6. Re-implement webhook event consumption

    If your application listens to SendGrid's Event Webhook (delivered, bounced, opened, clicked, spamreport, dropped, etc.), Postal has equivalents: Mail Server → Webhooks → New Webhook. Set the URL of your event consumer. Postal's payload format differs — adjust your handler accordingly. Common mapping: SendGrid `bounce` → Postal `MessageBounced`, `delivered` → `MessageDelivered`, `open` → `MessageOpened`, `click` → `MessageClicked`.

  7. Roll out gradually with a feature flag

    Wrap your send code in a feature flag: 1% of sends through Postal, 99% through SendGrid. Watch the bounce + complaint metrics on both sides. Bump to 10%, 50%, 100% over a week. Postal → Messages shows real-time delivery status.

  8. Cancel SendGrid

    Once 100% of sends are through Postal for 2-4 weeks with healthy delivery metrics, cancel SendGrid (or downgrade to a free tier as a fallback). Save the suppression CSVs for 90 days as an audit trail.

Field / concept mapping

SendGrid Postal Notes
SendGrid `POST /v3/mail/send` Postal `POST /api/v1/send/message` Different field names but similar structure. `personalizations[].to[].email` → `to[]`. `from.email` → `from`. `subject` → `subject`. `content[]` → `html_body` + `plain_body`.
SendGrid API key Postal Server API Key (X-Server-API-Key header) 1:1. One Postal key per sending application.
SendGrid Event Webhook Postal Webhook Event names differ: `delivered` → `MessageDelivered`, `bounce` → `MessageBounced`, etc. Adjust handler to the new payload shape.
SendGrid Dynamic Template External templating (your app or Maizzle) Postal does not have templates. Render the HTML in your application before calling Postal.
SendGrid Suppression List Postal Suppression List Bulk import via CSV. Postal honors suppressions globally per mail server.
SendGrid Subuser Postal Mail Server (one per tenant) Postal's organisation/mail-server model fits multi-tenant SaaS — give each tenant their own mail server with its own DKIM key for full isolation.
SendGrid IP Pool Postal IP Pool Postal supports multiple sending IPs per mail server. Useful if you have transactional and marketing on different IPs.
SendGrid Inbound Parse Postal Incoming Routes Both accept inbound email and POST to a webhook. Recreate the route in Postal's UI.
SendGrid stats / activity feed Postal Messages dashboard Postal's dashboard shows per-message status, opens, clicks, bounces. Comparable to SendGrid's Activity Feed.
SendGrid SMTP relay Postal SMTP Postal also accepts SMTP. If your stack uses SMTP today (most languages: just change `MAIL_HOST` env), the change is config-only.

Downtime estimate

Zero downtime if you do the gradual feature-flag rollout (1% → 10% → 50% → 100% over a week or two). Hard cutover (single deploy switches all sends): 5-15 minutes of risk during the deploy as the new code path is exercised under real load — usually fine for low-volume teams, dangerous for high-volume transactional senders.

Common gotchas

  • Cold IP reputation is the dominant risk. Sending 100k/day from day one of a fresh Postal install with a fresh VPS IP is the fastest path to spam-folder. The relay-through-SES path sidesteps this entirely.
  • DMARC misconfig (e.g. `p=reject` before SPF/DKIM is verified passing) bounces real production mail. Start with `p=none`, watch reports for a week, then move to `p=quarantine` then `p=reject`.
  • Postal stores message bodies in MariaDB by default — at high volume the DB grows quickly. Configure retention (Mail Server → Settings → Message Retention) to delete delivered messages after N days.
  • RabbitMQ is in the Postal stack and is a real operational concern — if it crashes, queued mail piles up. Monitor it.
  • SendGrid's `unsubscribe_groups` and `asm` (advanced suppression management) feature has no Postal equivalent. If you rely on per-list unsubscribe groups (newsletters vs transactional), pair Postal with Listmonk for the marketing layer.

Rollback plan

The feature-flag rollout makes rollback trivial — flip the flag back to SendGrid percentage. Keep your SendGrid account paid for the full migration window plus 30 days. The suppression list export from SendGrid is a one-way safety net — it ensures previously-unsubscribed users don't get re-mailed by your Postal instance, which would be a deliverability and legal issue. Hold the export CSVs in cold storage for at least 90 days.

Looking for setup time, monthly cost, and other alternatives? See Self-host SendGrid.