← Back to docs

Migrate from Resend

Move transactional email from Resend to EuroMail: API field mapping, code before/after, SMTP cutover, DNS, webhooks, and a zero-downtime checklist.

What changes, what doesn't

Both services are REST-first transactional email APIs, so the shape of your integration survives: one HTTP call per email, webhooks for delivery events, domain verification via DNS. What changes is the endpoint, the auth header, two field names, and where your data lives. EuroMail hosts email storage and AI processing in Finland; its DPA names ancillary services, including billing outside the EU. Resend remains the better choice in some situations; the honest comparison is at euromail vs Resend.

Plan for one dual-running window (both providers verified on your domain) and the cutover itself is a config change, not a rewrite.

API mapping

ResendEuroMail
EndpointPOST https://api.resend.com/emailsPOST https://api.euromail.dev/v1/emails
Auth headerAuthorization: Bearer re_...Authorization: Bearer em_live_...: same header, only the key changes (X-EuroMail-Api-Key also works)
HTML bodyhtmlhtml_body
Text bodytexttext_body
Recipientsto, cc, bcc, reply_toSame names. to accepts string or array, cc/bcc are arrays
Attachmentsattachments: [{ filename, content }]Same, plus an explicit content_type per attachment
Schedulingscheduled_atsend_at (RFC 3339). Response returns status: "scheduled"
IdempotencyIdempotency-Key header, 24 h expiryidempotency_key body field. Same key always returns the original result
Tagstags (array of objects)tags (array of strings) + free-form metadata object echoed on webhooks
Success response200 with { "id": ... }202 Accepted with { "id", "status": "queued", ... }

Two behavioural differences worth encoding in your client:

  • 202, not 200. EuroMail acknowledges that the email is queued, not delivered. If your code checks response.status === 200, change it to accept 202 (or any 2xx).
  • Idempotency moves from header to body. Generate the key from your own domain object (order id, invoice id) exactly as you did for Resend, only the transport changes. Details in Sending Emails.

Code before and after

curl:

# Before: Resend
curl -X POST https://api.resend.com/emails \
  -H "Authorization: Bearer re_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Welcome aboard",
    "html": "<h1>Welcome!</h1>",
    "text": "Welcome!"
  }'

# After: EuroMail
curl -X POST https://api.euromail.dev/v1/emails \
  -H "Authorization: Bearer em_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Welcome aboard",
    "html_body": "<h1>Welcome!</h1>",
    "text_body": "Welcome!"
  }'

TypeScript:

// Before: Resend
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome aboard",
  html: "<h1>Welcome!</h1>",
});

// After: EuroMail
import { EuroMail } from "@euromail/sdk";
const client = new EuroMail({ apiKey: process.env.EUROMAIL_API_KEY! });

await client.sendEmail({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome aboard",
  html_body: "<h1>Welcome!</h1>",
});

SDKs for Python, Rust, and Go follow the same field names as the REST API.

SMTP cutover

If you send through Resend's SMTP interface, the swap is four settings:

SettingResendEuroMail
Hostsmtp.resend.comsmtp.euromail.dev
Port25 / 465 / 587 / 2465 / 2587587 (STARTTLS) or 465 (implicit TLS)
Usernameresend (literal)apikey (literal)
Passwordyour API keyyour API key (em_live_...)

Most "SMTP provider" presets work unchanged. Full walkthroughs for WordPress, framework mailers, and legacy apps are in Send Email via SMTP.

DNS: verify your domain before switching

EuroMail rejects sends from unverified domains, so do this first, while Resend is still handling production traffic:

  1. Add your domain in the EuroMail dashboard and publish the DKIM, SPF, and DMARC records it gives you. Step-by-step in Domain Verification.
  2. Keep Resend's DNS records in place during the dual-run. DKIM records live under different selectors and SPF include:s coexist, so both providers stay verified simultaneously.
  3. After cutover, remove Resend's records on your own schedule, leaving them for a week or two is harmless and lets you roll back instantly.

Webhook event mapping

Resend eventEuroMail event
email.deliveredsent
email.bouncedbounced
email.delivery_delayeddeferred
email.complainedcomplained
email.openedopened
email.clickedclicked
email.sentNo equivalent. The 202 API response already confirms acceptance
email.failedfailed (refused for a reason other than the address, or stopped by a problem on our side). A refused address is bounced
email.receivedemail.inbound, sent for domains with inbound routing. See Inbound Email
email.scheduled / email.suppressedReflected in the send response (status: "scheduled") and the suppression list
contact.* / domain.*Managed via Contact Lists and the domains API rather than webhooks

Signature verification changes: Resend signs via Svix headers; EuroMail signs every payload with HMAC-SHA256 in the X-Euromail-Signature header. Swap your verification function for the one in the Webhooks guide.

Bring your suppression list

Carrying over suppressions protects your sender reputation from day one. addresses that bounced or complained on Resend should never get mail through EuroMail either. Export your suppressions from the Resend dashboard, then import them in bulk:

curl -X POST https://api.euromail.dev/v1/suppressions/import \
  -H "Authorization: Bearer em_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": ["[email protected]", "[email protected]"],
    "reason": "migrated from Resend: suppressed"
  }'

One request takes up to 10,000 addresses, so split a bigger export into chunks. The response reports how many were added and lists any address it could not parse. The key needs the contacts:manage scope.

Details in Suppression Lists.

Your first 24 hours

Every new account spends its first 24 hours -- counted from account creation, not from your first send -- inside a probation window with two limits designed to stop day-one spam blasts without starving a real migration:

  • A send-rate cap: 5 emails per minute and 25 per hour on day one, loosening over the first week.
  • A recipient allowance: a token bucket of 20 that refills at 2 per hour, counted per deliverable recipient (to plus every cc and bcc). A settled, non-zero invoice raises the allowance to 1,000 -- a paid plan still inside its trial does not count as a payment. The allowance lifts entirely once the account is 24 hours old.

A refused send returns 429 with a retry-after header (the SMTP relay returns 452, which well-behaved mailers retry). Test sends from a not-yet-verified domain to your own address (sandbox mode) never consume the allowance, so integration testing is free.

The practical consequence for a migration: create your EuroMail account and verify your domain at least a day before cutover, so the recipient allowance has expired by the time production traffic moves. If you must cut over with real volume on day one, settle your first invoice first (1,000 recipients covers a typical day) or keep Resend running in parallel.

After that, the account's age and payments set the pace:

WhenSend rateMonthly volume
First 24 hours5/minute, 25/hour, plus the recipient allowance above3,000
Days 2 to 715/minute, 75/hour3,000 until your first invoice is paid
From day 8100/minute, 500/hourYour plan's volume once an invoice has been paid

A paid plan that is still in its trial counts as unpaid: until a non-zero invoice has been paid, the account is held to the free allowance of 3,000 emails a month whatever plan it is on. Sends over a limit return 429. Plan the cutover around these numbers: if your normal hourly volume is above the rate for the week you cut over in, keep the old provider carrying part of the traffic until it fits.

Honest gaps

  • React Email doesn't port. Resend's React component rendering is a real innovation and EuroMail has no equivalent. If your templates are React components, render them to HTML in your app (React Email's render() works fine) and send the result as html_body, or move them to EuroMail's template engine.
  • Broadcasts/Audiences map, not clone. Resend Audiences become Contact Lists; marketing sends go through Newsletters with separate marketing suppression semantics.
  • The full feature-by-feature comparison, including where Resend is the better choice, is at euromail vs Resend.

Cutover checklist

  1. Create a EuroMail account at least 24 hours before cutover (probation limits expire with account age) and an API key with the emails:send and contacts:manage scopes (API Keys).
  2. Verify your sending domain; confirm DKIM/SPF/DMARC show green in the dashboard.
  3. Register your webhook endpoint and swap the signature verification.
  4. Import suppressions from your Resend export with POST /v1/suppressions/import.
  5. Switch the endpoint, auth header, and html/text → html_body/text_body (or the four SMTP settings), accepting 202 as success.
  6. Check your volume against the limits for the week you cut over in (see above), and pay the first invoice if you need more than 3,000 emails this month. For bulk jobs, send up to 500 messages per request with POST /v1/emails/batch and back off on 429 (wait for retry-after when the response has one).
  7. Send a staging email end-to-end; confirm the sent webhook arrives.
  8. Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
  9. Keep the Resend account read-only for a rollback window, then retire it.

Send your first email in about 90 seconds

The free tier includes 3,000 emails a month. Email storage and AI processing in Finland, with a DPA on every plan.

Create free account See live delivery data