@eusend_dev/sdk 0.3.10 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +582 -580
  2. package/dist/index.mjs.map +1 -1
  3. package/package.json +52 -52
package/README.md CHANGED
@@ -1,580 +1,582 @@
1
- # eusend
2
-
3
- Official Node.js SDK for the [Eusend](https://eusend.dev) API — the EU-native transactional email platform.
4
-
5
- ```bash
6
- npm install @eusend_dev/sdk
7
- # or
8
- bun add @eusend_dev/sdk
9
- ```
10
-
11
- ---
12
-
13
- ## Getting started
14
-
15
- ```ts
16
- import { Eusend } from '@eusend_dev/sdk'
17
-
18
- const client = new Eusend('eu_live_...')
19
- ```
20
-
21
- Your API key can also be set via the `EUSEND_API_KEY` environment variable, in which case the constructor argument can be omitted:
22
-
23
- ```ts
24
- const client = new Eusend()
25
- ```
26
-
27
- ---
28
-
29
- ## Emails
30
-
31
- ### Send an email
32
-
33
- ```ts
34
- const { data, error } = await client.emails.send({
35
- // `from` accepts a bare email or a display-name form: `Acme <you@yourdomain.com>`
36
- from: 'Acme <you@yourdomain.com>',
37
- to: 'user@example.com',
38
- subject: 'Hello',
39
- html: '<p>Hello world</p>',
40
- text: 'Hello world',
41
- })
42
-
43
- console.log(data?.id) // 9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d (UUID)
44
- ```
45
-
46
- #### Options
47
-
48
- | Field | Type | Description |
49
- |-------|------|-------------|
50
- | `from` | `string` | Sender email address |
51
- | `to` | `string \| string[]` | Recipient(s). Maximum 50. |
52
- | `cc` | `string \| string[]` | CC recipient(s). Maximum 50. |
53
- | `bcc` | `string \| string[]` | BCC recipient(s). Maximum 50. |
54
- | `replyTo` | `string \| string[]` | Reply-to address(es). Maximum 50. |
55
- | `subject` | `string` | Email subject |
56
- | `html` | `string` | HTML body |
57
- | `text` | `string` | Plain text body |
58
- | `templateId` | `string` | ID of a saved template |
59
- | `variables` | `Record<string, unknown>` | Template variable substitutions |
60
- | `headers` | `Record<string, string>` | Custom email headers, written into the outbound message. Header names and values may not contain line breaks. |
61
- | `trackOpens` | `boolean` | Track open events (default: `true`) |
62
- | `trackClicks` | `boolean` | Track click events (default: `true`) |
63
-
64
- At least one of `html`, `react`, `text`, or `templateId` is required.
65
-
66
- ### Send with React Email
67
-
68
- 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.
69
-
70
- ```tsx
71
- import { Eusend } from '@eusend_dev/sdk'
72
- import { WelcomeEmail } from './emails/welcome'
73
-
74
- const client = new Eusend()
75
-
76
- await client.emails.send({
77
- from: 'hello@yourdomain.com',
78
- to: 'user@example.com',
79
- subject: 'Welcome',
80
- react: <WelcomeEmail name="Jane" />,
81
- })
82
- ```
83
-
84
- Requires `react` and `@react-email/render` as peer dependencies:
85
-
86
- ```bash
87
- npm install react @react-email/render
88
- ```
89
-
90
- If you prefer to render yourself, pass the resulting HTML via `html` instead — useful when you want one rendered template to serve multiple sends.
91
-
92
- ### Idempotent sends
93
-
94
- 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.
95
-
96
- ```ts
97
- const { data } = await client.emails.send(
98
- {
99
- from: 'you@yourdomain.com',
100
- to: 'user@example.com',
101
- subject: 'Your receipt',
102
- html: '<p>Thanks for your order.</p>',
103
- },
104
- { idempotencyKey: `receipt-${orderId}` },
105
- )
106
- ```
107
-
108
- ### Send a batch
109
-
110
- Up to 100 emails in a single request.
111
-
112
- ```ts
113
- const { data } = await client.emails.batch([
114
- {
115
- from: 'you@yourdomain.com',
116
- to: 'alice@example.com',
117
- subject: 'Hello Alice',
118
- html: '<p>Hi Alice</p>',
119
- },
120
- {
121
- from: 'you@yourdomain.com',
122
- to: 'bob@example.com',
123
- subject: 'Hello Bob',
124
- html: '<p>Hi Bob</p>',
125
- },
126
- ])
127
-
128
- console.log(data?.data) // [{ id: '...' }, { id: '...' }]
129
- ```
130
-
131
- ### Retrieve an email
132
-
133
- ```ts
134
- const { data } = await client.emails.get('9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d')
135
-
136
- console.log(data?.status) // 'delivered'
137
- console.log(data?.events) // [{ type: 'sent', ... }, { type: 'delivered', ... }]
138
- ```
139
-
140
- ### List emails
141
-
142
- ```ts
143
- const { data } = await client.emails.list({ limit: 20 })
144
-
145
- console.log(data?.data) // array of emails
146
- console.log(data?.nextCursor) // pass as cursor to fetch the next page
147
- ```
148
-
149
- #### Filtering
150
-
151
- ```ts
152
- // By status
153
- await client.emails.list({ status: 'delivered' })
154
-
155
- // By sender
156
- await client.emails.list({ from: 'you@yourdomain.com' })
157
-
158
- // By recipient
159
- await client.emails.list({ to: 'user@example.com' })
160
-
161
- // Pagination
162
- await client.emails.list({ limit: 50, cursor: data.nextCursor })
163
- ```
164
-
165
- Available statuses: `queued` `sending` `sent` `delivered` `bounced` `complained` `suppressed` `failed`
166
-
167
- ---
168
-
169
- ## Domains
170
-
171
- ### Add a domain
172
-
173
- ```ts
174
- const { data } = await client.domains.create('yourdomain.com')
175
-
176
- // DNS records to add to your domain
177
- console.log(data?.dkim) // { type: 'TXT', name: 'eusend._domainkey.yourdomain.com', value: '...' }
178
- console.log(data?.spf) // { type: 'TXT', name: 'yourdomain.com', value: '...' }
179
- console.log(data?.dmarc) // { type: 'TXT', name: '_dmarc.yourdomain.com', value: '...' }
180
- ```
181
-
182
- ### Verify a domain
183
-
184
- After adding the DNS records, trigger verification:
185
-
186
- ```ts
187
- await client.domains.verify(domainId)
188
- ```
189
-
190
- ### List domains
191
-
192
- ```ts
193
- const { data } = await client.domains.list()
194
- // [{ id, name, status: 'verified', createdAt }]
195
- ```
196
-
197
- ### Get a domain
198
-
199
- ```ts
200
- const { data } = await client.domains.get(domainId)
201
- // { id, name, dkimPublicKey, dkimSelector, status, createdAt, verifiedAt }
202
- ```
203
-
204
- ### Delete a domain
205
-
206
- ```ts
207
- await client.domains.delete(domainId)
208
- ```
209
-
210
- ---
211
-
212
- ## API Keys
213
-
214
- ### Create an API key
215
-
216
- ```ts
217
- const { data } = await client.apiKeys.create({ name: 'Production' })
218
-
219
- console.log(data?.key) // eu_live_... — only returned once, store it securely
220
- ```
221
-
222
- Pass `testMode: true` to create a sandbox key. Emails sent with a test key are accepted and tracked but never delivered.
223
-
224
- ```ts
225
- const { data } = await client.apiKeys.create({ name: 'Sandbox', testMode: true })
226
- // data.key → 'eu_test_...'
227
- ```
228
-
229
- ### List API keys
230
-
231
- ```ts
232
- const { data } = await client.apiKeys.list()
233
- // [{ id, name, prefix, testMode, createdAt, lastUsedAt }]
234
- ```
235
-
236
- The full key is never returned after creation — only the prefix (e.g. `eu_live_Lx_e`).
237
-
238
- ### Delete an API key
239
-
240
- ```ts
241
- await client.apiKeys.delete(keyId)
242
- ```
243
-
244
- ---
245
-
246
- ## Audiences & Contacts
247
-
248
- ### Create an audience
249
-
250
- ```ts
251
- const { data } = await client.audiences.create('Newsletter')
252
- const audienceId = data!.id
253
- ```
254
-
255
- ### List audiences
256
-
257
- ```ts
258
- const { data } = await client.audiences.list()
259
- // [{ id, name, createdAt, contactCount }]
260
- ```
261
-
262
- ### Delete an audience
263
-
264
- ```ts
265
- await client.audiences.delete(audienceId)
266
- ```
267
-
268
- ### Add a contact
269
-
270
- ```ts
271
- const { data } = await client.audiences.createContact(audienceId, {
272
- email: 'user@example.com',
273
- firstName: 'Jane',
274
- lastName: 'Smith',
275
- })
276
- ```
277
-
278
- If a contact with that email already exists in the audience, it will be updated instead.
279
-
280
- ### Bulk import contacts
281
-
282
- Up to 1,000 contacts per call. Existing contacts are upserted.
283
-
284
- ```ts
285
- const { data } = await client.audiences.batchCreateContacts(audienceId, {
286
- contacts: [
287
- { email: 'alice@example.com', firstName: 'Alice' },
288
- { email: 'bob@example.com', firstName: 'Bob' },
289
- ],
290
- })
291
-
292
- console.log(data?.count) // 2
293
- ```
294
-
295
- ### List contacts
296
-
297
- ```ts
298
- const { data } = await client.audiences.listContacts(audienceId, { limit: 100 })
299
-
300
- // Filter by subscription status
301
- await client.audiences.listContacts(audienceId, { subscribed: true })
302
- await client.audiences.listContacts(audienceId, { subscribed: false })
303
-
304
- // Search by email
305
- await client.audiences.listContacts(audienceId, { search: 'gmail.com' })
306
-
307
- // Pagination
308
- await client.audiences.listContacts(audienceId, { cursor: data.nextCursor })
309
- ```
310
-
311
- ### Get a contact
312
-
313
- ```ts
314
- const { data } = await client.audiences.getContact(audienceId, contactId)
315
- // { id, audienceId, email, firstName, lastName, status, unsubscribedAt, createdAt, updatedAt }
316
- ```
317
-
318
- ### Update a contact
319
-
320
- ```ts
321
- // Update name
322
- await client.audiences.updateContact(audienceId, contactId, {
323
- firstName: 'Janet',
324
- })
325
-
326
- // Unsubscribe
327
- await client.audiences.updateContact(audienceId, contactId, {
328
- unsubscribed: true,
329
- })
330
-
331
- // Re-subscribe
332
- await client.audiences.updateContact(audienceId, contactId, {
333
- unsubscribed: false,
334
- })
335
- ```
336
-
337
- ### Delete a contact
338
-
339
- ```ts
340
- await client.audiences.deleteContact(audienceId, contactId)
341
- ```
342
-
343
- ---
344
-
345
- ## Templates
346
-
347
- Templates let you define reusable email layouts with `{{variable}}` placeholders that are substituted at send time.
348
-
349
- > **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.
350
-
351
- ### Create a template (HTML)
352
-
353
- ```ts
354
- const { data } = await client.templates.create({
355
- name: 'Welcome email',
356
- subject: 'Welcome, {{name}}!',
357
- html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
358
- })
359
- ```
360
-
361
- ### Create a template (React Email)
362
-
363
- Pass a React Email component via `react`. The SDK renders it to HTML locally before submitting — the JSX source never travels over the wire.
364
-
365
- ```tsx
366
- import { OrderConfirmation } from './emails/order-confirmation'
367
-
368
- const { data } = await client.templates.create({
369
- name: 'Order confirmation',
370
- subject: 'Your order {{order_id}} is confirmed',
371
- react: <OrderConfirmation />,
372
- })
373
- ```
374
-
375
- 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.
376
-
377
- ### Send using a template
378
-
379
- ```ts
380
- await client.emails.send({
381
- from: 'you@yourdomain.com',
382
- to: 'user@example.com',
383
- templateId: data!.id,
384
- variables: {
385
- first_name: 'Jane',
386
- order_id: 'ORD-1234',
387
- },
388
- })
389
- ```
390
-
391
- ### List, get, update, delete
392
-
393
- ```ts
394
- await client.templates.list()
395
- await client.templates.get(templateId)
396
- await client.templates.update(templateId, { name: 'New name', subject: 'New subject' })
397
- await client.templates.delete(templateId)
398
- ```
399
-
400
- ---
401
-
402
- ## Webhooks
403
-
404
- Receive real-time events when email statuses change.
405
-
406
- ### Create a webhook
407
-
408
- ```ts
409
- const { data } = await client.webhooks.create({
410
- url: 'https://yourapp.com/webhooks/eusend',
411
- events: ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
412
- })
413
-
414
- console.log(data?.secret) // signing secret — only returned once, store it securely
415
- ```
416
-
417
- Pass `'*'` in the events array to subscribe to all events.
418
-
419
- Available events: `email.sent` `email.delivered` `email.bounced` `email.complained` `email.opened` `email.clicked`
420
-
421
- **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.
422
-
423
- ### Verifying webhook signatures
424
-
425
- Every delivery is signed with HMAC-SHA256. Verify the signature before processing:
426
-
427
- ```ts
428
- import { createHmac, timingSafeEqual } from 'crypto'
429
-
430
- function verifyWebhook(req: Request, secret: string): boolean {
431
- const webhookId = req.headers.get('x-webhook-id') ?? ''
432
- const timestamp = req.headers.get('x-webhook-timestamp') ?? ''
433
- const signature = req.headers.get('x-webhook-signature') ?? ''
434
-
435
- const body = await req.text()
436
- const expected = 'v1,' + createHmac('sha256', secret)
437
- .update(`${webhookId}.${timestamp}.${body}`)
438
- .digest('base64')
439
-
440
- return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
441
- }
442
- ```
443
-
444
- ### List, get, update, delete
445
-
446
- ```ts
447
- await client.webhooks.list()
448
- await client.webhooks.get(webhookId) // includes recent deliveries
449
- await client.webhooks.update(webhookId, { events: ['email.bounced'] })
450
- await client.webhooks.delete(webhookId)
451
- ```
452
-
453
- ---
454
-
455
- ## Broadcasts
456
-
457
- Send a single email to every contact in an audience.
458
-
459
- ### Create a broadcast
460
-
461
- ```ts
462
- const { data } = await client.broadcasts.create({
463
- name: 'May newsletter',
464
- audienceId: 'aud_...',
465
- from: 'Sivert <hello@yourdomain.com>',
466
- subject: 'May update',
467
- html: '<p>Hi {{first_name}}, here is this month's update...</p>',
468
- })
469
- ```
470
-
471
- 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:
472
-
473
- ```tsx
474
- import { MayNewsletter } from './emails/may-newsletter'
475
-
476
- await client.broadcasts.create({
477
- name: 'May newsletter',
478
- audienceId: 'aud_...',
479
- from: 'Sivert <hello@yourdomain.com>',
480
- subject: 'May update',
481
- react: <MayNewsletter />,
482
- })
483
- ```
484
-
485
- `{{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.
486
-
487
- #### Unsubscribe handling
488
-
489
- Broadcasts and any send addressed to an 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. 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.
490
-
491
- ### Send a broadcast
492
-
493
- ```ts
494
- await client.broadcasts.send(broadcastId)
495
- ```
496
-
497
- ### Schedule a broadcast
498
-
499
- ```ts
500
- await client.broadcasts.send(broadcastId, {
501
- scheduledAt: '2026-06-01T09:00:00.000Z',
502
- })
503
- ```
504
-
505
- ### Cancel a broadcast
506
-
507
- ```ts
508
- await client.broadcasts.cancel(broadcastId)
509
- ```
510
-
511
- ### List, get, update, delete
512
-
513
- ```ts
514
- await client.broadcasts.list()
515
- await client.broadcasts.get(broadcastId) // includes delivery stats
516
- await client.broadcasts.update(broadcastId, { subject: 'Updated subject' })
517
- await client.broadcasts.delete(broadcastId)
518
- ```
519
-
520
- ---
521
-
522
- ## Error handling
523
-
524
- Every method returns `{ data, error, headers }`. On success `error` is `null`; on failure `data` is `null`.
525
-
526
- ```ts
527
- const { data, error } = await client.emails.send({ ... })
528
-
529
- if (error) {
530
- console.error(error.name) // 'MONTHLY_LIMIT_EXCEEDED'
531
- console.error(error.message) // 'Monthly send limit exceeded'
532
- console.error(error.statusCode) // 429
533
- } else {
534
- console.log(data.id)
535
- }
536
- ```
537
-
538
- #### Error codes
539
-
540
- | Code | Status | Description |
541
- |------|--------|-------------|
542
- | `UNAUTHORIZED` | 401 | Invalid or missing API key |
543
- | `FORBIDDEN` | 403 | Action not allowed on your plan |
544
- | `NOT_FOUND` | 404 | Resource not found |
545
- | `VALIDATION_ERROR` | 400 | Invalid request body |
546
- | `BAD_REQUEST` | 400 | Malformed request |
547
- | `CONFLICT` | 409 | Resource already exists |
548
- | `RATE_LIMITED` | 429 | Too many requests |
549
- | `MONTHLY_LIMIT_EXCEEDED` | 429 | Monthly send quota reached |
550
- | `DAILY_LIMIT_EXCEEDED` | 429 | Daily send quota reached (free plan) |
551
- | `PLAN_LIMIT_EXCEEDED` | 403 | Feature not available on your plan |
552
- | `DOMAIN_NOT_VERIFIED` | 403 | The sender domain is not verified for your organisation |
553
- | `SENDING_SUSPENDED` | 403 | Sending suspended for your account (high bounce or complaint rate) |
554
- | `ALL_SUPPRESSED` | 422 | All recipients are on the suppression list |
555
- | `SERVICE_PAUSED` | 503 | Sending is temporarily paused platform-wide |
556
- | `INTERNAL_ERROR` | 500 | Server error |
557
- | `application_error` | `null` | Network failure request never reached the server |
558
-
559
- ---
560
-
561
- ## TypeScript
562
-
563
- The SDK is written in TypeScript and ships with full type definitions. All request options, response shapes, and error codes are typed.
564
-
565
- ```ts
566
- import type {
567
- SendEmailOptions,
568
- Email,
569
- EmailStatus,
570
- EusendError,
571
- EusendResponse,
572
- } from '@eusend_dev/sdk'
573
- ```
574
-
575
- ---
576
-
577
- ## Requirements
578
-
579
- - Node.js 18 or later (uses the native `fetch` API)
580
- - An Eusend account and API key — [eusend.dev](https://eusend.dev)
1
+ # eusend
2
+
3
+ Official Node.js SDK for the [Eusend](https://eusend.dev) API — the EU-native transactional email platform.
4
+
5
+ ```bash
6
+ npm install @eusend_dev/sdk
7
+ # or
8
+ bun add @eusend_dev/sdk
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Getting started
14
+
15
+ ```ts
16
+ import { Eusend } from '@eusend_dev/sdk'
17
+
18
+ const client = new Eusend('eu_live_...')
19
+ ```
20
+
21
+ Your API key can also be set via the `EUSEND_API_KEY` environment variable, in which case the constructor argument can be omitted:
22
+
23
+ ```ts
24
+ const client = new Eusend()
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Emails
30
+
31
+ ### Send an email
32
+
33
+ ```ts
34
+ const { data, error } = await client.emails.send({
35
+ // `from` accepts a bare email or a display-name form: `Acme <you@yourdomain.com>`
36
+ from: 'Acme <you@yourdomain.com>',
37
+ to: 'user@example.com',
38
+ subject: 'Hello',
39
+ html: '<p>Hello world</p>',
40
+ text: 'Hello world',
41
+ })
42
+
43
+ console.log(data?.id) // 9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d (UUID)
44
+ ```
45
+
46
+ #### Options
47
+
48
+ | Field | Type | Description |
49
+ |-------|------|-------------|
50
+ | `from` | `string` | Sender address — a bare email or display-name form (`Acme <you@yourdomain.com>`). Must be from a verified domain. |
51
+ | `to` | `string \| string[]` | Recipient(s). Maximum 50. |
52
+ | `cc` | `string \| string[]` | CC recipient(s). Maximum 50. |
53
+ | `bcc` | `string \| string[]` | BCC recipient(s). Maximum 50. |
54
+ | `replyTo` | `string \| string[]` | Reply-to address(es). Maximum 50. |
55
+ | `subject` | `string` | Email subject |
56
+ | `html` | `string` | HTML body |
57
+ | `text` | `string` | Plain text body |
58
+ | `templateId` | `string` | ID of a saved template |
59
+ | `variables` | `Record<string, unknown>` | Template variable substitutions |
60
+ | `headers` | `Record<string, string>` | Custom email headers, written into the outbound message. Header names and values may not contain line breaks. |
61
+ | `trackOpens` | `boolean` | Track open events (default: `true`) |
62
+ | `trackClicks` | `boolean` | Track click events (default: `true`) |
63
+
64
+ At least one of `html`, `react`, `text`, or `templateId` is required.
65
+
66
+ ### Send with React Email
67
+
68
+ 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.
69
+
70
+ ```tsx
71
+ import { Eusend } from '@eusend_dev/sdk'
72
+ import { WelcomeEmail } from './emails/welcome'
73
+
74
+ const client = new Eusend()
75
+
76
+ await client.emails.send({
77
+ from: 'hello@yourdomain.com',
78
+ to: 'user@example.com',
79
+ subject: 'Welcome',
80
+ react: <WelcomeEmail name="Jane" />,
81
+ })
82
+ ```
83
+
84
+ Requires `react` and `@react-email/render` as peer dependencies:
85
+
86
+ ```bash
87
+ npm install react @react-email/render
88
+ ```
89
+
90
+ If you prefer to render yourself, pass the resulting HTML via `html` instead — useful when you want one rendered template to serve multiple sends.
91
+
92
+ ### Idempotent sends
93
+
94
+ 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.
95
+
96
+ ```ts
97
+ const { data } = await client.emails.send(
98
+ {
99
+ from: 'you@yourdomain.com',
100
+ to: 'user@example.com',
101
+ subject: 'Your receipt',
102
+ html: '<p>Thanks for your order.</p>',
103
+ },
104
+ { idempotencyKey: `receipt-${orderId}` },
105
+ )
106
+ ```
107
+
108
+ ### Send a batch
109
+
110
+ Up to 100 emails in a single request.
111
+
112
+ ```ts
113
+ const { data } = await client.emails.batch([
114
+ {
115
+ from: 'you@yourdomain.com',
116
+ to: 'alice@example.com',
117
+ subject: 'Hello Alice',
118
+ html: '<p>Hi Alice</p>',
119
+ },
120
+ {
121
+ from: 'you@yourdomain.com',
122
+ to: 'bob@example.com',
123
+ subject: 'Hello Bob',
124
+ html: '<p>Hi Bob</p>',
125
+ },
126
+ ])
127
+
128
+ console.log(data?.data) // [{ id: '...' }, { id: '...' }]
129
+ ```
130
+
131
+ ### Retrieve an email
132
+
133
+ ```ts
134
+ const { data } = await client.emails.get('9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d')
135
+
136
+ console.log(data?.status) // 'delivered'
137
+ console.log(data?.events) // [{ type: 'sent', ... }, { type: 'delivered', ... }]
138
+ ```
139
+
140
+ ### List emails
141
+
142
+ ```ts
143
+ const { data } = await client.emails.list({ limit: 20 })
144
+
145
+ console.log(data?.data) // array of emails
146
+ console.log(data?.nextCursor) // pass as cursor to fetch the next page
147
+ ```
148
+
149
+ #### Filtering
150
+
151
+ ```ts
152
+ // By status
153
+ await client.emails.list({ status: 'delivered' })
154
+
155
+ // By sender
156
+ await client.emails.list({ from: 'you@yourdomain.com' })
157
+
158
+ // By recipient
159
+ await client.emails.list({ to: 'user@example.com' })
160
+
161
+ // Pagination
162
+ await client.emails.list({ limit: 50, cursor: data.nextCursor })
163
+ ```
164
+
165
+ Available statuses: `queued` `sending` `sent` `delivered` `bounced` `complained` `suppressed` `failed`
166
+
167
+ ---
168
+
169
+ ## Domains
170
+
171
+ ### Add a domain
172
+
173
+ ```ts
174
+ const { data } = await client.domains.create('yourdomain.com')
175
+
176
+ // DNS records to add to your domain
177
+ console.log(data?.dkim) // { type: 'TXT', name: 'eusend._domainkey.yourdomain.com', value: '...' }
178
+ console.log(data?.spf) // { type: 'TXT', name: 'yourdomain.com', value: '...' }
179
+ console.log(data?.dmarc) // { type: 'TXT', name: '_dmarc.yourdomain.com', value: '...' }
180
+ ```
181
+
182
+ ### Verify a domain
183
+
184
+ After adding the DNS records, trigger verification:
185
+
186
+ ```ts
187
+ await client.domains.verify(domainId)
188
+ ```
189
+
190
+ ### List domains
191
+
192
+ ```ts
193
+ const { data } = await client.domains.list()
194
+ // [{ id, name, status: 'verified', createdAt }]
195
+ ```
196
+
197
+ ### Get a domain
198
+
199
+ ```ts
200
+ const { data } = await client.domains.get(domainId)
201
+ // { id, name, dkimPublicKey, dkimSelector, status, createdAt, verifiedAt }
202
+ ```
203
+
204
+ ### Delete a domain
205
+
206
+ ```ts
207
+ await client.domains.delete(domainId)
208
+ ```
209
+
210
+ ---
211
+
212
+ ## API Keys
213
+
214
+ ### Create an API key
215
+
216
+ ```ts
217
+ const { data } = await client.apiKeys.create({ name: 'Production' })
218
+
219
+ console.log(data?.key) // eu_live_... — only returned once, store it securely
220
+ ```
221
+
222
+ Pass `testMode: true` to create a sandbox key. Emails sent with a test key are accepted and tracked but never delivered.
223
+
224
+ ```ts
225
+ const { data } = await client.apiKeys.create({ name: 'Sandbox', testMode: true })
226
+ // data.key → 'eu_test_...'
227
+ ```
228
+
229
+ ### List API keys
230
+
231
+ ```ts
232
+ const { data } = await client.apiKeys.list()
233
+ // [{ id, name, prefix, testMode, createdAt, lastUsedAt }]
234
+ ```
235
+
236
+ The full key is never returned after creation — only the prefix (e.g. `eu_live_Lx_e`).
237
+
238
+ ### Delete an API key
239
+
240
+ ```ts
241
+ await client.apiKeys.delete(keyId)
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Audiences & Contacts
247
+
248
+ ### Create an audience
249
+
250
+ ```ts
251
+ const { data } = await client.audiences.create('Newsletter')
252
+ const audienceId = data!.id
253
+ ```
254
+
255
+ ### List audiences
256
+
257
+ ```ts
258
+ const { data } = await client.audiences.list()
259
+ // [{ id, name, createdAt, contactCount }]
260
+ ```
261
+
262
+ ### Delete an audience
263
+
264
+ ```ts
265
+ await client.audiences.delete(audienceId)
266
+ ```
267
+
268
+ ### Add a contact
269
+
270
+ ```ts
271
+ const { data } = await client.audiences.createContact(audienceId, {
272
+ email: 'user@example.com',
273
+ firstName: 'Jane',
274
+ lastName: 'Smith',
275
+ })
276
+ ```
277
+
278
+ If a contact with that email already exists in the audience, it will be updated instead.
279
+
280
+ ### Bulk import contacts
281
+
282
+ Up to 1,000 contacts per call. Existing contacts are upserted.
283
+
284
+ ```ts
285
+ const { data } = await client.audiences.batchCreateContacts(audienceId, {
286
+ contacts: [
287
+ { email: 'alice@example.com', firstName: 'Alice' },
288
+ { email: 'bob@example.com', firstName: 'Bob' },
289
+ ],
290
+ })
291
+
292
+ console.log(data?.count) // 2
293
+ ```
294
+
295
+ ### List contacts
296
+
297
+ ```ts
298
+ const { data } = await client.audiences.listContacts(audienceId, { limit: 100 })
299
+
300
+ // Filter by subscription status
301
+ await client.audiences.listContacts(audienceId, { subscribed: true })
302
+ await client.audiences.listContacts(audienceId, { subscribed: false })
303
+
304
+ // Search by email
305
+ await client.audiences.listContacts(audienceId, { search: 'gmail.com' })
306
+
307
+ // Pagination
308
+ await client.audiences.listContacts(audienceId, { cursor: data.nextCursor })
309
+ ```
310
+
311
+ ### Get a contact
312
+
313
+ ```ts
314
+ const { data } = await client.audiences.getContact(audienceId, contactId)
315
+ // { id, audienceId, email, firstName, lastName, status, unsubscribedAt, createdAt, updatedAt }
316
+ ```
317
+
318
+ ### Update a contact
319
+
320
+ ```ts
321
+ // Update name
322
+ await client.audiences.updateContact(audienceId, contactId, {
323
+ firstName: 'Janet',
324
+ })
325
+
326
+ // Unsubscribe
327
+ await client.audiences.updateContact(audienceId, contactId, {
328
+ unsubscribed: true,
329
+ })
330
+
331
+ // Re-subscribe
332
+ await client.audiences.updateContact(audienceId, contactId, {
333
+ unsubscribed: false,
334
+ })
335
+ ```
336
+
337
+ ### Delete a contact
338
+
339
+ ```ts
340
+ await client.audiences.deleteContact(audienceId, contactId)
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Templates
346
+
347
+ Templates let you define reusable email layouts with `{{variable}}` placeholders that are substituted at send time.
348
+
349
+ > **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.
350
+
351
+ ### Create a template (HTML)
352
+
353
+ ```ts
354
+ const { data } = await client.templates.create({
355
+ name: 'Welcome email',
356
+ subject: 'Welcome, {{name}}!',
357
+ html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
358
+ })
359
+ ```
360
+
361
+ ### Create a template (React Email)
362
+
363
+ Pass a React Email component via `react`. The SDK renders it to HTML locally before submitting — the JSX source never travels over the wire.
364
+
365
+ ```tsx
366
+ import { OrderConfirmation } from './emails/order-confirmation'
367
+
368
+ const { data } = await client.templates.create({
369
+ name: 'Order confirmation',
370
+ subject: 'Your order {{order_id}} is confirmed',
371
+ react: <OrderConfirmation />,
372
+ })
373
+ ```
374
+
375
+ 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.
376
+
377
+ ### Send using a template
378
+
379
+ ```ts
380
+ await client.emails.send({
381
+ from: 'you@yourdomain.com',
382
+ to: 'user@example.com',
383
+ templateId: data!.id,
384
+ variables: {
385
+ first_name: 'Jane',
386
+ order_id: 'ORD-1234',
387
+ },
388
+ })
389
+ ```
390
+
391
+ ### List, get, update, delete
392
+
393
+ ```ts
394
+ await client.templates.list()
395
+ await client.templates.get(templateId)
396
+ await client.templates.update(templateId, { name: 'New name', subject: 'New subject' })
397
+ await client.templates.delete(templateId)
398
+ ```
399
+
400
+ ---
401
+
402
+ ## Webhooks
403
+
404
+ Receive real-time events when email statuses change.
405
+
406
+ ### Create a webhook
407
+
408
+ ```ts
409
+ const { data } = await client.webhooks.create({
410
+ url: 'https://yourapp.com/webhooks/eusend',
411
+ events: ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
412
+ })
413
+
414
+ console.log(data?.secret) // signing secret — only returned once, store it securely
415
+ ```
416
+
417
+ Pass `'*'` in the events array to subscribe to all events.
418
+
419
+ Available events: `email.sent` `email.delivered` `email.bounced` `email.complained` `email.opened` `email.clicked`
420
+
421
+ **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.
422
+
423
+ ### Verifying webhook signatures
424
+
425
+ Every delivery is signed with HMAC-SHA256. Verify the signature before processing:
426
+
427
+ ```ts
428
+ import { createHmac, timingSafeEqual } from 'crypto'
429
+
430
+ async function verifyWebhook(req: Request, secret: string): Promise<boolean> {
431
+ const webhookId = req.headers.get('webhook-id') ?? ''
432
+ const timestamp = req.headers.get('webhook-timestamp') ?? ''
433
+ const signature = req.headers.get('webhook-signature') ?? ''
434
+
435
+ const body = await req.text()
436
+ const expected = 'v1,' + createHmac('sha256', secret)
437
+ .update(`${webhookId}.${timestamp}.${body}`)
438
+ .digest('base64')
439
+
440
+ return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
441
+ }
442
+ ```
443
+
444
+ ### List, get, update, delete
445
+
446
+ ```ts
447
+ await client.webhooks.list()
448
+ await client.webhooks.get(webhookId) // includes recent deliveries
449
+ await client.webhooks.update(webhookId, { events: ['email.bounced'] })
450
+ await client.webhooks.delete(webhookId)
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Broadcasts
456
+
457
+ Send a single email to every contact in an audience.
458
+
459
+ ### Create a broadcast
460
+
461
+ ```ts
462
+ const { data } = await client.broadcasts.create({
463
+ name: 'May newsletter',
464
+ audienceId: '550e8400-e29b-41d4-a716-446655440000',
465
+ from: 'Sivert <hello@yourdomain.com>',
466
+ subject: 'May update',
467
+ html: '<p>Hi {{first_name}}, your monthly update is here...</p>',
468
+ })
469
+ ```
470
+
471
+ 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:
472
+
473
+ ```tsx
474
+ import { MayNewsletter } from './emails/may-newsletter'
475
+
476
+ await client.broadcasts.create({
477
+ name: 'May newsletter',
478
+ audienceId: '550e8400-e29b-41d4-a716-446655440000',
479
+ from: 'Sivert <hello@yourdomain.com>',
480
+ subject: 'May update',
481
+ react: <MayNewsletter />,
482
+ })
483
+ ```
484
+
485
+ `{{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.
486
+
487
+ #### Unsubscribe handling
488
+
489
+ 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.
490
+
491
+ ### Send a broadcast
492
+
493
+ ```ts
494
+ await client.broadcasts.send(broadcastId)
495
+ ```
496
+
497
+ 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.
498
+
499
+ ### Schedule a broadcast
500
+
501
+ ```ts
502
+ await client.broadcasts.send(broadcastId, {
503
+ scheduledAt: '2026-06-01T09:00:00.000Z',
504
+ })
505
+ ```
506
+
507
+ ### Cancel a broadcast
508
+
509
+ ```ts
510
+ await client.broadcasts.cancel(broadcastId)
511
+ ```
512
+
513
+ ### List, get, update, delete
514
+
515
+ ```ts
516
+ await client.broadcasts.list()
517
+ await client.broadcasts.get(broadcastId) // includes delivery stats
518
+ await client.broadcasts.update(broadcastId, { subject: 'Updated subject' })
519
+ await client.broadcasts.delete(broadcastId)
520
+ ```
521
+
522
+ ---
523
+
524
+ ## Error handling
525
+
526
+ Every method returns `{ data, error, headers }`. On success `error` is `null`; on failure `data` is `null`.
527
+
528
+ ```ts
529
+ const { data, error } = await client.emails.send({ ... })
530
+
531
+ if (error) {
532
+ console.error(error.name) // 'MONTHLY_LIMIT_EXCEEDED'
533
+ console.error(error.message) // 'Monthly send limit exceeded'
534
+ console.error(error.statusCode) // 429
535
+ } else {
536
+ console.log(data.id)
537
+ }
538
+ ```
539
+
540
+ #### Error codes
541
+
542
+ | Code | Status | Description |
543
+ |------|--------|-------------|
544
+ | `UNAUTHORIZED` | 401 | Invalid or missing API key |
545
+ | `FORBIDDEN` | 403 | Action not allowed on your plan |
546
+ | `NOT_FOUND` | 404 | Resource not found |
547
+ | `VALIDATION_ERROR` | 400 | Invalid request body |
548
+ | `BAD_REQUEST` | 400 | Malformed request |
549
+ | `CONFLICT` | 409 | Resource already exists |
550
+ | `RATE_LIMITED` | 429 | Too many requests |
551
+ | `MONTHLY_LIMIT_EXCEEDED` | 429 | Monthly send quota reached |
552
+ | `DAILY_LIMIT_EXCEEDED` | 429 | Daily send ceiling reached (applies to all plans; ramps up as your account warms, resets midnight UTC) |
553
+ | `PLAN_LIMIT_EXCEEDED` | 403 | Feature not available on your plan |
554
+ | `DOMAIN_NOT_VERIFIED` | 403 | The sender domain is not verified for your organisation |
555
+ | `SENDING_SUSPENDED` | 403 | Sending suspended for your account (high bounce or complaint rate) |
556
+ | `ALL_SUPPRESSED` | 422 | All recipients are on the suppression list |
557
+ | `SERVICE_PAUSED` | 503 | Sending is temporarily paused platform-wide |
558
+ | `INTERNAL_ERROR` | 500 | Server error |
559
+ | `application_error` | `null` | Network failure — request never reached the server |
560
+
561
+ ---
562
+
563
+ ## TypeScript
564
+
565
+ The SDK is written in TypeScript and ships with full type definitions. All request options, response shapes, and error codes are typed.
566
+
567
+ ```ts
568
+ import type {
569
+ SendEmailOptions,
570
+ Email,
571
+ EmailStatus,
572
+ EusendError,
573
+ EusendResponse,
574
+ } from '@eusend_dev/sdk'
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Requirements
580
+
581
+ - Node.js 18 or later (uses the native `fetch` API)
582
+ - An Eusend account and API key — [eusend.dev](https://eusend.dev)