New:Socket for Asana Is Now Available.Learn more
Get Started

@eusend_dev/sdk

Package Overview
Dependencies
Maintainers
1
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@eusend_dev/sdk

Official Node.js SDK for the Eusend API

latest
Source
npmnpm
Version
0.11.4
Version published
Weekly downloads
717
408.51%
Maintainers
1
Weekly downloads
 
Created
Source

eusend

Official Node.js SDK for the Eusend API — the EU-native transactional email platform.

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

Getting started

import { Eusend } from '@eusend_dev/sdk'

const client = new Eusend('eu_live_...')

Your API key can also be set via the EUSEND_API_KEY environment variable, in which case the constructor argument can be omitted:

const client = new Eusend()

Emails

Send an email

const { data, error } = await client.emails.send({
  // `from` accepts a bare email or a display-name form: `Acme <you@yourdomain.com>`
  from: 'Acme <you@yourdomain.com>',
  to: 'user@example.com',
  subject: 'Hello',
  html: '<p>Hello world</p>',
  text: 'Hello world',
})

console.log(data?.id) // 9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d (UUID)

Options

FieldTypeDescription
fromstringSender address — a bare email or display-name form (Acme <you@yourdomain.com>). Must be from a verified domain.
tostring | string[]Recipient(s). Maximum 50.
ccstring | string[]CC recipient(s). Maximum 50.
bccstring | string[]BCC recipient(s). Maximum 50.
replyTostring | string[]Reply-to address(es). Maximum 50.
subjectstringEmail subject
htmlstringHTML body
textstringPlain text body
templateIdstringID of a saved template
variablesRecord<string, unknown>Template variable substitutions
headersRecord<string, string>Custom email headers, written into the outbound message. Header names and values may not contain line breaks.
trackOpensbooleanTrack open events. Omit to use your organization default
trackClicksbooleanTrack click events. Omit to use your organization default

At least one of html, react, text, or templateId is required.

Send with React Email

Pass a React Email component via the react field. The SDK renders it to HTML locally before sending — the JSX source never travels over the wire.

import { Eusend } from '@eusend_dev/sdk'
import { WelcomeEmail } from './emails/welcome'

const client = new Eusend()

await client.emails.send({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Welcome',
  react: <WelcomeEmail name="Jane" />,
})

Requires react and @react-email/render as peer dependencies:

npm install react @react-email/render

If you prefer to render yourself, pass the resulting HTML via html instead — useful when you want one rendered template to serve multiple sends.

Idempotent sends

Pass an idempotencyKey to safely retry without sending duplicates. If a request with the same key was already accepted, the original email ID is returned.

const { data } = await client.emails.send(
  {
    from: 'you@yourdomain.com',
    to: 'user@example.com',
    subject: 'Your receipt',
    html: '<p>Thanks for your order.</p>',
  },
  { idempotencyKey: `receipt-${orderId}` },
)

Send a batch

Up to 100 emails in a single request.

const { data } = await client.batch.send([
  {
    from: 'you@yourdomain.com',
    to: 'alice@example.com',
    subject: 'Hello Alice',
    html: '<p>Hi Alice</p>',
  },
  {
    from: 'you@yourdomain.com',
    to: 'bob@example.com',
    subject: 'Hello Bob',
    html: '<p>Hi Bob</p>',
  },
])

console.log(data?.data) // [{ id: '...' }, { id: '...' }]

Retrieve an email

const { data } = await client.emails.get('9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d')

console.log(data?.status) // 'delivered'
console.log(data?.events) // [{ type: 'sent', ... }, { type: 'delivered', ... }]

List emails

const { data } = await client.emails.list({ limit: 20 })

console.log(data?.data) // array of emails
console.log(data?.nextCursor) // pass as cursor to fetch the next page

Filtering

// By status
await client.emails.list({ status: 'delivered' })

// By sender
await client.emails.list({ from: 'you@yourdomain.com' })

// By recipient
await client.emails.list({ to: 'user@example.com' })

// Pagination
await client.emails.list({ limit: 50, cursor: data.nextCursor })

Available statuses: queued sending sent delivered bounced complained suppressed failed

Domains

Add a domain

const { data } = await client.domains.create('yourdomain.com')

// DNS records to add to your domain
console.log(data?.dkim) // { type: 'TXT', name: 'eusend._domainkey.yourdomain.com', value: '...' }
console.log(data?.spf) // { type: 'TXT', name: 'yourdomain.com', value: '...' }
console.log(data?.dmarc) // { type: 'TXT', name: '_dmarc.yourdomain.com', value: '...' }

Verify a domain

After adding the DNS records, trigger verification:

await client.domains.verify(domainId)

List domains

const { data } = await client.domains.list()
// [{ id, name, status: 'verified', createdAt }]

Get a domain

const { data } = await client.domains.get(domainId)
// { id, name, dkimPublicKey, dkimSelector, status, createdAt, verifiedAt }

Delete a domain

await client.domains.delete(domainId)

API Keys

Create an API key

const { data } = await client.apiKeys.create({ name: 'Production' })

console.log(data?.key) // eu_live_... — only returned once, store it securely

Pass testMode: true to create a sandbox key. Emails sent with a test key are accepted and tracked but never delivered.

const { data } = await client.apiKeys.create({ name: 'Sandbox', testMode: true })
// data.key → 'eu_test_...'

Scoping a key

permission defaults to 'full_access' — every resource. 'sending_access' limits the key to sending email (plus rescheduling and canceling a scheduled send); every other endpoint, including reading your email logs, returns 403 FORBIDDEN.

await client.apiKeys.create({ name: 'App server', permission: 'sending_access' })

A sending-access key can additionally be pinned to one sending domain. Sends from any other domain are rejected. domainId is only valid with permission: 'sending_access'.

await client.apiKeys.create({
  name: 'Billing service',
  permission: 'sending_access',
  domainId,
})

Deleting a domain revokes every key restricted to it.

List API keys

const { data } = await client.apiKeys.list()
// [{ id, name, prefix, testMode, permission, domainId, domainName, createdAt, lastUsedAt }]

The full key is never returned after creation — only the prefix (e.g. eu_live_Lx_e).

Delete an API key

await client.apiKeys.delete(keyId)

Audiences & Contacts

Create an audience

const { data } = await client.audiences.create('Newsletter')
const audienceId = data!.id

List audiences

const { data } = await client.audiences.list()
// [{ id, name, createdAt, contactCount }]

Delete an audience

await client.audiences.delete(audienceId)

Add a contact

const { data } = await client.audiences.createContact(audienceId, {
  email: 'user@example.com',
  firstName: 'Jane',
  lastName: 'Smith',
})

If a contact with that email already exists in the audience, it will be updated instead.

Bulk import contacts

Up to 1,000 contacts per call. Existing contacts are upserted.

const { data } = await client.audiences.batchCreateContacts(audienceId, {
  contacts: [
    { email: 'alice@example.com', firstName: 'Alice' },
    { email: 'bob@example.com', firstName: 'Bob' },
  ],
})

console.log(data?.count) // 2

List contacts

const { data } = await client.audiences.listContacts(audienceId, { limit: 100 })

// Filter by subscription status
await client.audiences.listContacts(audienceId, { subscribed: true })
await client.audiences.listContacts(audienceId, { subscribed: false })

// Search by email
await client.audiences.listContacts(audienceId, { search: 'gmail.com' })

// Pagination
await client.audiences.listContacts(audienceId, { cursor: data.nextCursor })

Get a contact

const { data } = await client.audiences.getContact(audienceId, contactId)
// { id, audienceId, email, firstName, lastName, status, unsubscribedAt, createdAt, updatedAt }

Update a contact

// Update name
await client.audiences.updateContact(audienceId, contactId, {
  firstName: 'Janet',
})

// Unsubscribe
await client.audiences.updateContact(audienceId, contactId, {
  unsubscribed: true,
})

// Re-subscribe
await client.audiences.updateContact(audienceId, contactId, {
  unsubscribed: false,
})

Delete a contact

await client.audiences.deleteContact(audienceId, contactId)

Suppressions

Addresses the account will not send to. Hard bounces and spam complaints are added automatically; these methods cover the ones you manage yourself. A send to a suppressed address is skipped and recorded with status suppressed; if every recipient is suppressed the send fails with ALL_SUPPRESSED.

Test-mode keys can read the list but not modify it.

List suppressions

const { data } = await client.suppressions.list({ reason: 'bounce', limit: 50 })
// { data: [{ id, email, reason, created_at }], next_cursor: null }

// Everything suppressed at one domain
await client.suppressions.list({ email: '@acme.com' })

Suppress an address

await client.suppressions.create({ email: 'opted-out@example.com' })

If the address is already suppressed the existing entry is returned unchanged — a manual add never rewrites a real bounce or complaint.

Import a list

Up to 1,000 addresses per call. Items may be bare strings or objects, so a column lifted straight out of a CSV works as-is. Import before your first send when migrating, so addresses that already bounced elsewhere don't get a fresh attempt from a new IP.

const { data } = await client.suppressions.import([
  'one@example.com',
  { email: 'two@example.com', reason: 'complaint' },
])

console.log(data?.count) // written
console.log(data?.already_suppressed) // were already on the list
console.log(data?.duplicates) // repeated rows collapsed

Remove an address

await client.suppressions.remove('invalid@example.com') // or the entry id

Export

const { data: csv } = await client.suppressions.export()
// "email,reason,created_at\n..."

Templates

Templates let you define reusable email layouts with {{variable}} placeholders that are substituted at send time.

Variable values are HTML-escaped. A value you pass in variables is inserted as text, not markup — {{name}} with "<b>Jane</b>" renders the literal characters, not bold text. Put any HTML structure (links, formatting) in the template html itself, not in the variable values.

Create a template (HTML)

const { data } = await client.templates.create({
  name: 'Welcome email',
  subject: 'Welcome, {{name}}!',
  html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
})

Create a template (React Email)

Pass a React Email component via react. The SDK renders it to HTML locally before submitting — the JSX source never travels over the wire.

import { OrderConfirmation } from './emails/order-confirmation'

const { data } = await client.templates.create({
  name: 'Order confirmation',
  subject: 'Your order {{order_id}} is confirmed',
  react: <OrderConfirmation />,
})

Use {{variable}} placeholders anywhere in your React component; they pass through to the rendered HTML and are substituted at send time when you provide variables. If you'd rather render yourself, pass html instead.

Send using a template

await client.emails.send({
  from: 'you@yourdomain.com',
  to: 'user@example.com',
  templateId: data!.id,
  variables: {
    first_name: 'Jane',
    order_id: 'ORD-1234',
  },
})

List, get, update, delete

await client.templates.list()
await client.templates.get(templateId)
await client.templates.update(templateId, { name: 'New name', subject: 'New subject' })
await client.templates.delete(templateId)

Webhooks

Receive real-time events when email statuses change.

Create a webhook

const { data } = await client.webhooks.create({
  url: 'https://yourapp.com/webhooks/eusend',
  events: ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
})

console.log(data?.secret) // signing secret — only returned once, store it securely

Pass '*' in the events array to subscribe to all events.

Available events: email.sent email.delivered email.bounced email.complained email.opened email.clicked

Endpoint requirements: the url must be a public http(s) endpoint — private, loopback, and internal addresses are rejected, both at creation and (after DNS resolution) before each delivery. Your endpoint must respond directly with a 2xx; redirects (3xx) are not followed and are treated as a failed delivery.

Verifying webhook signatures

Every delivery is signed with HMAC-SHA256. Verify the signature before processing:

import { createHmac, timingSafeEqual } from 'crypto'

async function verifyWebhook(req: Request, secret: string): Promise<boolean> {
  const webhookId = 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(`${webhookId}.${timestamp}.${body}`).digest('base64')

  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

List, get, update, delete

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)

Broadcasts

Send a single email to every contact in an audience.

Create a broadcast

const { data } = await client.broadcasts.create({
  name: 'May newsletter',
  audienceId: '550e8400-e29b-41d4-a716-446655440000',
  from: 'Sivert <hello@yourdomain.com>',
  subject: 'May update',
  html: '<p>Hi {{first_name}}, your monthly update is here...</p>',
})

You can also pass a React Email component via react, a saved template via templateId, or plain HTML. With react, the SDK renders to HTML locally before submitting:

import { MayNewsletter } from './emails/may-newsletter'

await client.broadcasts.create({
  name: 'May newsletter',
  audienceId: '550e8400-e29b-41d4-a716-446655440000',
  from: 'Sivert <hello@yourdomain.com>',
  subject: 'May update',
  react: <MayNewsletter />,
})

{{first_name}}, {{last_name}}, {{full_name}}, and {{email}} are automatically available per recipient. Custom variables can be defined on the broadcast and are merged with per-recipient data.

Unsubscribe handling

Broadcasts — and any single-recipient send whose recipient is a known audience contact — automatically include RFC 8058 one-click unsubscribe headers (List-Unsubscribe + List-Unsubscribe-Post: List-Unsubscribe=One-Click), so you satisfy Gmail/Yahoo bulk-sender requirements without any extra work. (Sends to multiple recipients at once omit the header, since a single unsubscribe link can't be attributed to one recipient.) Broadcasts additionally render a visible unsubscribe footer in the email body. An unsubscribe is recorded against the contact (unsubscribedAt) and excludes them from future broadcasts; transactional sends to that address still go through. You don't need to set these headers yourself.

Send a broadcast

await client.broadcasts.send(broadcastId)

Calling send on a paused broadcast resumes it — sending continues from where it stopped, skipping recipients already sent. A broadcast pauses if it hits your monthly or daily send limit, the sender domain becomes unverified, or platform-wide sending is paused.

Schedule a broadcast

await client.broadcasts.send(broadcastId, {
  scheduledAt: '2026-06-01T09:00:00.000Z',
})

Cancel a broadcast

await client.broadcasts.cancel(broadcastId)

List, get, update, delete

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)

Error handling

Every method returns { data, error, headers }. On success error is null; on failure data is null.

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
} else {
  console.log(data.id)
}

Error codes

CodeStatusDescription
UNAUTHORIZED401Invalid or missing API key
FORBIDDEN403Action not allowed on your plan
NOT_FOUND404Resource not found
VALIDATION_ERROR400Invalid request body
BAD_REQUEST400Malformed request
CONFLICT409Resource already exists
RATE_LIMITED429Too many requests
MONTHLY_LIMIT_EXCEEDED429Monthly send quota reached
DAILY_LIMIT_EXCEEDED429Daily send ceiling reached (applies to all plans; ramps up as your account warms, resets midnight UTC)
PLAN_LIMIT_EXCEEDED403Feature not available on your plan
DOMAIN_NOT_VERIFIED403The sender domain is not verified for your organisation
SENDING_SUSPENDED403Sending suspended for your account (high bounce or complaint rate)
LIST_SEND_HELD403Past the unreviewed account's list-send allowance — the rest waits for review
BROADCAST_HELD403Broadcast used its unreviewed allowance; the remainder waits for review
ALL_SUPPRESSED422All recipients are on the suppression list
SERVICE_PAUSED503Sending is temporarily paused platform-wide
INTERNAL_ERROR500Server error
application_errornullNetwork failure — request never reached the server

TypeScript

The SDK is written in TypeScript and 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'

Requirements

  • Node.js 18 or later (uses the native fetch API)
  • An Eusend account and API key — eusend.dev

FAQs

Package last updated on 26 Aug 2026

Related posts