Migrate from Mailjet
This page maps Mailjet's Send API v3.1 payload, blocklist, and Event API to Bird. Follow the main migration guide in order, and use these mappings for steps 1, 3, and 4.
The biggest structural change is the envelope. Mailjet wraps every send in a Messages array of PascalCase objects (POST /v3.1/send). We take one flat, lowercase JSON object per POST /v1/email/messages, and many independent messages go to the batch endpoint instead of the Messages array.
Hand this to your agent
Paste this into Claude Code, Cursor, or Codex. The agent works through this page against your own repository, using whichever Bird surface it already has: the MCP server if one is connected, the CLI if it is installed and signed in.
Exemple de code
I am moving an email integration from Mailjet to Bird. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/email/migrate/mailjet.md for the payload, suppression and event mapping, and https://bird.com/docs/guides/email/migrate.md for the order the steps go in.
3. Find and list my Mailjet usage in this repository before you change anything: the POST /v3.1/send call sites and any SDK wrappers around them, the event handler and the URL it is registered at, and every domain I send from.
4. Register each of those sending domains with Bird and give me the DNS records to publish, following https://bird.com/docs/guides/email/sending-domains.md. Leave every DNS record my current provider uses exactly as it is: Bird's records are published alongside them and both providers authenticate side by side until I switch traffic. Publishing DNS affects mail for the whole domain, so show me the records and let me publish them.
5. Export my blocklist from Mailjet and import it into Bird before any production traffic goes through Bird, so my first sends do not reach addresses that already bounced or complained. Read it through the contact-management API, or from the contact statistics pages if that is what I have access to. The Bird import takes one address per request and is idempotent, so a partial re-run is safe. https://bird.com/docs/guides/email/suppressions.md has the reason taxonomy.
6. Port the send call and the event handler using the mapping tables on the provider page. Mailjet posts a Messages array of PascalCase objects, and each entry becomes either one flat Bird send or one entry in a batch, so tell me which shape my call sites map onto before you rewrite them. EventPayload becomes metadata. Bird signs deliveries per Standard Webhooks rather than Mailjet's scheme, so treat verification as a rewrite rather than a URL change: https://bird.com/docs/guides/webhooks.md and https://bird.com/docs/guides/email/events.md.
7. Run my whole integration against Bird's mail sandbox before any production traffic, following https://bird.com/docs/guides/email/testing-sandbox.md. Sandbox sends run the real pipeline without reaching an inbox or touching my sending reputation.
8. Stop and ask me wherever a step needs a decision. Do not point production traffic at Bird until I have seen the sandbox results and replied with the words cut over to Bird. Retiring the Mailjet path is a separate step that comes later: ask me again and wait for me to reply with the words retire the Mailjet path. A reply that agrees without naming what it is authorising is not authorisation. Finish by telling me what is left that only a person can do.Map the send call
| What it does | Mailjet (Send API v3.1) | Bird |
|---|---|---|
| Sender | From: { "Email", "Name" } | from: string or { "email", "name" } |
| Recipients | To / Cc / Bcc: [{ "Email", "Name" }] | to / cc / bcc: arrays |
| Subject | Subject | subject |
| Body | TextPart / HTMLPart | text / html (at least one) |
| Reply-to | ReplyTo: { "Email", "Name" } | reply_to: array |
| Custom headers | Headers | headers: string → string object |
| Round-trip context | EventPayload (string), echoed on events | metadata: arbitrary JSON |
| Your own send ID | CustomID | Idempotency-Key header + tags/metadata |
| Stored template | TemplateID + Variables | template + template.parameters |
| Attachments | Attachments: { "ContentType", "Filename", "Base64Content" } | attachments: { "content_type", "filename", "content" } |
| Inline images | InlinedAttachments, with ContentID | attachments with content_id |
| Tracking | account/template setting | track_opens / track_clicks (default true) |
| Category | (none) | category: marketing (default) or transactional |
Our field caps and defaults (recipient counts, tag and metadata limits) live in Sending email.
Porting notes:
- Unwrap the Messages array. A single Mailjet send is one entry in Messages. Here, that's the whole request body. A Messages array with several entries maps to our batch endpoint. Repeated fields in one request cannot represent the batch.
- Case changes from PascalCase to lowercase. Every field renames: HTMLPart → html, TextPart → text, From.Email → from.email. This is mechanical but touches every send.
- EventPayload becomes metadata. Mailjet echoes a single EventPayload string back on every event. We echo structured metadata (JSON) and tags on every webhook event, so you can split correlation data into typed fields. See tags vs metadata.
- CustomID splits in two. Mailjet's CustomID does double duty as a dedup key and a correlation handle. Here, use the Idempotency-Key header for safe retries and put your own correlation ID in metadata.
- Stored templates port directly. Mailjet's TemplateID + Variables map to our template field (referenced by ID or slug) with values in template.parameters. See sending with a template. Templating logic beyond variable substitution ports too: Mailjet's TemplateLanguage conditionals and loops become Liquid {% if %} and {% for %} in our template.
- Attachments port directly. Mailjet's Base64Content is our base64 content, and InlinedAttachments + ContentID become attachments entries with content_id. See attachments.
Export suppressions
Mailjet keeps unreachable and unwanted addresses on its blocklist (hard/soft bounces and blocked sends) and tracks spam and unsubscribe signals separately. Export the blocked and bounced addresses from Mailjet's contact statistics pages, or pull them through the contact management API, and run the list through the import loop. If you send marketing mail, also carry over contacts marked unsubscribed so those preferences survive the move.
Translate webhook events
Mailjet's Event API posts one trigger per event type. The mapping to our event vocabulary:
| Outcome | Mailjet | Bird |
|---|---|---|
| Accepted/processed | (none) | email.accepted → email.processed |
| Delivered | sent | email.delivered |
| Permanent bounce | bounce | email.bounced / email.out_of_band_bounce |
| Blocked | blocked | email.rejected |
| Spam complaint | spam | email.complained |
| Open | open | email.opened |
| Click | click | email.clicked |
| Unsubscribe | unsub | email.unsubscribed / email.list_unsubscribed |
Two differences worth coding for:
- We report the pre-delivery stages explicitly. Mailjet's sent fires once the recipient's mail server takes the message, which corresponds to our email.delivered. We also emit email.accepted and email.processed before it, so you see a send progressing before the delivery confirmation. Don't treat those earlier events as delivery.
- Events are recipient-scoped. Mailjet keys events by MessageID. Our delivery events have recipient_id alongside email_id, so a multi-recipient send produces one event stream per recipient. We sign deliveries per the Standard Webhooks spec. See Webhooks & events for verification.
Cut over
Work through domains & DNS and the sandbox smoke test in the main guide. Both are provider-independent.
Next steps
- Sending domains: registration, verification lifecycle, and the DNS records you're re-pointing
- Webhooks & events: endpoint setup and Standard Webhooks verification
- Testing sandbox: smoke-test the new integration before cutover
- Suppressions: confirm your imported list and how we maintain it from here