@eusend_dev/sdk 0.3.4 → 0.3.5
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 +572 -572
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +51 -51
package/README.md
CHANGED
|
@@ -1,572 +1,572 @@
|
|
|
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. **Note:** not currently applied to outbound mail — the send path uses SES `SendEmail`, which doesn't carry custom 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
|
-
> **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.
|
|
349
|
-
|
|
350
|
-
### Create a template (HTML)
|
|
351
|
-
|
|
352
|
-
```ts
|
|
353
|
-
const { data } = await client.templates.create({
|
|
354
|
-
name: 'Welcome email',
|
|
355
|
-
subject: 'Welcome, {{name}}!',
|
|
356
|
-
html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
|
|
357
|
-
})
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Create a template (React Email)
|
|
361
|
-
|
|
362
|
-
Pass a React Email component via `react`. The SDK renders it to HTML locally before submitting — the JSX source never travels over the wire.
|
|
363
|
-
|
|
364
|
-
```tsx
|
|
365
|
-
import { OrderConfirmation } from './emails/order-confirmation'
|
|
366
|
-
|
|
367
|
-
const { data } = await client.templates.create({
|
|
368
|
-
name: 'Order confirmation',
|
|
369
|
-
subject: 'Your order {{order_id}} is confirmed',
|
|
370
|
-
react: <OrderConfirmation />,
|
|
371
|
-
})
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
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.
|
|
375
|
-
|
|
376
|
-
### Send using a template
|
|
377
|
-
|
|
378
|
-
```ts
|
|
379
|
-
await client.emails.send({
|
|
380
|
-
from: 'you@yourdomain.com',
|
|
381
|
-
to: 'user@example.com',
|
|
382
|
-
templateId: data!.id,
|
|
383
|
-
variables: {
|
|
384
|
-
first_name: 'Jane',
|
|
385
|
-
order_id: 'ORD-1234',
|
|
386
|
-
},
|
|
387
|
-
})
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### List, get, update, delete
|
|
391
|
-
|
|
392
|
-
```ts
|
|
393
|
-
await client.templates.list()
|
|
394
|
-
await client.templates.get(templateId)
|
|
395
|
-
await client.templates.update(templateId, { name: 'New name', subject: 'New subject' })
|
|
396
|
-
await client.templates.delete(templateId)
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
## Webhooks
|
|
402
|
-
|
|
403
|
-
Receive real-time events when email statuses change.
|
|
404
|
-
|
|
405
|
-
### Create a webhook
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
const { data } = await client.webhooks.create({
|
|
409
|
-
url: 'https://yourapp.com/webhooks/eusend',
|
|
410
|
-
events: ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
console.log(data?.secret) // signing secret — only returned once, store it securely
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
Pass `'*'` in the events array to subscribe to all events.
|
|
417
|
-
|
|
418
|
-
Available events: `email.sent` `email.delivered` `email.bounced` `email.complained` `email.opened` `email.clicked`
|
|
419
|
-
|
|
420
|
-
**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.
|
|
421
|
-
|
|
422
|
-
### Verifying webhook signatures
|
|
423
|
-
|
|
424
|
-
Every delivery is signed with HMAC-SHA256. Verify the signature before processing:
|
|
425
|
-
|
|
426
|
-
```ts
|
|
427
|
-
import { createHmac, timingSafeEqual } from 'crypto'
|
|
428
|
-
|
|
429
|
-
function verifyWebhook(req: Request, secret: string): boolean {
|
|
430
|
-
const webhookId = req.headers.get('x-webhook-id') ?? ''
|
|
431
|
-
const timestamp = req.headers.get('x-webhook-timestamp') ?? ''
|
|
432
|
-
const signature = req.headers.get('x-webhook-signature') ?? ''
|
|
433
|
-
|
|
434
|
-
const body = await req.text()
|
|
435
|
-
const expected = 'v1,' + createHmac('sha256', secret)
|
|
436
|
-
.update(`${webhookId}.${timestamp}.${body}`)
|
|
437
|
-
.digest('base64')
|
|
438
|
-
|
|
439
|
-
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### List, get, update, delete
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
await client.webhooks.list()
|
|
447
|
-
await client.webhooks.get(webhookId) // includes recent deliveries
|
|
448
|
-
await client.webhooks.update(webhookId, { events: ['email.bounced'] })
|
|
449
|
-
await client.webhooks.delete(webhookId)
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
---
|
|
453
|
-
|
|
454
|
-
## Broadcasts
|
|
455
|
-
|
|
456
|
-
Send a single email to every contact in an audience.
|
|
457
|
-
|
|
458
|
-
### Create a broadcast
|
|
459
|
-
|
|
460
|
-
```ts
|
|
461
|
-
const { data } = await client.broadcasts.create({
|
|
462
|
-
name: 'May newsletter',
|
|
463
|
-
audienceId: 'aud_...',
|
|
464
|
-
from: 'Sivert <hello@yourdomain.com>',
|
|
465
|
-
subject: 'May update',
|
|
466
|
-
html: '<p>Hi {{first_name}}, here is this month's update...</p>',
|
|
467
|
-
})
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
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:
|
|
471
|
-
|
|
472
|
-
```tsx
|
|
473
|
-
import { MayNewsletter } from './emails/may-newsletter'
|
|
474
|
-
|
|
475
|
-
await client.broadcasts.create({
|
|
476
|
-
name: 'May newsletter',
|
|
477
|
-
audienceId: 'aud_...',
|
|
478
|
-
from: 'Sivert <hello@yourdomain.com>',
|
|
479
|
-
subject: 'May update',
|
|
480
|
-
react: <MayNewsletter />,
|
|
481
|
-
})
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
`{{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.
|
|
485
|
-
|
|
486
|
-
### Send a broadcast
|
|
487
|
-
|
|
488
|
-
```ts
|
|
489
|
-
await client.broadcasts.send(broadcastId)
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### Schedule a broadcast
|
|
493
|
-
|
|
494
|
-
```ts
|
|
495
|
-
await client.broadcasts.send(broadcastId, {
|
|
496
|
-
scheduledAt: '2026-06-01T09:00:00.000Z',
|
|
497
|
-
})
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### Cancel a broadcast
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
await client.broadcasts.cancel(broadcastId)
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### List, get, update, delete
|
|
507
|
-
|
|
508
|
-
```ts
|
|
509
|
-
await client.broadcasts.list()
|
|
510
|
-
await client.broadcasts.get(broadcastId) // includes delivery stats
|
|
511
|
-
await client.broadcasts.update(broadcastId, { subject: 'Updated subject' })
|
|
512
|
-
await client.broadcasts.delete(broadcastId)
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
---
|
|
516
|
-
|
|
517
|
-
## Error handling
|
|
518
|
-
|
|
519
|
-
Every method returns `{ data, error, headers }`. On success `error` is `null`; on failure `data` is `null`.
|
|
520
|
-
|
|
521
|
-
```ts
|
|
522
|
-
const { data, error } = await client.emails.send({ ... })
|
|
523
|
-
|
|
524
|
-
if (error) {
|
|
525
|
-
console.error(error.name) // 'MONTHLY_LIMIT_EXCEEDED'
|
|
526
|
-
console.error(error.message) // 'Monthly send limit exceeded'
|
|
527
|
-
console.error(error.statusCode) // 429
|
|
528
|
-
} else {
|
|
529
|
-
console.log(data.id)
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
#### Error codes
|
|
534
|
-
|
|
535
|
-
| Code | Status | Description |
|
|
536
|
-
|------|--------|-------------|
|
|
537
|
-
| `UNAUTHORIZED` | 401 | Invalid or missing API key |
|
|
538
|
-
| `FORBIDDEN` | 403 | Action not allowed on your plan |
|
|
539
|
-
| `NOT_FOUND` | 404 | Resource not found |
|
|
540
|
-
| `VALIDATION_ERROR` | 400 | Invalid request body |
|
|
541
|
-
| `CONFLICT` | 409 | Resource already exists |
|
|
542
|
-
| `RATE_LIMITED` | 429 | Too many requests |
|
|
543
|
-
| `MONTHLY_LIMIT_EXCEEDED` | 429 | Monthly send quota reached |
|
|
544
|
-
| `DAILY_LIMIT_EXCEEDED` | 429 | Daily send quota reached (free plan) |
|
|
545
|
-
| `PLAN_LIMIT_EXCEEDED` | 403 | Feature not available on your plan |
|
|
546
|
-
| `DOMAIN_NOT_VERIFIED` | 403 | The sender domain is not verified for your organisation |
|
|
547
|
-
| `ALL_SUPPRESSED` | 422 | All recipients are on the suppression list |
|
|
548
|
-
| `INTERNAL_ERROR` | 500 | Server error |
|
|
549
|
-
| `application_error` | `null` | Network failure — request never reached the server |
|
|
550
|
-
|
|
551
|
-
---
|
|
552
|
-
|
|
553
|
-
## TypeScript
|
|
554
|
-
|
|
555
|
-
The SDK is written in TypeScript and ships with full type definitions. All request options, response shapes, and error codes are typed.
|
|
556
|
-
|
|
557
|
-
```ts
|
|
558
|
-
import type {
|
|
559
|
-
SendEmailOptions,
|
|
560
|
-
Email,
|
|
561
|
-
EmailStatus,
|
|
562
|
-
EusendError,
|
|
563
|
-
EusendResponse,
|
|
564
|
-
} from '@eusend_dev/sdk'
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
## Requirements
|
|
570
|
-
|
|
571
|
-
- Node.js 18 or later (uses the native `fetch` API)
|
|
572
|
-
- 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. **Note:** not currently applied to outbound mail — the send path uses SES `SendEmail`, which doesn't carry custom 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
|
+
> **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.
|
|
349
|
+
|
|
350
|
+
### Create a template (HTML)
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const { data } = await client.templates.create({
|
|
354
|
+
name: 'Welcome email',
|
|
355
|
+
subject: 'Welcome, {{name}}!',
|
|
356
|
+
html: '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Create a template (React Email)
|
|
361
|
+
|
|
362
|
+
Pass a React Email component via `react`. The SDK renders it to HTML locally before submitting — the JSX source never travels over the wire.
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { OrderConfirmation } from './emails/order-confirmation'
|
|
366
|
+
|
|
367
|
+
const { data } = await client.templates.create({
|
|
368
|
+
name: 'Order confirmation',
|
|
369
|
+
subject: 'Your order {{order_id}} is confirmed',
|
|
370
|
+
react: <OrderConfirmation />,
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
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.
|
|
375
|
+
|
|
376
|
+
### Send using a template
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
await client.emails.send({
|
|
380
|
+
from: 'you@yourdomain.com',
|
|
381
|
+
to: 'user@example.com',
|
|
382
|
+
templateId: data!.id,
|
|
383
|
+
variables: {
|
|
384
|
+
first_name: 'Jane',
|
|
385
|
+
order_id: 'ORD-1234',
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### List, get, update, delete
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
await client.templates.list()
|
|
394
|
+
await client.templates.get(templateId)
|
|
395
|
+
await client.templates.update(templateId, { name: 'New name', subject: 'New subject' })
|
|
396
|
+
await client.templates.delete(templateId)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Webhooks
|
|
402
|
+
|
|
403
|
+
Receive real-time events when email statuses change.
|
|
404
|
+
|
|
405
|
+
### Create a webhook
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
const { data } = await client.webhooks.create({
|
|
409
|
+
url: 'https://yourapp.com/webhooks/eusend',
|
|
410
|
+
events: ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
console.log(data?.secret) // signing secret — only returned once, store it securely
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Pass `'*'` in the events array to subscribe to all events.
|
|
417
|
+
|
|
418
|
+
Available events: `email.sent` `email.delivered` `email.bounced` `email.complained` `email.opened` `email.clicked`
|
|
419
|
+
|
|
420
|
+
**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.
|
|
421
|
+
|
|
422
|
+
### Verifying webhook signatures
|
|
423
|
+
|
|
424
|
+
Every delivery is signed with HMAC-SHA256. Verify the signature before processing:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
import { createHmac, timingSafeEqual } from 'crypto'
|
|
428
|
+
|
|
429
|
+
function verifyWebhook(req: Request, secret: string): boolean {
|
|
430
|
+
const webhookId = req.headers.get('x-webhook-id') ?? ''
|
|
431
|
+
const timestamp = req.headers.get('x-webhook-timestamp') ?? ''
|
|
432
|
+
const signature = req.headers.get('x-webhook-signature') ?? ''
|
|
433
|
+
|
|
434
|
+
const body = await req.text()
|
|
435
|
+
const expected = 'v1,' + createHmac('sha256', secret)
|
|
436
|
+
.update(`${webhookId}.${timestamp}.${body}`)
|
|
437
|
+
.digest('base64')
|
|
438
|
+
|
|
439
|
+
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### List, get, update, delete
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
await client.webhooks.list()
|
|
447
|
+
await client.webhooks.get(webhookId) // includes recent deliveries
|
|
448
|
+
await client.webhooks.update(webhookId, { events: ['email.bounced'] })
|
|
449
|
+
await client.webhooks.delete(webhookId)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Broadcasts
|
|
455
|
+
|
|
456
|
+
Send a single email to every contact in an audience.
|
|
457
|
+
|
|
458
|
+
### Create a broadcast
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
const { data } = await client.broadcasts.create({
|
|
462
|
+
name: 'May newsletter',
|
|
463
|
+
audienceId: 'aud_...',
|
|
464
|
+
from: 'Sivert <hello@yourdomain.com>',
|
|
465
|
+
subject: 'May update',
|
|
466
|
+
html: '<p>Hi {{first_name}}, here is this month's update...</p>',
|
|
467
|
+
})
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
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:
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
import { MayNewsletter } from './emails/may-newsletter'
|
|
474
|
+
|
|
475
|
+
await client.broadcasts.create({
|
|
476
|
+
name: 'May newsletter',
|
|
477
|
+
audienceId: 'aud_...',
|
|
478
|
+
from: 'Sivert <hello@yourdomain.com>',
|
|
479
|
+
subject: 'May update',
|
|
480
|
+
react: <MayNewsletter />,
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`{{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.
|
|
485
|
+
|
|
486
|
+
### Send a broadcast
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
await client.broadcasts.send(broadcastId)
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Schedule a broadcast
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
await client.broadcasts.send(broadcastId, {
|
|
496
|
+
scheduledAt: '2026-06-01T09:00:00.000Z',
|
|
497
|
+
})
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Cancel a broadcast
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
await client.broadcasts.cancel(broadcastId)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### List, get, update, delete
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
await client.broadcasts.list()
|
|
510
|
+
await client.broadcasts.get(broadcastId) // includes delivery stats
|
|
511
|
+
await client.broadcasts.update(broadcastId, { subject: 'Updated subject' })
|
|
512
|
+
await client.broadcasts.delete(broadcastId)
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Error handling
|
|
518
|
+
|
|
519
|
+
Every method returns `{ data, error, headers }`. On success `error` is `null`; on failure `data` is `null`.
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
const { data, error } = await client.emails.send({ ... })
|
|
523
|
+
|
|
524
|
+
if (error) {
|
|
525
|
+
console.error(error.name) // 'MONTHLY_LIMIT_EXCEEDED'
|
|
526
|
+
console.error(error.message) // 'Monthly send limit exceeded'
|
|
527
|
+
console.error(error.statusCode) // 429
|
|
528
|
+
} else {
|
|
529
|
+
console.log(data.id)
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### Error codes
|
|
534
|
+
|
|
535
|
+
| Code | Status | Description |
|
|
536
|
+
|------|--------|-------------|
|
|
537
|
+
| `UNAUTHORIZED` | 401 | Invalid or missing API key |
|
|
538
|
+
| `FORBIDDEN` | 403 | Action not allowed on your plan |
|
|
539
|
+
| `NOT_FOUND` | 404 | Resource not found |
|
|
540
|
+
| `VALIDATION_ERROR` | 400 | Invalid request body |
|
|
541
|
+
| `CONFLICT` | 409 | Resource already exists |
|
|
542
|
+
| `RATE_LIMITED` | 429 | Too many requests |
|
|
543
|
+
| `MONTHLY_LIMIT_EXCEEDED` | 429 | Monthly send quota reached |
|
|
544
|
+
| `DAILY_LIMIT_EXCEEDED` | 429 | Daily send quota reached (free plan) |
|
|
545
|
+
| `PLAN_LIMIT_EXCEEDED` | 403 | Feature not available on your plan |
|
|
546
|
+
| `DOMAIN_NOT_VERIFIED` | 403 | The sender domain is not verified for your organisation |
|
|
547
|
+
| `ALL_SUPPRESSED` | 422 | All recipients are on the suppression list |
|
|
548
|
+
| `INTERNAL_ERROR` | 500 | Server error |
|
|
549
|
+
| `application_error` | `null` | Network failure — request never reached the server |
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## TypeScript
|
|
554
|
+
|
|
555
|
+
The SDK is written in TypeScript and ships with full type definitions. All request options, response shapes, and error codes are typed.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import type {
|
|
559
|
+
SendEmailOptions,
|
|
560
|
+
Email,
|
|
561
|
+
EmailStatus,
|
|
562
|
+
EusendError,
|
|
563
|
+
EusendResponse,
|
|
564
|
+
} from '@eusend_dev/sdk'
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Requirements
|
|
570
|
+
|
|
571
|
+
- Node.js 18 or later (uses the native `fetch` API)
|
|
572
|
+
- An Eusend account and API key — [eusend.dev](https://eusend.dev)
|