eusend
SDKs

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.

Installation

npm install @eusend_dev/sdk
# or
bun add @eusend_dev/sdk

Getting 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_KEY

Sending 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

FieldTypeDescription
fromstringSender address — bare email or "Name <email>"
tostring | string[]Recipient(s)
ccstring | string[]CC recipient(s)
bccstring | string[]BCC recipient(s)
replyTostring | string[]Reply-to address(es)
subjectstringEmail subject
htmlstringHTML body
reactReact.ReactElementA React Email component. The SDK renders to HTML locally before sending. Requires react and @react-email/render as peer dependencies.
textstringPlain text body
templateIdstringID of a saved template
variablesRecord<string, unknown>Template variable substitutions
headersRecord<string, string>Custom email headers
trackOpensbooleanTrack open events (default: true)
trackClicksbooleanTrack click events (default: true)
attachmentsAttachment[]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:.
scheduledAtstring | DateSchedule 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 page

Domains

// 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
}
CodeStatusDescription
UNAUTHORIZED401Invalid or missing API key
FORBIDDEN403Action not allowed on your plan
NOT_FOUND404Resource not found
VALIDATION_ERROR400Invalid request body
CONFLICT409Resource already exists
RATE_LIMITED429Too many requests
MONTHLY_LIMIT_EXCEEDED429Monthly send quota reached
DAILY_LIMIT_EXCEEDED429Daily send ceiling reached (resets midnight UTC)
PLAN_LIMIT_EXCEEDED403Feature not available on your plan
DOMAIN_NOT_VERIFIED403Sender domain is not verified
ALL_SUPPRESSED422All recipients are on the suppression list
SENDING_SUSPENDED403Account suspended — high bounce or complaint rate
SERVICE_PAUSED503Sending temporarily paused platform-wide
BAD_REQUEST400Malformed request
INTERNAL_ERROR500Server error
application_errornullNetwork 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.