Docs

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.

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

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

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