# Migrate from another provider

This guide walks you through moving production email to Bird. The shape of the work is the same regardless of where you're coming from: one send endpoint to re-map, a handful of DNS records to publish, a suppression list to carry over, a webhook vocabulary to translate, and a sandbox to verify everything against before you flip traffic.

The migration checklist:

1. [Map your send call](#1-map-the-send-call) to `POST /v1/email/messages`
2. [Re-point your sending domains and DNS](#2-re-point-domains-and-dns)
3. [Import your suppression list](#3-import-suppressions)
4. [Switch webhooks to Bird's event vocabulary](#4-switch-webhooks)
5. [Verify against the mail sandbox](#5-verify-in-the-sandbox-before-cutover) before cutover

Steps 1, 3, and 4 depend on which provider you're leaving; your [provider guide](#migrating-from-a-specific-provider) carries the field-by-field payload mapping, where to export your suppression list, and the webhook event-name translation table.

## 1. Map the send call

Bird has one single-send endpoint, [`POST /v1/email/messages`](/docs/api/reference/create-email-message). You build a flat JSON payload (no `personalizations` wrapper, no MIME assembly) with `from`, `to`/`cc`/`bcc` arrays, `subject`, `html` and/or `text`, an optional `reply_to` list, and `headers` for custom email headers. A successful send returns `202 Accepted` with an `em_`-prefixed message ID; delivery outcomes arrive asynchronously through webhooks and the read endpoints. The full payload, with every field cap and default, is in [Sending email](/docs/guides/email/sending-email); the field-by-field mapping from your current payload is in your [provider guide](#migrating-from-a-specific-provider). If your application submits over SMTP today, you may not need to port the call at all: Bird [accepts SMTP submission](/docs/guides/email/smtp) into the same pipeline, which turns this step into a credentials swap.

The distinction between `tags` and `metadata` matters more on Bird than the equivalents did at your old provider: tags are first-class filter dimensions (message list, analytics, dashboard rollups), while metadata is opaque round-trip context, stored on the message, returned on API reads, and echoed on every webhook event. The rule of thumb and the caps are in [Tags vs metadata](/docs/guides/email/sending-email#tags-vs-metadata).

**Bird differences worth flagging before you port code:**

- **Scheduling and stored templates are supported; a few fields are still reserved.** `scheduled_at` schedules a send for later ([scheduled sending](/docs/guides/email/scheduled-sending)), `template` sends a stored template in place of inline content ([sending with a template](/docs/guides/email/sending-email#sending-with-a-template)), and attachments port to Bird's [attachments](/docs/guides/email/attachments) array. Three request fields (`contact_id`, `topic_id`, `in_reply_to_message_id`) remain reserved and return a `422` (`UnsupportedEmailFeature`); see [reserved fields](/docs/guides/email/sending-email#reserved-fields).
- **Suppressed recipients are rejected, visibly.** A suppressed address still gets a `recipient_id` and appears in the message's recipient list with status `rejected` and an `email.rejected` event (`rejection_reason: recipient_suppressed`), never a silent drop. Even when every recipient is suppressed, the request is still accepted with a `202`; each recipient comes back rejected. See [Suppressions](/docs/guides/email/suppressions).
- **Set `category: "transactional"` for operational mail.** A send defaults to `marketing`, and the category controls suppression policy: `marketing` blocks on complaints and unsubscribes, `transactional` delivers through them. Newsletters and campaigns are handled correctly by the default; mark receipts, password resets, and similar operational mail `transactional` so they are not gated by an unsubscribe.

## 2. Re-point domains and DNS

Register each sending domain with [`POST /v1/email/domains`](/docs/api/reference/create-domain) or on the [**Domains**](https://bird.com/dashboard/w/email/domains) page in the dashboard, then publish the records from the response's `dns_records` array. Sending is gated on three of them: the DKIM record, the return-path CNAME, and a DMARC policy (an existing DMARC record, including one at a parent domain, already counts). The tracking CNAME gates only branded open/click tracking, never sending. The record-by-record table, the verification lifecycle, and the regional model are in [Sending domains](/docs/guides/email/sending-domains); the [DNS record splitter](/tools/dns-record-splitter) breaks the long DKIM value into quoted strings if your DNS provider requires that, and the [DMARC policy generator](/tools/dmarc-policy) writes a policy if you don't have one yet.

One record most providers make you start with is deliberately absent: you publish no SPF record at your domain apex. SPF is evaluated against the envelope-from domain, which the return-path CNAME points at Bird's bounce infrastructure, so SPF passes and aligns without touching your apex. If your old provider had you add an `include:` to your apex SPF record, leave it in place during the transition and remove it after cutover; it neither helps nor hurts your Bird mail, and removing it frees one of the 10 DNS lookups apex SPF allows. The full explanation is in [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc#wheres-spf).

You can publish Bird's records while your old provider's records are still live: the DKIM record uses a Bird-specific selector, the return-path and tracking CNAMEs are new hostnames you choose, and your existing DMARC record satisfies the gate as-is. Both providers authenticate side by side until you're ready to switch traffic. Domain state is regional, so register the domain in every Bird region you send from.

## 3. Import suppressions

Carry your suppression list over **before** sending production traffic through Bird. Otherwise your first sends go to addresses that already bounced or complained at your old provider, which damages the reputation you're trying to protect.

Export the list from your current provider (your [provider guide](#migrating-from-a-specific-provider) has the exact endpoints), then add each address to Bird with [`POST /v1/email/suppressions`](/docs/api/reference/create-suppression):

```bash
while read -r address; do
  curl -s -X POST https://us1.platform.bird.com/v1/email/suppressions \
    -H "Authorization: Bearer $BIRD_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"email\": \"$address\"}"
done < suppressions.txt
```

Two things to know about this import path:

- **No bulk-import endpoint exists yet.** The v1 suppressions API is single-entry CRUD, one address per `POST`, so importing a large list means looping, as above. Bulk import is deferred to a later release; until then, the loop is the supported path. The call is idempotent (`201` for a new record, `200` with the existing record if the address is already manually suppressed), so re-running a partial import is safe.
- **Imported addresses get `reason: manual`, `applies_to: all`**, which blocks every category, including transactional. That is stricter than a complaint or unsubscribe record would be natively (those block only non-transactional sends), so if you need the category-aware behavior for specific addresses, see the reason taxonomy in [Suppressions](/docs/guides/email/suppressions#the-four-reasons-and-what-they-block).

Going forward you don't manage bounces yourself: Bird auto-suppresses hard bounces, complaints, and unsubscribes, and fires `email_suppression.created` so your systems can mirror the list.

## 4. Switch webhooks

Register an endpoint with [`POST /v1/webhooks`](/docs/api/reference/create-webhook) and subscribe it to an explicit list of event types. Bird's event names follow `resource.action`: `email.accepted` → `email.processed` → `email.delivered` on the happy path, with `email.deferred`, `email.bounced`, `email.complained`, `email.rejected`, `email.opened`, `email.clicked`, and the unsubscribe pair covering the rest. The event-name translation from your current provider's vocabulary is in your [provider guide](#migrating-from-a-specific-provider); per-event payload schemas are in the [events reference](/docs/guides/email/events).

Correlation ports cleanly. Every event carries the `email_id`, `recipient_id`, and `workspace_id` identifiers, and echoes the `tags` and `metadata` from the send request, so the context your old provider handed back through payload echo comes back on every Bird event too, without an extra lookup. Put your internal IDs in `metadata` on the send and read them straight off each event.

Bird signs deliveries per the [Standard Webhooks](https://www.standardwebhooks.com) specification: `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers carrying an HMAC-SHA256 over `{id}.{timestamp}.{raw body}`. If you already verify Standard Webhooks deliveries from another platform, the exact same verification code works for Bird; otherwise the verification recipe, retry schedule, and replay tooling are in [Webhooks & events](/docs/guides/webhooks). Deliveries are at-least-once and unordered, so deduplicate on `webhook-id` and sort by the payload `timestamp`, the same discipline your current handler should already have.

## 5. Verify in the sandbox before cutover

Before you move production traffic, run your full integration (the ported send call, your webhook handler, your suppression mirroring) against the [mail sandbox](/docs/guides/email/testing-sandbox). Sandbox sends go to magic addresses at `messagebird.dev` and run through the real production pipeline: same `202`, same event sequence, same signed webhook deliveries, without ever reaching an inbox or touching your reputation.

A minimal pre-cutover smoke test:

1. Send to `delivered@messagebird.dev` and assert your handler processes `email.accepted` → `email.processed` → `email.delivered`.
2. Send to `bounce@messagebird.dev` and assert your bounce handling fires on `email.bounced` (simulated bounces don't write to your suppression list, so the address stays reusable).
3. Send to `suppressed@messagebird.dev` and assert you handle `email.accepted` followed by `email.rejected`, with no `email.processed` or delivery events after it. That is the shape every suppressed recipient produces in production; the `rejection_reason: recipient_suppressed` detail is on the recipient record and the events API.
4. Send a `category: "marketing"` message and confirm the category shows up where you expect on the message read.

Use `+label` subaddressing (`bounce+cutover-test@messagebird.dev`) to correlate test cases; the full address appears in your events. Once the smoke test passes, cut traffic over: point your application at Bird, keep the old provider's DNS in place until your Bird domains show `capabilities.sending` verified, and watch the first hours of real deliveries in the dashboard and your webhook stream.

## Migrating from a specific provider

- [SendGrid](/docs/guides/email/migrate/sendgrid): `personalizations` → flat payload, `categories`/`custom_args` → `tags`/`metadata`, the `dropped` ↔ `email.rejected` equivalence
- [Mailgun](/docs/guides/email/migrate/mailgun): `o:*`/`v:*`/`h:*` parameters → first-class fields, bounces/complaints/unsubscribes export
- [Amazon SES](/docs/guides/email/migrate/ses): SendEmail v2 → one endpoint, configuration sets → per-message tracking flags, SNS → signed webhooks
- [Resend](/docs/guides/email/migrate/resend): near-identical payload shape, Svix-signed webhooks → Standard Webhooks
- [Mailjet](/docs/guides/email/migrate/mailjet): the `Messages` array → one flat payload, `EventPayload` → `metadata`, blocklist export
- [Mandrill](/docs/guides/email/migrate/mandrill): the `message` wrapper and body auth → flat payload and bearer auth, rejection blacklist export

## Next steps

- [Sending email](/docs/guides/email/sending-email): the full send payload, tags vs metadata, the async 202 model
- [Sending domains](/docs/guides/email/sending-domains): registration, verification lifecycle, multi-region setup
- [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc): what each record proves, and why apex SPF isn't required
- [Suppressions](/docs/guides/email/suppressions): reasons, categories, and the management API
- [Webhooks & events](/docs/guides/webhooks): endpoint setup, Standard Webhooks verification, retries and replay
- [Events](/docs/guides/email/events): per-event payload schemas
- [Testing sandbox](/docs/guides/email/testing-sandbox): the full magic-address list and walkthroughs
- [API reference](/docs/api/reference/create-email-message): request and response schemas for the send endpoint