Migrate from SparkPost
Use this guide to move outbound email from SparkPost to Bird. Follow the main migration checklist, 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 and API key in your chosen region. 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 when replacing a SparkPost client, and review its retries, timeouts, and pagination.
If you use SparkPost subaccounts, contact us 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 and rate limits cover your sending volume, resource counts, and peak traffic.
Register your 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 before removing an active domain.
Dedicated IPs: Contact us 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 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, 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:
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, or POST /v1/email/batches for independent messages. The SparkPost API overview 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. To-only sends remain individually addressed; check visible headers when adding Cc/Bcc copies.
Map the SparkPost transmission fields:
| 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 |
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 |
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 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, scheduling, and attachment rules. Update inline-image IDs and matching cid: references to meet Bird's rules. Configure the return path and 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 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 example needs no verified domain and reaches no real inbox. For an EU key, use https://eu1.platform.bird.com:
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. Acceptance does not establish delivery: a suppressed recipient can be accepted and later rejected. Update response parsing, error handling, and idempotent retries 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, 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 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, username bird, and an API key with email sending enabled. Check the region, TLS, and key configuration.
Convert SparkPost's X-MSYS-API options before removing the header. Set category, tags, tracking, and pool defaults in Bird's SMTP 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, 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: 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 in the inventory.
SparkPost's 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, then follow 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; 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 with GET /api/v1/recipient-lists/{id}?show_recipients=true to include membership and personalization. Create destination audiences and register 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 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 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.
Classify records by type, source, and scope before using the main guide's import loop. Bird's POST /v1/email/suppressions 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/preferenceswithchannel: "email", the address inhandle,status: "revoked", andcoverage: "non_transactional". Setsource: "sparkpost-migration"for reconciliation. Review existing Bird preferences first and preserve stricter restrictions; inspectappliedand 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: falsefor 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 for native bounce and complaint handling.
Translate webhook events
SparkPost sends batched webhook events under msys wrappers. Bird delivers one event per request with type, timestamp, and data. Register a Bird endpoint 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. Read bounce details before deciding whether an address should be suppressed; bounce classification distinguishes permanent address failures from temporary or policy failures.
Preserve reporting history
Export the SparkPost event history and aggregate reports 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: 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, follow 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
- Verify each domain's sending capability. Run the sandbox smoke test 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.
- 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.
- 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.
- Move a controlled portion of traffic and monitor delivery metrics and webhook processing. For dedicated IPs, follow the migration plan agreed with our team, including any warmup. Increase traffic after the observed results meet your delivery requirements.
- 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 and event details to verify each correction.
Next steps
- Sending email: payload fields, personalization, and asynchronous outcomes
- Email templates: preview, publication, and Liquid support
- Suppressions: suppression reasons and management
- Webhooks and events: signatures, retries, and replay
Related resources
Continue with the documentation, guides and examples for this topic.