Migrate from Resend
Move a transactional Node.js integration from Resend to OpenSES with minimal application changes.
OpenSES includes a Resend-compatible facade for the transactional email path. Existing code can keep the resend variable, emails.send() call, and { data, error } response handling.
Before changing code
OpenSES sends through your Amazon SES account. Complete these dashboard steps first:
- Connect an AWS account with SES access.
- Verify the sender domain used by the application.
- Create an OpenSES project API key.
A Resend API key cannot be used with OpenSES.
Replace the package
npm remove resend
npm install @openses/opensesChange the import and API key:
- import { Resend } from 'resend';
+ import { Resend } from '@openses/openses';
- const resend = new Resend(process.env.RESEND_API_KEY);
+ const resend = new Resend(process.env.OPENSES_API_KEY, {
+ baseUrl: 'https://email.mycompany.com',
+ });The facade temporarily falls back to RESEND_API_KEY to make staged deployments easier, but the variable must contain an OpenSES opk_live_... or opk_test_... key. Renaming it to OPENSES_API_KEY is recommended.
Existing send code
This call shape remains valid:
const { data, error } = await resend.emails.send(
{
from: 'Acme <noreply@acme.com>',
to: 'user@example.com',
subject: 'Welcome',
html: '<h1>Welcome</h1>',
replyTo: ['support@acme.com', 'billing@acme.com'],
headers: {
'X-Entity-ID': 'user_123',
},
tags: [
{ name: 'source', value: 'signup' },
],
},
{
idempotencyKey: 'welcome-user-123',
},
);
if (error) {
console.error(error.name, error.message);
} else {
console.log(data.id);
}Unlike the native OpenSES client, the Resend facade returns errors instead of throwing them.
Compatibility
| Resend transactional feature | OpenSES compatibility |
|---|---|
emails.send() and emails.create() | Supported |
| HTML and plain text | Supported |
| React Email | Supported when @react-email/render is installed |
| CC, BCC, custom headers | Supported |
| Multiple reply-to addresses | Supported |
| Buffer, base64, local path, and URL attachments | Supported, 40 MB total |
| Tags | Supported; the facade converts tag arrays to OpenSES tags |
| Templates | Supported; the Resend template id maps to an OpenSES template slug |
| Idempotency keys | Supported, including mismatched-payload protection |
emails.get() | Supported |
emails.list({ after, limit }) | Supported |
emails.cancel() | Supported |
{ data, error, headers } responses | Supported |
webhooks.verify() | Supported through Standard Webhooks headers |
OpenSES continues to send its original X-OpenSES-* webhook headers and now also sends webhook-id, webhook-timestamp, and webhook-signature. Existing Resend verification code can therefore keep this shape:
const event = resend.webhooks.verify({
payload: rawBody,
headers: {
id: request.headers.get('webhook-id')!,
timestamp: request.headers.get('webhook-timestamp')!,
signature: request.headers.get('webhook-signature')!,
},
webhookSecret: process.env.OPENSES_WEBHOOK_SECRET!,
});Current differences
| Resend feature | Status |
|---|---|
scheduledAt and emails.update() | Not implemented; the facade returns a validation error |
batch.send() | Not implemented; OpenSES does not emulate atomic batch behavior with multiple sends |
emails.list({ before }) | Not implemented; forward cursor pagination is supported |
| Topics | Not implemented; OpenSES is transactional-first |
| Receiving and attachment retrieval | Not implemented |
| Broadcasts, automations, contacts, segments | Out of the transactional migration scope |
| Domain, template, API key, and webhook management SDKs | Dashboard operations over tRPC today |
OpenSES requires from even when using a stored template because sender identity belongs to the connected SES account. A template may supply its own subject.
Remaining implementation plan
- Persist
scheduled_at, enqueue delayed BullMQ jobs, and support rescheduling. - Add a public batch REST endpoint with per-item validation and idempotency.
- Add project API-key REST resources for domains, templates, API keys, and webhooks.
- Add inbound email receiving after the transactional migration surface is complete.
The public SDK stays on REST because it uses project API keys and must work across languages. Dashboard administration remains on tRPC with Better Auth sessions.