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:
- Map your send call to POST /v1/email/messages
- Re-point your sending domains and DNS
- Import your suppression list
- Switch webhooks to Bird's event vocabulary
- Verify against the mail sandbox before cutover
Steps 1, 3, and 4 depend on which provider you're leaving; your provider guide 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. 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: Bird accepts SMTP submission 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.
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), template sends a stored template in place of inline content (sending with a template), and attachments port to Bird's attachments array. Three request fields (contact_id, topic_id, in_reply_to_message_id) remain reserved and return a 422 (UnsupportedEmailFeature); see 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.
- 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 on the 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; the DNS record splitter breaks the long DKIM value into quoted strings if your DNS provider requires that, and the DMARC policy generator 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.
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 has the exact endpoints), then add each address to Bird with POST /v1/email/suppressions:
Codebeispiel
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:
- 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.
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 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; per-event payload schemas are in the events reference.
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 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. 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 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: 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