---
title: "Migrate from SparkPost"
description: "Move your SparkPost email integration to Bird with mappings for transmissions, SMTP, templates, suppressions, and webhooks, plus a cutover checklist."
canonical: "https://bird.com/docs/guides/email/migrate/sparkpost"
---

# Migrate from SparkPost

Use this guide to move outbound email from SparkPost to Bird. Follow the [main migration checklist](/docs/guides/email/migrate), using the mappings below for your HTTP or SMTP integration.

## Before you begin

You need access to your SparkPost account and subaccounts, sending-domain DNS, application configuration, and webhook handler. Prepare a [Bird workspace](/docs/guides/workspaces) and [API key](/docs/api/authentication) in your chosen [region](/docs/api/regions). The opt-out import also requires `preferences` write permission on the key.

Inventory senders, templates, snippets, recipient lists, suppressions, scheduled sends, IP pools, and webhooks. Include SDKs, framework mail adapters, background jobs, and inbound email flows. Record new resource IDs as you create them; SparkPost IDs and credentials do not work in Bird. Use the [Bird SDKs](/docs/sdks) when replacing a SparkPost client, and review its retries, timeouts, and pagination.

If you use [SparkPost subaccounts](https://developers.sparkpost.com/api/subaccounts/), [contact us](/help/support) before choosing a workspace layout. Confirm the available workspaces, permissions, shared resources, and tenant-provisioning workflow. A workspace API key cannot switch tenants with `X-MSYS-SUBACCOUNT`. Preserve suspended tenants and tenant-specific sending restrictions during migration.

Confirm that your Bird [plan allowances](/docs/guides/billing-and-usage) and [rate limits](/docs/guides/rate-limits) cover your sending volume, resource counts, and peak traffic.

Register your [sending domains](/docs/guides/email/sending-domains) early. Preserve working SparkPost DNS and choose separate return-path and tracking hostnames where needed. If registration reports an ownership conflict, [contact support](/help/support) before removing an active domain.

**Dedicated IPs:** [Contact us](/help/support) or your account team before migrating. Ask us to confirm whether your existing SparkPost IPs can move to Bird and agree on pool setup, timing, and any warmup needed. Include your account region, IP addresses, pool names, and sending volume. Keep your current IPs active until the migration plan is confirmed.

Confirm [pool selection](/docs/guides/email/dedicated-ips-and-pools#select-a-pool-at-send-time) and recipient IP or hostname allowlists before cutover. Buying an IP does not change the default pool. Newly purchased IPs can send overflow through shared infrastructure during [warmup](/docs/guides/email/ip-warmup), which matters if recipients accept mail only from specific IPs.

## Hand this to your agent

Paste this into your coding agent in your application repository:

```text
Help me migrate my SparkPost email integration to Bird.
1. Use an existing Bird MCP connection or signed-in CLI. Otherwise follow https://bird.com/docs/ai/set-up-your-agent.md. Append .md to Bird docs URLs to read Markdown.
2. Read https://bird.com/docs/guides/email/migrate/sparkpost.md and https://bird.com/docs/guides/email/migrate.md. Make a read-only inventory of my SparkPost call sites, SDKs, resource IDs, configured URLs, sending domains, and scheduled jobs before editing. Never print API keys.
3. Propose workspace and region mappings. If subaccounts, dedicated IPs, or domain ownership conflicts need a migration plan, use the available Bird tools to open a human email support ticket and return its ID. For CLI usage, read https://bird.com/docs/cli/reference/support-tickets-create.md. Wait for the agreed plan before moving those resources.
4. Follow https://bird.com/docs/guides/email/sending-domains.md; preserve working DNS and show me proposed records. Ask before DNS changes or paid provisioning.
5. Port sending, templates, and webhooks using this guide. Preserve recipient privacy, personalization, and categories. Export suppressions with scope and type; show me the handling before importing or changing preferences.
6. Test using https://bird.com/docs/guides/email/testing-sandbox.md. Show me results and unresolved differences, including tracking, signatures, and correlation.
7. Ask for explicit approval before production cutover. Keep a rollback path; get separate approval before retiring SparkPost resources or credentials.
```

## Map the send call

Replace `POST /api/v1/transmissions` with [`POST /v1/email/messages`](/docs/api/reference/create-email-message), or [`POST /v1/email/batches`](/docs/api/reference/create-email-message-batch) for independent messages. The [SparkPost API overview](https://developers.sparkpost.com/api/) lists its regional hosts and authentication. Bird uses `https://us1.platform.bird.com` or `https://eu1.platform.bird.com`, matching your API key's region, with `Authorization: Bearer $BIRD_API_KEY`.

A SparkPost transmission can generate separate, personalized emails for its recipients. Bird shares content and parameters across recipients of one send. Use a separate message for each personalization, optionally grouped into a [batch](/docs/guides/email/sending-bulk). To-only sends remain individually addressed; check visible headers when adding Cc/Bcc copies.

Map the [SparkPost transmission fields](https://developers.sparkpost.com/api/transmissions/):

| SparkPost                                         | Bird migration                                                                                                                                            |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content.from`, `subject`, `html`, `text`         | Top-level fields with the same names                                                                                                                      |
| `content.reply_to`                                | `reply_to` array                                                                                                                                          |
| `recipients[].address`                            | One message per personalized recipient                                                                                                                    |
| `address.header_to`, `content.headers.CC`         | Rebuild `to` / `cc` / `bcc` groups; see the addressing note below                                                                                         |
| `content.headers`                                 | `headers`; check reserved names in the send guide                                                                                                         |
| `substitution_data`                               | Inline `parameters` or stored `template.parameters`; resolve overrides and fit Bird's [smaller parameter limit](/docs/guides/email/sending-email#content) |
| `content.template_id`                             | New Bird template `id` or `slug`; convert and publish content first                                                                                       |
| Transmission/recipient `metadata`                 | Merge into `metadata` with recipient keys winning; fit Bird's [smaller metadata limit](/docs/guides/email/sending-email#tags-vs-metadata)                 |
| Recipient `tags`, `campaign_id`                   | Choose `{ name, value }` tags; no broadcast is created                                                                                                    |
| `options.transactional`                           | Explicit `category`: `transactional` or `marketing`                                                                                                       |
| `options.open_tracking`, `options.click_tracking` | `track_opens`, `track_clicks`; resolve overrides first                                                                                                    |
| `options.start_time`                              | `scheduled_at`; template content is fixed at acceptance; see scheduling notes below                                                                       |
| `options.ip_pool`                                 | Bird `ip_pool_id`; contact us before moving dedicated IPs                                                                                                 |
| `content.attachments`                             | `type` → `content_type` (base MIME type), `name` → `filename`, `data` → base64 `content`; validate files that rely on MIME parameters                     |
| `content.inline_images`                           | Same file mapping, plus `name` → `content_id`; see below                                                                                                  |
| `return_path`, `tracking_domain`                  | Bird domain configuration; see below                                                                                                                      |
| `content.ab_test_id`                              | Choose variants and track results in your application; no direct send-field equivalent                                                                    |

For To/Cc/Bcc copies of one email, rebuild the recipient group once. SparkPost's [displayed addresses](https://developers.sparkpost.com/api/recipient-lists/#header-address-object) can differ from delivery recipients; Bird's `to`, `cc`, and `bcc` each add delivery recipients. Copying a SparkPost `CC` header into every expanded message's `cc` can send duplicate copies. Verify visible headers and recipient counts before switching.

Check [send field limits](/docs/guides/email/sending-email), [scheduling](/docs/guides/email/scheduled-sending), and [attachment rules](/docs/guides/email/attachments). Update inline-image IDs and matching `cid:` references to meet Bird's rules. Configure the [return path](/docs/guides/email/bounce-domain) and [tracking domain](/docs/guides/email/tracking-domain) on the domain.

Bird's HTTP send fields do not include SparkPost's `content.email_rfc822`, `content.amp_html`, or `options.inline_css`. Rebuild raw messages with the supported fields, provide HTML/text fallbacks for AMP, and inline CSS before submitting HTML. SMTP parses and rebuilds supported message parts; validate the received MIME if you depend on its exact structure. Attachment MIME parameters such as calendar `method` or text `charset` are not preserved.

For scheduled API messages, Bird fixes the template version, language, and parameters when it accepts the request. Later template edits do not update that message. To change it, [cancel the scheduled message](/docs/guides/email/scheduled-sending#canceling-a-scheduled-send) before processing starts, then submit a replacement. Keep a group of Bird message IDs if you need to replace SparkPost's campaign-based cancellation.

Set `BIRD_API_KEY` to your Bird key. This [sandbox](/docs/guides/email/testing-sandbox) example needs no verified domain and reaches no real inbox. For an EU key, use `https://eu1.platform.bird.com`:

```bash
curl --fail-with-body https://us1.platform.bird.com/v1/email/messages \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "onboarding@messagebird.dev",
    "to": ["delivered@messagebird.dev"],
    "subject": "Your receipt",
    "text": "Thanks for your order, {{ first_name }}.",
    "parameters": {"first_name": "Alex"},
    "category": "transactional",
    "metadata": {"order_id": "order_123"},
    "tags": [{"name": "mailstream", "value": "receipts"}]
  }'
```

Expect `202 Accepted` and an `em_` message ID. Store that ID and follow recipient outcomes through [events](/docs/guides/email/events). Acceptance does not establish delivery: a suppressed recipient can be accepted and later rejected. Update response parsing, [error handling](/docs/guides/errors), and [idempotent retries](/docs/guides/idempotency) with the send call.

For a batch, read the `data` array and save each message's ID against your own send record. Bird validates the batch before queueing: one invalid message can reject the whole request. Split large transmissions to fit the [batch limits](/docs/guides/email/sending-bulk), and follow Bird's retry rules when a response is ambiguous.

Give each distinct request or batch chunk its own stable idempotency key. Bird's [replay window](/docs/guides/idempotency) differs from SparkPost's; keep your application send records beyond that window to prevent duplicates during cutover or rollback.

## Move SMTP senders

Use Bird's [SMTP connection settings](/docs/guides/email/smtp), username `bird`, and an API key with email sending enabled. Check the region, TLS, and key configuration.

Convert SparkPost's [`X-MSYS-API` options](https://developers.sparkpost.com/api/smtp/) before removing the header. Set category, tags, tracking, and pool defaults in Bird's [SMTP configuration](/docs/guides/email/smtp#what-comes-from-the-message-and-what-comes-from-the-keys-configuration). Those settings apply per API key; use separate configured keys or HTTP when they vary between messages. Use HTTP for per-message metadata or template parameters.

Put every delivery recipient in the SMTP envelope, visible recipients in the MIME `To`/`Cc` headers, and Bcc recipients only in the envelope. Inventory `X-MSYS-API.archive` separately: SparkPost archive copies preserve the original recipient's tracking URLs, so ordinary Bcc is not equivalent. Validate a replacement before switching that flow.

Copy your effective tracking settings explicitly: Bird's unconfigured SMTP key enables open and click tracking, while SparkPost defaults vary by account. Set the category too: Bird SMTP defaults to transactional and inline HTTP to marketing. Newsletter senders need `marketing` on either path.

For [SMTP retries](/docs/guides/email/smtp#retrying-safely), reuse the idempotency key, envelope, and exact MIME bytes. Regenerating `Date`, `Message-ID`, or MIME boundaries changes the payload and can prevent a safe retry.

## Convert templates

Export the versions you actually send through SparkPost's [Templates API](https://developers.sparkpost.com/api/templates/): list with `GET /api/v1/templates?draft=false`, then retrieve content with `GET /api/v1/templates/{id}?draft=false`. Save drafts separately if needed. Include templates shared with subaccounts and referenced [snippets](https://developers.sparkpost.com/api/snippets/) in the inventory.

SparkPost's [template language](https://developers.sparkpost.com/api/template-language/) and Bird's Liquid syntax differ. Convert conditionals, loops, defaults, and nested values. Resolve recipient overrides and metadata used for rendering into explicit parameters. For example, `{{ if ... }}` becomes `{% if ... %}`. Shared `{{ name }}` syntax alone does not establish compatibility.

Expand snippets before publishing; Bird's Liquid does not support `include` or `render`. For stored templates, replace external references such as `{{ user.name }}` with flat parameters such as `{{ user_name }}`. If you insert dynamic HTML through SparkPost parameters, render it in your application and submit the completed body without inline `parameters`; ordinary HTML parameter values are escaped.

Create, preview, and publish a [Bird template](/docs/guides/email/templates), then follow [sending with a template](/docs/guides/email/sending-email#sending-with-a-template). Carry the effective sender, Reply-To, and custom headers from SparkPost into the send request; Bird templates supply the content.

For inline Liquid, include `parameters`, even `{}`; omitting it leaves tokens unchanged. Verify missing values, escaping, and URLs.

Replace unsubscribe placeholders with `{{ bird.unsubscribe_url }}`. Bird supplies [marketing unsubscribe headers](/docs/guides/email/unsubscribe-links); remove custom `List-Unsubscribe` and `List-Unsubscribe-Post` headers from marketing sends to avoid a `422` rejection.

Bird's unsubscribe links opt the address out of marketing email across the workspace. These links do not provide list-specific opt-outs. Check this behavior if your SparkPost integration offers separate subscriptions.

## Move recipient lists

Export each [stored recipient list](https://developers.sparkpost.com/api/recipient-lists/) with `GET /api/v1/recipient-lists/{id}?show_recipients=true` to include membership and personalization. Create destination [audiences](/docs/guides/email/audiences) and register [contact properties](/docs/guides/email/contacts#contact-properties) before importing. Review each import result and reconcile membership counts.

Contact properties belong to the contact across its audiences. If the same address has different substitution data in several SparkPost lists, reconcile those values before importing to avoid overwriting them. Bird contact properties have scalar types; keep list-specific or structured personalization in your application when it cannot be represented safely.

Use [broadcasts](/docs/guides/email/broadcasts) when a published template can be populated from contact properties. Audience membership is resolved when sending starts, and broadcast send and concurrency allowances apply. Use independent [batch messages](/docs/guides/email/sending-bulk) for request-specific parameters or a fixed recipient snapshot. Validate consent and suppression handling before activating a migrated list.

## Export suppressions

Export before production sending. Start with `GET /api/v1/suppression-list?cursor=initial&types=transactional,non_transactional,open_tracking`, then follow pagination to completion. Save the complete records, including type, source, list ID, subaccount, and timestamps. Use `X-MSYS-SUBACCOUNT: 0` for the primary account and each subaccount ID for its own list. See SparkPost's [Suppression List API](https://developers.sparkpost.com/api/suppression-list/).

Classify records by type, source, and scope before using the main guide's import loop. Bird's [`POST /v1/email/suppressions`](/docs/api/reference/create-suppression) takes an `email` and creates a manual, workspace-wide block on both categories:

- **Addresses blocked from receiving email:** import those that should be blocked across categories. Preserve the original export for reconciliation; imported records carry Bird's manual reason.
- **Account-wide marketing opt-outs:** use [`POST /v1/preferences`](/docs/api/reference/create-preference) with `channel: "email"`, the address in `handle`, `status: "revoked"`, and `coverage: "non_transactional"`. Set `source: "sparkpost-migration"` for reconciliation. Review existing Bird preferences first and preserve stricter restrictions; inspect `applied` and the returned preference after each write.
- **List-specific or transactional-only restrictions:** preserve their scope in your application's send eligibility. Bird's email preferences are channel-wide and cannot represent these scopes. A manual suppression can also block password resets. Keep affected traffic paused until you verify the replacement.
- **Open-tracking opt-outs:** set `track_opens: false` for the independent message, in addition to any sending restrictions. For SMTP, use a key with open tracking disabled or use HTTP for per-message control.

The preference request above records the restriction at import time. Keep SparkPost's original timestamps in your export and reconcile any later consent before writing. Reconcile imported records and failed writes, then test both categories. Synchronize new opt-outs and suppressions while both providers send. Continue applying opt-outs from previously delivered SparkPost mail to Bird after cutover. See [Suppressions](/docs/guides/email/suppressions) for native bounce and complaint handling.

## Translate webhook events

SparkPost sends [batched webhook events](https://developers.sparkpost.com/api/webhooks/) under `msys` wrappers. Bird delivers one event per request with `type`, `timestamp`, and `data`. [Register a Bird endpoint](/docs/guides/webhooks) with explicit event subscriptions and signature verification. Keep the SparkPost handler active for its remaining traffic.

| SparkPost event                | Bird event                 |
| ------------------------------ | -------------------------- |
| `injection`                    | `email.processed`          |
| `delivery`                     | `email.delivered`          |
| `delay`                        | `email.deferred`           |
| `bounce`                       | `email.bounced`            |
| `out_of_band`                  | `email.out_of_band_bounce` |
| `spam_complaint`               | `email.complained`         |
| Send-side failures (see below) | `email.rejected`           |
| `open`, `initial_open`         | `email.opened`             |
| `click`                        | `email.clicked`            |
| `link_unsubscribe`             | `email.unsubscribed`       |
| `list_unsubscribe`             | `email.list_unsubscribed`  |

Direct API and SMTP sends emit `email.accepted` before processing. Broadcasts record acceptance in the events API and email log but omit that webhook. SparkPost's `policy_rejection`, `generation_failure`, and `generation_rejection` map to `email.rejected`; inspect `rejection_reason`. Deduplicate opens separately when counting unique engagement.

Use `data.email_id` and `data.recipient_id` for Bird correlation, and carry your own identifiers in `metadata`. Replace SparkPost batch-ID handling with Bird's [webhook deduplication and ordering rules](/docs/guides/webhooks). Read bounce details before deciding whether an address should be suppressed; [bounce classification](/docs/guides/email/events#bounce-classification) distinguishes permanent address failures from temporary or policy failures.

## Preserve reporting history

Export the [SparkPost event history](https://developers.sparkpost.com/api/events/) and [aggregate reports](https://developers.sparkpost.com/api/metrics/) you need before their retention windows expire. Follow event pagination to completion and preserve provider IDs, account/subaccount scope, timestamps, and reporting filters. Keep collecting late events during the overlap, and retain SparkPost history in a separate archive.

Save a baseline for each sending stream. Compare matching recipient populations and reporting windows, and check [metric definitions](/docs/guides/email/tracking-and-metrics): provider acceptance, recipient-server delivery, unique engagement, and prefetched opens are different measures. Matching metric names alone do not establish comparable rates.

## Migrate inbound email separately

If you use [SparkPost relay webhooks](https://developers.sparkpost.com/api/relay-webhooks/), follow [Receiving email](/docs/guides/email/receiving-email) for that flow. Bird's `email.received` webhook supplies an `inbound_message_id`; fetch the body, attachments, or raw MIME through the API instead of expecting the full message in the webhook. Test your handler with a Bird forwarding address, then prepare domain receiving before changing MX records. Verify reply routing after the DNS change and archive content you need beyond Bird's receiving retention period.

## Verify and cut over

1. Verify each domain's sending capability. Run the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) and complaint cases. Confirm signed events reach your handler, correlate to the right message, and handle duplicate deliveries. Sandbox events do not prove inbox delivery, rendering, or tracking.
2. Send from your verified domain to controlled real inboxes. Check personalization, To/Cc/Bcc visibility, attachments, authentication, and tracking. Test an unsubscribe: marketing must stop while eligible transactional mail continues. Separately test that all-category blocks reject both. Keep these checks distinct from simulated sandbox outcomes.
3. Assign pending scheduled sends to one provider. Drain or cancel the original before recreating it elsewhere. Maintain an application record of which provider accepted each logical send so retries or rollback do not send a second copy.
4. Move a controlled portion of traffic and monitor [delivery metrics](/docs/guides/email/tracking-and-metrics) and webhook processing. For dedicated IPs, follow the migration plan agreed with our team, including any [warmup](/docs/guides/email/ip-warmup). Increase traffic after the observed results meet your delivery requirements.
5. If validation fails, pause the affected Bird traffic and route new sends through the retained SparkPost path with current opt-outs applied. Reconcile ambiguous sends before retrying them. Retire old credentials, webhooks, and DNS after queues and late events are accounted for; keep old tracking and unsubscribe links functional for previously delivered mail.

If authentication fails, check the Bird bearer token and region. If preference imports return `403`, check the key's `preferences` write permission before continuing. If personalization renders incorrectly, inspect Liquid conversion and parameters. If transactional mail is unexpectedly rejected, check imported manual suppressions. Use the [email log](/docs/guides/email/email-log) and event details to verify each correction.

## Next steps

- [Sending email](/docs/guides/email/sending-email): payload fields, personalization, and asynchronous outcomes
- [Email templates](/docs/guides/email/templates): preview, publication, and Liquid support
- [Suppressions](/docs/guides/email/suppressions): suppression reasons and management
- [Webhooks and events](/docs/guides/webhooks): signatures, retries, and replay

## Related resources

- [Getting started with email](/learn/email/getting-started-with-email) (video)
- [Email](/email-api) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=email)
