Migrating from Resend.
Irisend's REST API, error catalogue, and wire shapes are modelled directly on Resend's — most integrations move by changing a base URL and an API key, nothing else. This page is the exhaustive list of what's a pure drop-in and the handful of places behavior genuinely differs, so you don't have to diff the two APIs yourself.
What's a drop-in
REST API — base URL and key only
Every documented endpoint (see the API reference) matches Resend's request/response shape: same snake_case fields, the same { statusCode, name, message } error envelope, the same cursor-pagination contract, the same Standard Webhooks/Svix-compatible signing.
# Resend
curl -X POST https://api.resend.com/emails \
-H "Authorization: Bearer re_xxxx" \
-H "Content-Type: application/json" \
-d '{ "from": "[email protected]", "to": "[email protected]", "subject": "Hi", "html": "<p>Hi</p>" }'
# Irisend — only the host and the key change
curl -X POST https://api.irisend.com/emails \
-H "Authorization: Bearer ir_xxxx" \
-H "Content-Type: application/json" \
-d '{ "from": "[email protected]", "to": "[email protected]", "subject": "Hi", "html": "<p>Hi</p>" }'SMTP — host, password, done
smtp.irisend.com runs the identical send pipeline as POST /emails. The username can stay resend — Irisend accepts it as an alias specifically so an existing Nodemailer/Postfix config only needs its host and password changed.
// Resend // Irisend
host: "smtp.resend.com", host: "smtp.irisend.com",
auth: { auth: {
user: "resend", user: "resend", // unchanged — works as an alias
pass: "re_xxxx", pass: "ir_xxxx", // your Irisend API key
}, },Node SDK — same return shape
@irisend/sdk mirrors the `resend` package's { data, error } return shape and batch.send alias — most call sites only need the import and the client construction to change.
// Resend's own SDK: npm install resend
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
// Irisend's SDK: npm install @irisend/sdk — same { data, error } shape, same batch.send alias
import { Irisend } from "@irisend/sdk";
const irisend = new Irisend(process.env.IRISEND_API_KEY!);
const { data, error } = await irisend.emails.send({
from: "[email protected]",
to: "[email protected]",
subject: "Hi",
html: "<p>Hi</p>",
});Where it differs
Everything below is a real, confirmed behavioral difference — grounded in the actual validation and serialization code, not a guess.
scheduled_at is ISO 8601 only
Resend also accepts natural language ("in 1 min", "tomorrow at 9am"). Irisend's scheduled_at (on POST /emails, PATCH /emails/:id, and broadcasts) only accepts a strict ISO 8601 datetime with a UTC offset — a natural-language string gets a 422 validation_error instead of being parsed.
Timestamps use T…Z with milliseconds everywhere, not just in webhooks
Resend's own REST responses format timestamps as Postgres text ("2026-04-03 22:13:42.674981+00" — a space separator, microsecond precision, +00 offset) and only use true ISO 8601 (T…Z, millisecond precision) in webhook payloads. Irisend uses T…Z with millisecond precision everywhere: REST responses, /logs, and webhooks alike. Both are valid UTC and parse fine with new Date(...), but a byte-for-byte response diff against Resend's REST API will show a different timestamp format.
Suppression origin folds unsubscribe into manual
Resend's origin enum on a suppression is bounce | complaint | manual. Irisend tracks a 4th internal reason, unsubscribe (one-click/consent withdrawal), but surfaces it on the wire as manual to match Resend's 3-value enum — filtering GET /suppressions?origin=manual matches both internal reasons. The dashboard shows the finer-grained reason; the API never does.
SMTP username resend works as a drop-in alias
Point an existing Nodemailer/Postfix config at smtp.irisend.com and change only the host and password (an Irisend API key) — the username can stay resend (irisend also works). One real gap: Irisend's SMTP relay reads the idempotency key from a plain Idempotency-Key message header, not Resend's Resend-Idempotency-Key — rename that header when porting an SMTP integration that relies on idempotency.
Batch sending has no x-batch-validation header or partial-success mode
Resend's POST /emails/batch supports an x-batch-validation: strict|permissive header to control whether one bad item fails the whole batch. Irisend always validates the whole array up front — any invalid item (or any item carrying attachments/scheduled_at, which batch doesn't support at all) rejects the entire request with 422 validation_error before anything is sent.
Smaller error catalogue
Irisend doesn't implement every Resend error name — missing ones include invalid_attachment, missing_required_parameter, email_above_quota, resource_locked, suspended_api_key, and service_unavailable. It also adds two Irisend-only names with no Resend equivalent: domain_reputation_paused and account_suspended. The {statusCode, name, message} envelope itself is identical. See Errors below for the full catalogue.
No User-Agent-required 403
Resend rejects a request with no User-Agent header. Irisend never checks for one — it's only read for the dashboard's request log, never enforced.
Domain health, template versioning, DMARC monitoring, and sub-accounts are Irisend-only
GET /domains/:id/health, GET /domains/:id/dmarc/*, GET /templates/:id/versions + rollback, and the whole /accounts (sub-account) resource have no Resend equivalent at all — purely additive, nothing to migrate away from.
A send, byte for byte
The same send, generated for every language, against api.irisend.com instead of api.resend.com:
curl -X POST https://api.irisend.com/emails \
-H "Authorization: Bearer $IRISEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Hi",
"html": "<p>Hi</p>"
}'Irisend-only, nothing to migrate
These have no Resend equivalent — purely additive, so there's nothing to reconcile when moving over: domain health scoring and DMARC monitoring, template version history and rollback, and sub-accounts for platforms sending on behalf of their own customers.
Next
Read the full API reference for every endpoint and SDK, or paste a prompt into your AI assistant to do the swap for you.