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.
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 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.