@eusend_dev/sdk 0.3.2 → 0.3.3

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