Resend vs Loops
Resend and Loops both target modern SaaS teams but solve different problems. Resend is a transactional email API in the classic sense: you construct a message — recipients, subject, HTML, attachments — and it delivers it, with scheduling, tags, and full header control. Loops is an email platform for product companies where transactional sends are one feature next to marketing loops and audience management; its transactional API is intentionally minimal and template-driven, so the unified message shape maps onto a much smaller surface — no CC or BCC, no custom headers, no tags, and no scheduled sending, though it does carry per-message metadata (data variables) and attachments. Pick Loops when the same tool should own lifecycle and marketing email; pick Resend when you want a full-fidelity send API.
Message field support
Field support as encoded in Email SDK's own adapter capability matrix — the same data the SDK uses to reject sends that would silently drop fields.
| Message field | Resend | Loops |
|---|---|---|
| CC recipients | ✓ | — |
| BCC recipients | ✓ | — |
| Reply-To address | ✓ | — |
| Custom headers | ✓ | — |
| Attachments | ✓ | ✓ |
| Tags | ✓ | — |
| Metadata | — | ✓ |
| Scheduled sending (sendAt) | ✓ | — |
Fallback compatibility
Failing over from Resend to Loops loses: cc recipients, bcc recipients, reply-to address, custom headers, tags, scheduled sending (sendat). Email SDK rejects the fallback send for messages that use these fields instead of dropping them silently.
Failing over from Loops to Resend loses: metadata.
Same code, either provider
With Email SDK the send call is identical for both providers — only the adapter import changes:
import { createEmailClient } from "@opencoredev/email-sdk";
import { resend } from "@opencoredev/email-sdk/resend";
const client = createEmailClient({
adapters: [resend({ apiKey: process.env.RESEND_API_KEY! })],
});
await client.send({
from: "hello@yourdomain.com",
to: "user@example.com",
subject: "Welcome!",
html: "<p>It works.</p>",
});Or run both, with automatic fallback:
import { createEmailClient } from "@opencoredev/email-sdk";
import { resend } from "@opencoredev/email-sdk/resend";
import { loops } from "@opencoredev/email-sdk/loops";
const client = createEmailClient({
adapters: [
resend({ apiKey: process.env.RESEND_API_KEY! }),
loops({
apiKey: process.env.LOOPS_API_KEY!,
transactionalId: "your-template-id",
}),
],
defaultAdapter: "resend",
fallback: ["loops"],
});FAQ
- Can I switch from Resend to Loops without rewriting my email code?
- Yes. With Email SDK both providers share one typed send() call and one message shape, so switching from Resend to Loops is a one-line adapter change plus an API key. The SDK fails fast if a message uses a field Loops does not support, so nothing is silently dropped.
- Which message fields does Resend support that Loops doesn't?
- Resend supports cc recipients, bcc recipients, reply-to address, custom headers, tags, scheduled sending (sendat) in the unified message shape, which Loops does not.
- Can I use Loops as a fallback for Resend?
- Partially. Email SDK checks field support before every send: a fallback from Resend to Loops is rejected for messages using cc recipients, bcc recipients, reply-to address, custom headers, tags, scheduled sending (sendat), and from Loops to Resend for messages using metadata. Messages that avoid those fields fail over cleanly.
Adapter docs
Setup guides: Resend adapter and Loops adapter. Sending from your own domain? Verify SPF, DKIM, and DMARC with the free email DNS checker.