Migrate from another provider
Use this guide to move production email from another provider. Map the send request, publish DNS records, import suppressions, translate webhook events, and test the integration before directing production traffic to Bird.
The migration checklist:
- Map your send call to POST /v1/email/messages
- Re-point your sending domains and DNS
- Import your suppression list
- Switch webhooks to our event vocabulary
- Verify against the mail sandbox before cutover
Steps 1, 3, and 4 depend on which provider you're leaving. Your provider guide has the field-by-field payload mapping, where to export your suppression list, and the webhook event-name translation table.
1. Map the send call
We have one single-send endpoint, POST /v1/email/messages. 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. The field-by-field mapping from your current payload is in your provider guide. If your application submits over SMTP today, you may not need to port the call at all: we accept SMTP submission into the same pipeline, which turns this step into a credentials swap.
Use tags for filter dimensions in message lists, analytics, and dashboard rollups. Use metadata for structured context that Bird stores on the message, returns on API reads, and echoes on webhook events. See Tags vs metadata for limits.
Before you port code, account for these differences:
- Scheduling, stored templates, and attachments all port. Use scheduled_at for scheduled sending. Use template instead of inline content for stored templates. Map files to the attachments array.
- 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.
- 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 or in Email > Domains, then publish the records from dns_records. DKIM, the return-path CNAME, and a DMARC policy gate sending. An existing DMARC record, including one at a parent domain, counts. The tracking CNAME gates only branded open and click tracking. Sending domains covers the records, verification lifecycle, and regional model. Use the DNS record splitter if your provider requires a split DKIM value, and the DMARC policy generator if you need a policy.
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 us, 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 mail sent through us, and removing it frees one of the 10 DNS lookups apex SPF allows. The full explanation is in DKIM, SPF & DMARC.
You can publish our records while your old provider's records are still live. The DKIM record uses a selector of ours. 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 region you send from.
3. Import suppressions
Carry your suppression list over before sending production traffic through us. 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 has the exact endpoints), then add each address here with POST /v1/email/suppressions:
Code example
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.txtTwo things to know about this import path:
- You import one address per request. The suppressions API is single-entry CRUD, so a large list means looping over the exported addresses. 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.
Going forward you don't manage bounces yourself: we auto-suppress hard bounces, complaints, and unsubscribes, and fire email_suppression.created so your systems can mirror the list.
4. Switch webhooks
Register an endpoint with POST /v1/webhooks and subscribe it to an explicit list of event types. Our 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. Per-event payload schemas are in the events reference.
Correlation ports cleanly. Every event has the email_id, recipient_id, and workspace_id identifiers. It also echoes the tags and metadata from the send request. This returns the context your old provider handed back through payload echo without an extra lookup. Put your internal IDs in metadata on the send and read them straight off each event.
We sign deliveries per the Standard Webhooks specification, using three headers: webhook-id, webhook-timestamp, and webhook-signature, with an HMAC-SHA256 over {id}.{timestamp}.{raw body}. If you already verify Standard Webhooks deliveries from another platform, the exact same verification code works here. Otherwise, the verification recipe, retry schedule, and replay tooling are in Webhooks & events. 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. 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:
- Send to delivered@messagebird.dev and assert your handler processes email.accepted → email.processed → email.delivered.
- 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).
- 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.
- 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 us, and keep the old provider's DNS in place until your domains here show capabilities.sending verified. Watch the first hours of real deliveries in the dashboard and your webhook stream.
Migrating from a specific provider
- SendGrid: personalizations → flat payload, categories/custom_args → tags/metadata, the dropped ↔ email.rejected equivalence
- Mailgun: o:*/v:*/h:* parameters → first-class fields, bounces/complaints/unsubscribes export
- Amazon SES: SendEmail v2 → one endpoint, configuration sets → per-message tracking flags, SNS → signed webhooks
- Resend: near-identical payload shape, Svix-signed webhooks → Standard Webhooks
- Mailjet: the Messages array → one flat payload, EventPayload → metadata, blocklist export
- Mandrill: the message wrapper and body auth → flat payload and bearer auth, rejection blacklist export
Next steps
- Sending email: the full send payload, tags vs metadata, the async 202 model
- Sending domains: registration, verification lifecycle, multi-region setup
- DKIM, SPF & DMARC: what each record proves, and why apex SPF isn't required
- Suppressions: reasons, categories, and the management API
- Webhooks & events: endpoint setup, Standard Webhooks verification, retries and replay
- Events: per-event payload schemas
- Testing sandbox: the full magic-address list and walkthroughs
- API reference: request and response schemas for the send endpoint