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 processes and stores everything in Finland. 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
| Resend | EuroMail | |
|---|---|---|
| Endpoint | POST https://api.resend.com/emails | POST https://api.euromail.dev/v1/emails |
| Auth header | Authorization: Bearer re_... | X-EuroMail-Api-Key: em_live_... |
| HTML body | html | html_body |
| Text body | text | text_body |
| Recipients | to, cc, bcc, reply_to | Same names. to accepts string or array, cc/bcc are arrays |
| Attachments | attachments: [{ filename, content }] | Same, plus an explicit content_type per attachment |
| Scheduling | scheduled_at | send_at (RFC 3339). Response returns status: "scheduled" |
| Idempotency | Idempotency-Key header, 24 h expiry | idempotency_key body field. Same key always returns the original result |
| Tags | tags (array of objects) | tags (array of strings) + free-form metadata object echoed on webhooks |
| Success response | 200 with { "id": ... } | 202 Accepted with { "id", "status": "queued", ... } |
Two behavioural differences worth encoding in your client:
202, not200. EuroMail acknowledges that the email is queued, not delivered. If your code checksresponse.status === 200, change it to accept202(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 "X-EuroMail-Api-Key: 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.emails.send({
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:
| Setting | Resend | EuroMail |
|---|---|---|
| Host | smtp.resend.com | smtp.euromail.dev |
| Port | 25 / 465 / 587 / 2465 / 2587 | 587 (STARTTLS) or 465 (implicit TLS) |
| Username | resend (literal) | apikey (literal) |
| Password | your API key | your 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:
- Add your domain in the EuroMail dashboard and publish the DKIM, SPF, and DMARC records it gives you. Step-by-step in Domain Verification.
- 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. - 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 event | EuroMail event |
|---|---|
email.delivered | delivered |
email.bounced | bounced |
email.delivery_delayed | deferred |
email.complained | complained |
email.opened | opened |
email.clicked | clicked |
email.sent | No equivalent. The 202 API response already confirms acceptance |
email.failed | No single event. Permanent failures surface as bounced; undeliverable jobs land in the dead-letter queue |
email.received | Inbound is a route, not a webhook event. See Inbound Email |
email.scheduled / email.suppressed | Reflected 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 replay them:
curl -X POST https://api.euromail.dev/v1/suppressions \
-H "X-EuroMail-Api-Key: em_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"email_address": "[email protected]",
"reason": "migrated from Resend: hard bounce"
}'
One call per address (a ten-line loop over your export). 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 (
toplus 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. By the time production traffic moves, the recipient allowance has expired and only the loosened week-one rate cap (15/minute, 75/hour) remains. 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 for the first 24 hours.
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 ashtml_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
- Create a EuroMail account at least 24 hours before cutover (probation limits expire with account age) and an API key with the
emails:sendscope (API Keys). - Verify your sending domain; confirm DKIM/SPF/DMARC show green in the dashboard.
- Register your webhook endpoint and swap the signature verification.
- Import suppressions from your Resend export.
- Switch the endpoint, auth header, and
html/text→html_body/text_body(or the four SMTP settings), accepting202as success. - Send a staging email end-to-end; confirm the
deliveredwebhook arrives. - Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
- Keep the Resend account read-only for a rollback window, then retire it.