@eusend_dev/sdk 0.1.0 → 0.3.0

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