opensend
TypeScript SDK for the OpenSend email API with a Resend-compatible client surface.
Use your OpenSend API key (os_...) with the Resend-compatible API surface.
Installation
bun add opensendGetting Started
Use the Resend-compatible export for the easiest migration path:
import { Resend } from "opensend";
const resend = new Resend("os_your_api_key");By default the SDK targets OpenSend's hosted API origin, https://api.opensend.com.
Self-hosted deployments can override the origin with baseUrl:
import { Resend } from "opensend";
const resend = new Resend("os_your_api_key", {
baseUrl: "https://api.your-deployment.example.com",
});The existing Opensend export is still supported for backwards compatibility:
import { Opensend } from "opensend";
const client = new Opensend("os_your_api_key", {
baseUrl: "https://api.your-deployment.example.com",
});Sending Emails
const { data, error } = await resend.emails.send({
from: "hello@updates.example.com",
to: "user@example.com",
subject: "Welcome!",
html: "# Welcome aboard",
});
if (error) {
console.error(error.message);
} else {
console.log("Queued:", data.id);
}resend.emails.send() posts to the Resend-compatible /emails endpoint and
returns after the API persists the row and queues background delivery work. Poll
resend.emails.get(id) or list emails to observe the lifecycle: queued →
processing → sent, followed by SES delivery events such as delivered,
bounced, opened, or clicked. The created_at timestamp is queue time;
sent_at is set by the worker after SES accepts the message.
Multiple Reply-To addresses
Use Resend-compatible replyTo for one or more Reply-To addresses. The SDK
serializes it to the REST reply_to field.
await resend.emails.send({
from: "hello@updates.example.com",
to: "user@example.com",
subject: "Need help?",
html: "
Reply to reach our support team.
",
replyTo: ["support@example.com", "billing@example.com"],
});With React components
Install React and ReactDOM alongside the SDK when you use the react payload.
They are optional peer dependencies so plain html/text sends do not pull
React into non-React applications.
bun add opensend react react-dom @react-email/componentsimport { Html, Text } from "@react-email/components";
import { Resend } from "opensend";
function EmailTemplate({ name }: { name: string }) {
return (
<Html>
<Text>Hello {name}, welcome to OpenSend.</Text>
</Html>
);
}
const resend = new Resend(process.env.OPENSEND_API_KEY);
export async function POST() {
const { data, error } = await resend.emails.send({
from: "hello@updates.example.com",
to: "user@example.com",
subject: "Welcome!",
react: <EmailTemplate name="Ada" />,
});
if (error) {
return Response.json({ error: error.message }, { status: error.statusCode });
}
return Response.json(data);
}The SDK renders the React element to HTML locally with react-dom/server and
sends only JSON/HTML to the OpenSend REST API. If ReactDOM is missing or the
component throws while rendering, emails.send() returns a
react_render_error SDK error and does not call the API.
const { data } = await resend.emails.send({
from: "hello@updates.example.com",
to: "user@example.com",
subject: "Invoice",
react: <InvoiceEmail amount={49.99} />,
});Batch Sending
const { data, error } = await resend.emails.sendBatch([
{
from: "hello@updates.example.com",
to: "a@example.com",
subject: "Hi A",
html: "
A
",
},
{
from: "hello@updates.example.com",
to: "b@example.com",
subject: "Hi B",
html: "
B
",
},
]);resend.emails.sendBatch() posts to the Resend-compatible /emails/batch
endpoint.
Scheduled Sends
Pass scheduled_at as a string to defer delivery for send or sendBatch.
The API reschedule endpoint accepts the same formats. Supported values are
future ISO 8601 date-times with a timezone (for example 2026-05-08T00:00:00.000Z) or the small
Resend-compatible natural-language form in <positive integer>
<minute|min|minutes|hour|hours|day|days> such as in 1 min. Values must be
within 30 days; unparseable, past, or out-of-policy values return
validation_error.
Cancel a scheduled email before it is sent with the Resend-compatible cancel surface:
const { data, error } = await resend.emails.cancel("email-id");
// data: { object: "email", id: "email-id" }Idempotency Keys
Pass a per-request idempotencyKey option to prevent accidental duplicate
acceptance when retrying sends. Keys must match the API contract: 1-256
characters. OpenSend replays duplicate single-send keys with the original
accepted { id } response and duplicate batch keys with the original
accepted { data: [{ id }] } response before reserving quota, creating
additional rows, or publishing more queue jobs. Idempotency keys expire after
24 hours; reusing the same key after that window is accepted as a new request.
await resend.emails.send(
{
from: "hello@updates.example.com",
to: "user@example.com",
subject: "Welcome!",
html: "# Welcome aboard",
},
{ idempotencyKey: "welcome-user-123" },
);
await resend.emails.sendBatch(
[
{
from: "hello@updates.example.com",
to: "a@example.com",
subject: "Hi A",
html: "
A
",
},
{
from: "hello@updates.example.com",
to: "b@example.com",
subject: "Hi B",
html: "
B
",
},
],
{ idempotencyKey: "batch-campaign-123" },
);Listing Emails
const { data } = await resend.emails.list();
console.log(data.data); // EmailListItem[]
const queued = await resend.emails.list({ status: "queued" });Getting an Email
const { data } = await resend.emails.get("email-id");Domains
// Create a domain
await resend.domains.create({ name: "example.com" });
// List domains
const { data } = await resend.domains.list();
// Get a domain
await resend.domains.get("domain-id");
// Verify a domain
await resend.domains.verify("domain-id");API Keys
// Create an API key
const { data } = await resend.apiKeys.create({ name: "Production Key" });
console.log(data.token); // Only shown once
// List API keys
await resend.apiKeys.list();
// Delete an API key
await resend.apiKeys.delete("key-id");Contacts
// Create a contact
await resend.contacts.create({ email: "user@example.com" });
// List contacts
const { data } = await resend.contacts.list();
// Get a contact
await resend.contacts.get("contact-id");
// Update a contact by id or email
await resend.contacts.update("user@example.com", { unsubscribed: true });
// Delete a contact by id or email
await resend.contacts.delete("user@example.com");TypeScript
Public request, response, and error shapes are exported from the package entrypoint:
import type {
ApiError,
ApiResponse,
BatchEmailResponse,
EmailOptions,
EmailResponse,
RequestOptions,
SendEmailPayload,
SDKOptions,
} from "opensend";Error Handling
All methods return { data, error }. Check error before using data:
const { data, error } = await resend.emails.send({ ... });
if (error) {
console.error(`Error ${error.statusCode}: ${error.message}`);
return;
}
// data is guaranteed non-null here
console.log(data.id);Configuration
new Resend(apiKey, options?) accepts an optional baseUrl. Use this for
self-hosted OpenSend deployments:
const resend = new Resend("os_your_api_key", {
baseUrl: "https://api.your-deployment.example.com",
});