Node.js SDK
Official TypeScript SDK for the eusend API. Works in Node.js 18+, Bun, and any runtime with native fetch.
Official TypeScript SDK for the eusend API. Works in Node.js 18+, Bun, and any
runtime that supports the native fetch API.
⬡ Node.js / TypeScript
@eusend_dev/sdk
🐍 Python
pip install eusend
🐹 Go
go get github.com/eusend-dev/eusend-go
Installation
npm install @eusend_dev/sdk
# or
bun add @eusend_dev/sdkGetting started
Create a client with your API key. The key can also be read from the
EUSEND_API_KEY environment variable.
import { Eusend } from '@eusend_dev/sdk'
const client = new Eusend('eu_live_...')
// or: const client = new Eusend() // reads EUSEND_API_KEYSending an email
Every method returns a { data, error, headers } union. On success error is
null; on failure data is null.
const { data, error } = await client.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Your order is confirmed',
html: '<p>Thanks for your order!</p>',
text: 'Thanks for your order!',
})
if (error) {
console.error(error.name, error.message) // 'VALIDATION_ERROR' ...
} else {
console.log(data.id) // 9a8b7c6d-...
}Send options
| Field | Type | Description |
|---|---|---|
from | string | Sender address — bare email or "Name <email>" |
to | string | string[] | Recipient(s) |
cc | string | string[] | CC recipient(s) |
bcc | string | string[] | BCC recipient(s) |
replyTo | string | string[] | Reply-to address(es) |
subject | string | Email subject |
html | string | HTML body |
react | React.ReactElement | A React Email component. The SDK renders to HTML locally before sending. Requires react and @react-email/render as peer dependencies. |
text | string | Plain text body |
templateId | string | ID of a saved template |
variables | Record<string, unknown> | Template variable substitutions |
headers | Record<string, string> | Custom email headers |
trackOpens | boolean | Track open events (default: true) |
trackClicks | boolean | Track click events (default: true) |
attachments | Attachment[] | File attachments (up to 20, 10 MB combined). Each: { filename, one of content (base64 string or Uint8Array) or path (a public URL fetched at send time), contentType?, contentId? }. Set contentId to embed an image inline via cid:. |
scheduledAt | string | Date | Schedule the send for a future time, at most 30 days out. Reschedule with emails.update() or cancel with emails.cancel() while the email is still scheduled. |
At least one of html, react, text, or templateId is required.
Send with React Email
Pass a React Email component via react. The SDK renders it to HTML locally
before POSTing — the JSX source never travels over the wire.
import { WelcomeEmail } from './emails/welcome'
await client.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Welcome',
react: <WelcomeEmail name="Jane" />,
})Requires react and @react-email/render as peer dependencies:
npm install react @react-email/render.
Idempotent sends
Pass an idempotencyKey to safely retry without sending duplicates.
const { data } = await client.emails.send(
{ from: '[email protected]', to: '[email protected]', subject: 'Receipt', html: '<p>Thanks</p>' },
{ idempotencyKey: `receipt-${orderId}` },
)Batch send
Send up to 100 emails in a single request.
const { data } = await client.batch.send([
{ from: '[email protected]', to: '[email protected]', subject: 'Hi Alice', html: '<p>Hi Alice</p>' },
{ from: '[email protected]', to: '[email protected]', subject: 'Hi Bob', html: '<p>Hi Bob</p>' },
])
console.log(data?.data) // [{ id: '9a8b7c6d-...' }, { id: '1f2e3d4c-...' }]Scheduled sends
Pass scheduledAt — a Date, an ISO 8601 string, or natural language like
"in 1 hour" (at most 30 days out) — to schedule instead of sending
immediately. While the email's status is scheduled you can move or cancel it;
after that, both calls return a 409.
// Schedule for tomorrow morning
const { data } = await client.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Your trial ends tomorrow',
html: '<p>Reminder: your trial ends in 24 hours.</p>',
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
})
// Reschedule
await client.emails.update(data!.id, {
scheduledAt: '2026-07-04T09:00:00Z',
})
// Or cancel — the send is refunded to your monthly quota
await client.emails.cancel(data!.id)Retrieve & list emails
// Get a single email with its delivery events
const { data } = await client.emails.get('9a8b7c6d-...')
console.log(data?.status) // 'delivered'
console.log(data?.events) // [{ type: 'sent', ... }, ...]
// List with filters
const { data: list } = await client.emails.list({ status: 'delivered', limit: 50 })
console.log(list?.data) // array of emails
console.log(list?.nextCursor) // pass as cursor for next pageDomains
// Add a domain and get DNS records
const { data } = await client.domains.create('yourdomain.com')
console.log(data?.dkim) // { type: 'TXT', name: 'eusend._domainkey...', value: '...' }
console.log(data?.spf) // { type: 'TXT', name: 'yourdomain.com', value: '...' }
console.log(data?.dmarc) // { type: 'TXT', name: '_dmarc...', value: '...' }
// Trigger DNS verification
await client.domains.verify(domainId)
// List / get / delete
await client.domains.list()
await client.domains.get(domainId)
await client.domains.delete(domainId)API Keys
// Create a live key
const { data } = await client.apiKeys.create({ name: 'Production' })
console.log(data?.key) // eu_live_... — only returned once
// Create a test/sandbox key
const { data: test } = await client.apiKeys.create({ name: 'Sandbox', testMode: true })
// test.key → 'eu_test_...'
await client.apiKeys.list()
await client.apiKeys.delete(keyId)Templates
// Create an HTML template with {{variable}} placeholders
const { data } = await client.templates.create({
name: 'Welcome email',
subject: 'Welcome, {{name}}!',
html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
})
// Using React Email — SDK renders locally before sending
import { OrderConfirmation } from './emails/order-confirmation'
await client.templates.create({
name: 'Order confirmation',
subject: 'Order {{order_id}} confirmed',
react: <OrderConfirmation />,
})
// Send using a template
await client.emails.send({
from: '[email protected]',
to: '[email protected]',
templateId: data!.id,
variables: { name: 'Jane', product: 'Acme' },
})
await client.templates.list()
await client.templates.get(templateId)
await client.templates.update(templateId, { name: 'New name' })
await client.templates.delete(templateId)Audiences & Contacts
// Create an audience
const { data: aud } = await client.audiences.create('Newsletter')
// Add / upsert a contact
await client.audiences.createContact(aud!.id, {
email: '[email protected]',
firstName: 'Jane',
lastName: 'Smith',
})
// Bulk import (up to 1,000 per call)
await client.audiences.batchCreateContacts(aud!.id, {
contacts: [
{ email: '[email protected]', firstName: 'Alice' },
{ email: '[email protected]', firstName: 'Bob' },
],
})
// List with filters
await client.audiences.listContacts(aud!.id, { subscribed: true, search: 'gmail.com' })
// Update / unsubscribe / delete
await client.audiences.updateContact(aud!.id, contactId, { unsubscribed: true })
await client.audiences.deleteContact(aud!.id, contactId)
await client.audiences.list()
await client.audiences.delete(aud!.id)Broadcasts
// Create a broadcast — HTML, a React Email component, or a saved template
import { MayNewsletter } from './emails/may-newsletter'
const { data } = await client.broadcasts.create({
name: 'May newsletter',
audienceId: '550e8400-e29b-41d4-a716-446655440000',
from: '[email protected]',
subject: 'May update',
react: <MayNewsletter />, // SDK renders to HTML locally
// or: html: '<p>Hi {{first_name}}...</p>'
// or: templateId: '7c9e6679-7425-40de-944b-e07fc1f90ae7'
})
// Send immediately
await client.broadcasts.send(data!.id)
// Or schedule for later
await client.broadcasts.send(data!.id, { scheduledAt: '2026-06-01T09:00:00.000Z' })
// Cancel a scheduled broadcast
await client.broadcasts.cancel(data!.id)
await client.broadcasts.list()
await client.broadcasts.get(broadcastId) // includes delivery stats
await client.broadcasts.update(broadcastId, { subject: 'Updated subject' })
await client.broadcasts.delete(broadcastId)Webhooks
// Subscribe to events
const { data } = await client.webhooks.create({
url: 'https://yourapp.com/webhooks/eusend',
events: ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
// or: events: ['*'] to receive everything
})
console.log(data?.secret) // signing secret — store it securely, only returned once
await client.webhooks.list()
await client.webhooks.get(webhookId) // includes recent deliveries
await client.webhooks.update(webhookId, { events: ['email.bounced'] })
await client.webhooks.delete(webhookId)Verifying webhook signatures
Every delivery is signed with HMAC-SHA256. Verify before processing:
import { createHmac, timingSafeEqual } from 'crypto'
async function verifyWebhook(req: Request, secret: string): Promise<boolean> {
const id = req.headers.get('webhook-id') ?? ''
const timestamp = req.headers.get('webhook-timestamp') ?? ''
const signature = req.headers.get('webhook-signature') ?? ''
const body = await req.text()
const expected = 'v1,' + createHmac('sha256', secret)
.update(`${id}.${timestamp}.${body}`)
.digest('base64')
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}Error handling
const { data, error } = await client.emails.send({ ... })
if (error) {
console.error(error.name) // 'MONTHLY_LIMIT_EXCEEDED'
console.error(error.message) // 'Monthly send limit exceeded'
console.error(error.statusCode) // 429
}| Code | Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or missing API key |
FORBIDDEN | 403 | Action not allowed on your plan |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 400 | Invalid request body |
CONFLICT | 409 | Resource already exists |
RATE_LIMITED | 429 | Too many requests |
MONTHLY_LIMIT_EXCEEDED | 429 | Monthly send quota reached |
DAILY_LIMIT_EXCEEDED | 429 | Daily send ceiling reached (resets midnight UTC) |
PLAN_LIMIT_EXCEEDED | 403 | Feature not available on your plan |
DOMAIN_NOT_VERIFIED | 403 | Sender domain is not verified |
ALL_SUPPRESSED | 422 | All recipients are on the suppression list |
SENDING_SUSPENDED | 403 | Account suspended — high bounce or complaint rate |
SERVICE_PAUSED | 503 | Sending temporarily paused platform-wide |
BAD_REQUEST | 400 | Malformed request |
INTERNAL_ERROR | 500 | Server error |
application_error | null | Network failure — request never reached the server |
TypeScript
The SDK ships with full type definitions. All request options, response shapes, and error codes are typed.
import type {
SendEmailOptions,
Email,
EmailStatus,
EusendError,
EusendResponse,
} from '@eusend_dev/sdk'View the full source and changelog on npm.