
Security News
pnpm 12’s Rust Rewrite Cuts Install Times by Up to 90%
pnpm 12 rewrites the package manager in Rust, cutting install times by up to 90% while preserving pnpm 11 workflows and lockfiles.
@eusend_dev/sdk
Advanced tools
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
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()
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)
| Field | Type | Description |
|---|---|---|
from | string | Sender address — a bare email or display-name form (Acme <you@yourdomain.com>). Must be from a verified domain. |
to | string | string[] | Recipient(s). Maximum 50. |
cc | string | string[] | CC recipient(s). Maximum 50. |
bcc | string | string[] | BCC recipient(s). Maximum 50. |
replyTo | string | string[] | Reply-to address(es). Maximum 50. |
subject | string | Email subject |
html | string | HTML body |
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, written into the outbound message. Header names and values may not contain line breaks. |
trackOpens | boolean | Track open events (default: true) |
trackClicks | boolean | Track click events (default: true) |
At least one of html, react, text, or templateId is required.
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.
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}` },
)
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: '...' }]
const { data } = await client.emails.get('9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d')
console.log(data?.status) // 'delivered'
console.log(data?.events) // [{ type: 'sent', ... }, { type: 'delivered', ... }]
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
// 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
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: '...' }
After adding the DNS records, trigger verification:
await client.domains.verify(domainId)
const { data } = await client.domains.list()
// [{ id, name, status: 'verified', createdAt }]
const { data } = await client.domains.get(domainId)
// { id, name, dkimPublicKey, dkimSelector, status, createdAt, verifiedAt }
await client.domains.delete(domainId)
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_...'
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.
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).
await client.apiKeys.delete(keyId)
const { data } = await client.audiences.create('Newsletter')
const audienceId = data!.id
const { data } = await client.audiences.list()
// [{ id, name, createdAt, contactCount }]
await client.audiences.delete(audienceId)
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.
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
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 })
const { data } = await client.audiences.getContact(audienceId, contactId)
// { id, audienceId, email, firstName, lastName, status, unsubscribedAt, createdAt, updatedAt }
// 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,
})
await client.audiences.deleteContact(audienceId, contactId)
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.
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' })
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.
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
await client.suppressions.remove('invalid@example.com') // or the entry id
const { data: csv } = await client.suppressions.export()
// "email,reason,created_at\n..."
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
variablesis 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 templatehtmlitself, not in the variable values.
const { data } = await client.templates.create({
name: 'Welcome email',
subject: 'Welcome, {{name}}!',
html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
})
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.
await client.emails.send({
from: 'you@yourdomain.com',
to: 'user@example.com',
templateId: data!.id,
variables: {
first_name: 'Jane',
order_id: 'ORD-1234',
},
})
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)
Receive real-time events when email statuses change.
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.
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))
}
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)
Send a single email to every contact in an audience.
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.
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.
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.
await client.broadcasts.send(broadcastId, {
scheduledAt: '2026-06-01T09:00:00.000Z',
})
await client.broadcasts.cancel(broadcastId)
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)
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)
}
| 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 |
BAD_REQUEST | 400 | Malformed request |
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 (applies to all plans; ramps up as your account warms, resets midnight UTC) |
PLAN_LIMIT_EXCEEDED | 403 | Feature not available on your plan |
DOMAIN_NOT_VERIFIED | 403 | The sender domain is not verified for your organisation |
SENDING_SUSPENDED | 403 | Sending suspended for your account (high bounce or complaint rate) |
LIST_SEND_HELD | 403 | Past the unreviewed account's list-send allowance — the rest waits for review |
BROADCAST_HELD | 403 | Broadcast used its unreviewed allowance; the remainder waits for review |
ALL_SUPPRESSED | 422 | All recipients are on the suppression list |
SERVICE_PAUSED | 503 | Sending is temporarily paused platform-wide |
INTERNAL_ERROR | 500 | Server error |
application_error | null | Network failure — request never reached the server |
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'
fetch API)FAQs
Official Node.js SDK for the Eusend API
The npm package @eusend_dev/sdk receives a total of 591 weekly downloads. As such, @eusend_dev/sdk popularity was classified as not popular.
We found that @eusend_dev/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
pnpm 12 rewrites the package manager in Rust, cutting install times by up to 90% while preserving pnpm 11 workflows and lockfiles.

Security News
Socket CTO Ahmad Nassri joins AppSec leaders at Black Hat to discuss active malware, package manager risks, and software supply chain defense.

Research
/Security News
Thirteen malicious Packagist themes expose visitors on unpatched iPhones to a WebKit-to-kernel exploit chain that steals device data and wallet seeds.