@sendora/sdk 1.1.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 +364 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +17 -0
- package/dist/base64.js.map +1 -0
- package/dist/broadcasts.d.ts +60 -0
- package/dist/broadcasts.d.ts.map +1 -0
- package/dist/broadcasts.js +98 -0
- package/dist/broadcasts.js.map +1 -0
- package/dist/client.d.ts +51 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +69 -0
- package/dist/client.js.map +1 -0
- package/dist/domains.d.ts +34 -0
- package/dist/domains.d.ts.map +1 -0
- package/dist/domains.js +70 -0
- package/dist/domains.js.map +1 -0
- package/dist/email.d.ts +40 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +68 -0
- package/dist/email.js.map +1 -0
- package/dist/error.d.ts +67 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +173 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +37 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +68 -0
- package/dist/messages.js.map +1 -0
- package/dist/pagination.d.ts +3 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +14 -0
- package/dist/pagination.js.map +1 -0
- package/dist/retry.d.ts +6 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +14 -0
- package/dist/retry.js.map +1 -0
- package/dist/streams.d.ts +45 -0
- package/dist/streams.d.ts.map +1 -0
- package/dist/streams.js +82 -0
- package/dist/streams.js.map +1 -0
- package/dist/suppressions.d.ts +37 -0
- package/dist/suppressions.d.ts.map +1 -0
- package/dist/suppressions.js +56 -0
- package/dist/suppressions.js.map +1 -0
- package/dist/tokens.d.ts +28 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +57 -0
- package/dist/tokens.js.map +1 -0
- package/dist/transport.d.ts +34 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +146 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +802 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/webhook-verify.d.ts +33 -0
- package/dist/webhook-verify.d.ts.map +1 -0
- package/dist/webhook-verify.js +115 -0
- package/dist/webhook-verify.js.map +1 -0
- package/dist/webhooks.d.ts +63 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +118 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +49 -0
- package/skills/sendora/SKILL.md +86 -0
- package/src/base64.ts +17 -0
- package/src/broadcasts.ts +115 -0
- package/src/client.ts +88 -0
- package/src/domains.ts +84 -0
- package/src/email.ts +83 -0
- package/src/error.ts +252 -0
- package/src/index.ts +16 -0
- package/src/messages.ts +88 -0
- package/src/pagination.ts +18 -0
- package/src/retry.ts +20 -0
- package/src/streams.ts +100 -0
- package/src/suppressions.ts +73 -0
- package/src/tokens.ts +70 -0
- package/src/transport.ts +179 -0
- package/src/types.ts +848 -0
- package/src/version.ts +2 -0
- package/src/webhook-verify.ts +147 -0
- package/src/webhooks.ts +157 -0
package/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# @sendora/sdk
|
|
2
|
+
|
|
3
|
+
The official TypeScript SDK for [Sendora](https://sendora.se), transactional
|
|
4
|
+
email delivery hosted in Sweden. Zero dependencies. Runs on Node 20.19 or
|
|
5
|
+
newer and on any runtime with `fetch` and WebCrypto: Bun, Deno, Cloudflare
|
|
6
|
+
Workers. ESM only; CommonJS projects on Node 20.19 or 22.12 and newer load it
|
|
7
|
+
with `require`.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @sendora/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The token is a secret. Create it in the dashboard, keep it on the server,
|
|
14
|
+
and never ship it to a browser. The client throws at construction when the
|
|
15
|
+
token is missing, so build it at startup.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Sendora } from '@sendora/sdk';
|
|
19
|
+
|
|
20
|
+
const sendora = new Sendora({ token: process.env.SENDORA_API_TOKEN });
|
|
21
|
+
|
|
22
|
+
const { messageId } = await sendora.email.send({
|
|
23
|
+
from: { email: 'no-reply@example.se', name: 'Example AB' },
|
|
24
|
+
to: ['anna@example.com'],
|
|
25
|
+
subject: 'Din faktura för september',
|
|
26
|
+
text: 'Hej Anna, fakturan finns bifogad.',
|
|
27
|
+
attachments: [{ name: 'faktura.pdf', content: pdfBytes, contentType: 'application/pdf' }],
|
|
28
|
+
tag: 'invoice',
|
|
29
|
+
metadata: { invoiceId: '2026-0912' },
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`from` must be on a verified sending domain. `to`, `cc` and `bcc` take
|
|
34
|
+
plain addresses or objects with a display name, at most 50 in all; at least
|
|
35
|
+
one of `text` and `html` is required. `streamId` names the stream of the
|
|
36
|
+
server the message goes on; without it the default transactional stream. Attachments take bytes (`Uint8Array`,
|
|
37
|
+
so a Node `Buffer` too) or a base64 string. Every method takes what the
|
|
38
|
+
route takes and answers what the route answers; every call accepts an
|
|
39
|
+
`options.signal` to cancel it.
|
|
40
|
+
|
|
41
|
+
## Sending
|
|
42
|
+
|
|
43
|
+
| Call | Answers |
|
|
44
|
+
| --------------------------------------------- | -------------------------------------------------------------------------- |
|
|
45
|
+
| `sendora.email.send(message, options?)` | `{ messageId, status: 'accepted', submittedAt }` |
|
|
46
|
+
| `sendora.email.sendBatch(messages, options?)` | `{ results }`, one per message in order, `accepted` with its id or `error` |
|
|
47
|
+
|
|
48
|
+
`accepted` means the message is stored and on its way to the mail server;
|
|
49
|
+
delivery, deferral, bounce and complaint arrive later as events on the
|
|
50
|
+
message and as webhooks.
|
|
51
|
+
|
|
52
|
+
`options.idempotencyKey` names the request so a retry cannot send twice.
|
|
53
|
+
The SDK makes a random UUID when you give none; give your own when a retry
|
|
54
|
+
may come from another process. The API remembers a key for 24 hours and
|
|
55
|
+
refuses it with a different body (`idempotency_key_mismatch`).
|
|
56
|
+
|
|
57
|
+
A batch never fails halfway. Each result is `status: 'accepted'` with the
|
|
58
|
+
id, or `status: 'error'` with the same `error` code a single send would
|
|
59
|
+
throw; a retried batch answers the items sent before with `replayed: true`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const { results } = await sendora.email.sendBatch(messages, { idempotencyKey: 'invoices-2026-09' });
|
|
63
|
+
for (const result of results) {
|
|
64
|
+
if (result.status === 'error') {
|
|
65
|
+
console.log(result.index, result.error, result.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Messages
|
|
71
|
+
|
|
72
|
+
| Call | Answers |
|
|
73
|
+
| -------------------------------------- | --------------------------------------------------------------------------- |
|
|
74
|
+
| `sendora.messages.get(messageId)` | The message with its recipients, its attachments described and its timeline |
|
|
75
|
+
| `sendora.messages.search(filters?)` | `{ messages, next }`, one page newest first |
|
|
76
|
+
| `sendora.messages.searchAll(filters?)` | Every match, page by page, for `for await` |
|
|
77
|
+
|
|
78
|
+
Filters: `recipient`, `streamId`, `tag`, `status` (`queued`, `delivered`,
|
|
79
|
+
`deferred`, `bounced`, `expired`), `from` and `to` as `Date` or ISO 8601,
|
|
80
|
+
`limit` (1 to 100), `after`. Bodies are never returned; the log keeps messages for 13
|
|
81
|
+
months.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const message = await sendora.messages.get(messageId);
|
|
85
|
+
const delivered = message.recipients.every((recipient) => recipient.status === 'delivered');
|
|
86
|
+
|
|
87
|
+
for await (const bounced of sendora.messages.searchAll({
|
|
88
|
+
status: 'bounced',
|
|
89
|
+
from: new Date('2026-09-01'),
|
|
90
|
+
})) {
|
|
91
|
+
console.log(bounced.messageId, bounced.subject);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Streams
|
|
96
|
+
|
|
97
|
+
Every message goes on a stream of its server. A server starts with a
|
|
98
|
+
default transactional stream, and the account adds more: transactional
|
|
99
|
+
ones for every account, broadcast ones once Sendora has enabled broadcast
|
|
100
|
+
for the account, which an administrator applies for under Broadcast in
|
|
101
|
+
the dashboard, and one inbound stream, which receives mail at its
|
|
102
|
+
`inboundAddress` instead of sending. Each sending stream has a
|
|
103
|
+
suppression list of its own.
|
|
104
|
+
|
|
105
|
+
| Call | Answers |
|
|
106
|
+
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `sendora.streams.create({ kind, name })` | The stream; a taken name is `stream_exists`, a second inbound stream `inbound_stream_exists`, both with `existingId` |
|
|
108
|
+
| `sendora.streams.list()` | `{ streams }`, archived ones included |
|
|
109
|
+
| `sendora.streams.get(streamId)` | One stream |
|
|
110
|
+
| `sendora.streams.update(streamId, { name, contentRetentionDays })` | The updated stream; the days apply to an inbound stream |
|
|
111
|
+
| `sendora.streams.archive(streamId)` | The stream with `archivedAt`; it takes no new messages from then on |
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const stream = await sendora.streams.create({ kind: 'transactional', name: 'Aviseringar' });
|
|
115
|
+
await sendora.email.send({ ...message, streamId: stream.streamId });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Broadcasts
|
|
119
|
+
|
|
120
|
+
One message to many on a broadcast stream, sent as a whole: the content
|
|
121
|
+
once and one entry per message, up to 50,000 and 50 MB per broadcast.
|
|
122
|
+
Every message is a message in the log with its own events and webhooks,
|
|
123
|
+
carrying the `broadcastId`, and every recipient gets their own
|
|
124
|
+
unsubscribe link where `{{ unsubscribe_url }}` stands. Addresses on the
|
|
125
|
+
stream's suppression list are dropped and counted in `suppressed`. The
|
|
126
|
+
SDK sets an idempotency key, so a retry answers the same broadcast. A
|
|
127
|
+
message's `substitutions`, up to 20 strings, replace `{{ key }}` in the
|
|
128
|
+
subject, the text and the HTML, escaped in the HTML; a key the content
|
|
129
|
+
names that a message lacks throws `substitution_missing`.
|
|
130
|
+
|
|
131
|
+
| Call | Answers |
|
|
132
|
+
| ---------------------------------------------- | ------------------------------------------------------------- |
|
|
133
|
+
| `sendora.broadcasts.send(broadcast, options?)` | `{ broadcastId, total, suppressed }`; refused whole otherwise |
|
|
134
|
+
| `sendora.broadcasts.get(broadcastId)` | The broadcast with `status`, `released` and `failed` |
|
|
135
|
+
| `sendora.broadcasts.list({ limit?, after? })` | `{ broadcasts, next }`, newest first |
|
|
136
|
+
| `sendora.broadcasts.cancel(broadcastId)` | The cancelled broadcast; what had not left counts as failed |
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const { broadcastId } = await sendora.broadcasts.send({
|
|
140
|
+
streamId: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
|
|
141
|
+
from: { email: 'nyheter@example.se', name: 'Example AB' },
|
|
142
|
+
subject: 'Nyheter i oktober',
|
|
143
|
+
text: 'Hej! Här är månadens nyheter. Vill du inte ha fler? {{ unsubscribe_url }}',
|
|
144
|
+
messages: ['anna@example.com', 'bo@example.com'].map((email) => ({ to: [email] })),
|
|
145
|
+
});
|
|
146
|
+
const progress = await sendora.broadcasts.get(broadcastId);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Suppressions
|
|
150
|
+
|
|
151
|
+
Addresses the server no longer sends to: hard bounces, spam complaints and
|
|
152
|
+
entries added by hand. A send to one of them throws `recipient_suppressed`.
|
|
153
|
+
|
|
154
|
+
| Call | Answers |
|
|
155
|
+
| ---------------------------------------------------------- | ----------------------------------------- |
|
|
156
|
+
| `sendora.suppressions.list({ streamId?, limit?, after? })` | `{ suppressions, next }`, newest first |
|
|
157
|
+
| `sendora.suppressions.listAll({ streamId? })` | Every entry, for `for await` |
|
|
158
|
+
| `sendora.suppressions.delete({ address, streamId? })` | Nothing; the address may be sent to again |
|
|
159
|
+
|
|
160
|
+
Every stream has a list of its own; without `streamId` the calls mean the
|
|
161
|
+
default transactional stream. A spam complaint or a recipient's own
|
|
162
|
+
unsubscribe cannot be lifted this way (`spam_complaint_locked`,
|
|
163
|
+
`unsubscribe_locked`); only Sendora support can, at the recipient's own
|
|
164
|
+
request. A refused send carries `streamId` beside `suppressed`.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
await sendora.suppressions.delete({ address: 'anna@example.com' });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Tokens
|
|
171
|
+
|
|
172
|
+
Every token of a server has the same rights; the value is shown once, when
|
|
173
|
+
it is created.
|
|
174
|
+
|
|
175
|
+
| Call | Answers |
|
|
176
|
+
| --------------------------------- | ------------------------------------------------------ |
|
|
177
|
+
| `sendora.tokens.create({ name })` | The token with its `token` value, once |
|
|
178
|
+
| `sendora.tokens.list()` | `{ tokens }`, revoked ones included |
|
|
179
|
+
| `sendora.tokens.get(tokenId)` | One token, without its value |
|
|
180
|
+
| `sendora.tokens.revoke(tokenId)` | Nothing; the last live token is refused (`last_token`) |
|
|
181
|
+
|
|
182
|
+
## Webhooks
|
|
183
|
+
|
|
184
|
+
An https URL of yours that Sendora posts events to as they happen, signed
|
|
185
|
+
with a secret shown once at creation.
|
|
186
|
+
|
|
187
|
+
| Call | Answers |
|
|
188
|
+
| -------------------------------------------------------- | ------------------------------------------------ |
|
|
189
|
+
| `sendora.webhooks.create({ url, events?, streamId? })` | The webhook with its `secret`, once |
|
|
190
|
+
| `sendora.webhooks.list()` | `{ webhooks }` |
|
|
191
|
+
| `sendora.webhooks.get(webhookId)` | One webhook |
|
|
192
|
+
| `sendora.webhooks.delete(webhookId)` | Nothing; pending deliveries are dropped |
|
|
193
|
+
| `sendora.webhooks.deliveries(webhookId, { status? })` | `{ deliveries, next }`, newest first |
|
|
194
|
+
| `sendora.webhooks.deliveriesAll(webhookId, { status? })` | Every delivery, for `for await` |
|
|
195
|
+
| `sendora.webhooks.replay(webhookId, deliveryId)` | The delivery, queued again |
|
|
196
|
+
| `sendora.webhooks.receive(request, secret)` | The verified, typed event from a Fetch `Request` |
|
|
197
|
+
|
|
198
|
+
Events: `delivered`, `bounced`, `deferred`, `spam_complaint` and
|
|
199
|
+
`unsubscribed` about a message, with `messageId`, `streamId`, `recipient`,
|
|
200
|
+
`occurredAt`, `serverId`, your `tag` and `metadata`, and `details` from the
|
|
201
|
+
receiver, or for `unsubscribed` the `source` (`link` or `one_click`);
|
|
202
|
+
`cap_warning` and `cap_reached` about usage, with `scope`, `cap`, `used`
|
|
203
|
+
and the period; `inbound` about a message received on an inbound stream,
|
|
204
|
+
with `inboundMessageId`, `envelopeRecipient`, the sizes, the
|
|
205
|
+
`authentication` verdicts and, for a webhook with `inboundContent:
|
|
206
|
+
'full'`, the message itself: `from`, `to`, `subject`, `text`, `html`,
|
|
207
|
+
`attachments` and the rest. Every event carries `id`, the delivery id,
|
|
208
|
+
and `attempt`.
|
|
209
|
+
|
|
210
|
+
A failed delivery is retried with growing delays for about a day, then it
|
|
211
|
+
is `dead` until you replay it. Deliveries arrive at least once and not
|
|
212
|
+
always in order, so key your handling on `event.id`.
|
|
213
|
+
|
|
214
|
+
### Receiving
|
|
215
|
+
|
|
216
|
+
Verify the signature before you parse anything, answer 2xx quickly, and do
|
|
217
|
+
the work afterwards. With a framework that hands you a Fetch `Request`
|
|
218
|
+
(Next.js route handlers, Hono, SvelteKit, Cloudflare Workers):
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { Sendora } from '@sendora/sdk';
|
|
222
|
+
|
|
223
|
+
const sendora = new Sendora({ token: process.env.SENDORA_API_TOKEN });
|
|
224
|
+
|
|
225
|
+
export async function POST(request: Request): Promise<Response> {
|
|
226
|
+
const event = await sendora.webhooks.receive(request, process.env.SENDORA_WEBHOOK_SECRET);
|
|
227
|
+
if (event.event === 'bounced' && event.details.hard) {
|
|
228
|
+
console.log(
|
|
229
|
+
`${event.recipient} is undeliverable: ${event.details.classification ?? 'unknown'}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return new Response(null, { status: 204 });
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
With Express or Fastify, keep the raw body (`express.raw({ type: '*/*' })`
|
|
237
|
+
or the raw-body plugin) and call the verifier directly:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { verifyWebhook } from '@sendora/sdk';
|
|
241
|
+
|
|
242
|
+
const event = await verifyWebhook({
|
|
243
|
+
secret: process.env.SENDORA_WEBHOOK_SECRET,
|
|
244
|
+
signature: req.headers['sendora-signature'],
|
|
245
|
+
body: req.body,
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The signature is `Sendora-Signature: t=<unix seconds>,v1=<hex>`, an
|
|
250
|
+
HMAC-SHA256 with your secret over `<t>.<raw body>`, compared in constant
|
|
251
|
+
time through WebCrypto. A signature older than five minutes is refused
|
|
252
|
+
(`stale_signature`; `toleranceSeconds` changes the limit), a wrong one
|
|
253
|
+
throws `invalid_signature`.
|
|
254
|
+
|
|
255
|
+
## Domains
|
|
256
|
+
|
|
257
|
+
Sending domains belong to the account, and every server sends from them.
|
|
258
|
+
A domain sends once both DNS records are seen.
|
|
259
|
+
|
|
260
|
+
| Call | Answers |
|
|
261
|
+
| ------------------------------------ | ---------------------------------------------------------------------------- |
|
|
262
|
+
| `sendora.domains.create({ domain })` | The domain with `returnPath` (CNAME) and `dkim` (TXT) to add |
|
|
263
|
+
| `sendora.domains.list()` | `{ domains }` |
|
|
264
|
+
| `sendora.domains.get(domainId)` | One domain with the state of its records |
|
|
265
|
+
| `sendora.domains.verify(domainId)` | The domain plus `check` per record: `ok`, `missing`, `mismatch`, `dns_error` |
|
|
266
|
+
| `sendora.domains.delete(domainId)` | Nothing; mail from it is refused from then on |
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const domain = await sendora.domains.create({ domain: 'example.se' });
|
|
270
|
+
console.log(`${domain.returnPath.type} ${domain.returnPath.host} -> ${domain.returnPath.value}`);
|
|
271
|
+
console.log(`${domain.dkim.type} ${domain.dkim.host} -> ${domain.dkim.value}`);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Errors
|
|
275
|
+
|
|
276
|
+
Every failed call throws `SendoraError`. Narrow on `code`:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { SendoraError } from '@sendora/sdk';
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await sendora.email.send(message);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error instanceof SendoraError) {
|
|
285
|
+
switch (error.code) {
|
|
286
|
+
case 'recipient_suppressed':
|
|
287
|
+
// error.suppressed lists the addresses and why
|
|
288
|
+
break;
|
|
289
|
+
case 'rate_limited':
|
|
290
|
+
// error.retryAfter seconds; error.scope and error.limit say whose limit
|
|
291
|
+
break;
|
|
292
|
+
case 'invalid_request':
|
|
293
|
+
// error.issues, one { path, message } per field
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
| Code | Status | Meaning |
|
|
302
|
+
| --------------------------------- | ------ | ------------------------------------------------------------------------- |
|
|
303
|
+
| `invalid_request` | 400 | A field is wrong; `issues` says which. |
|
|
304
|
+
| `idempotency_key_required` | 400 | A batch was sent without a key. |
|
|
305
|
+
| `unauthorized` | 401 | The token is missing, malformed or revoked. |
|
|
306
|
+
| `payment_required` | 402 | The account has no active subscription. |
|
|
307
|
+
| `tenant_paused` | 403 | Sendora has paused the account. |
|
|
308
|
+
| `tenant_not_active` | 403 | The account is not approved to send, or not active for an inbound stream. |
|
|
309
|
+
| `spam_complaint_locked` | 403 | Only support can lift a spam-complaint suppression. |
|
|
310
|
+
| `unsubscribe_locked` | 403 | Only support can lift a recipient's own unsubscribe. |
|
|
311
|
+
| `not_found` | 404 | No such id on this server, or no such suppressed address. |
|
|
312
|
+
| `domain_exists` | 409 | The account already has the domain; `existingId` names it. |
|
|
313
|
+
| `webhook_exists` | 409 | The server already has a webhook for that URL; `existingId` names it. |
|
|
314
|
+
| `stream_exists` | 409 | The server already has a stream with that name; `existingId` names it. |
|
|
315
|
+
| `inbound_stream_exists` | 409 | The server already has a live inbound stream; `existingId` names it. |
|
|
316
|
+
| `default_stream` | 409 | The default stream cannot be archived. |
|
|
317
|
+
| `last_token` | 409 | The only live token cannot be revoked. |
|
|
318
|
+
| `request_too_large` | 413 | The body exceeds 10 MB. |
|
|
319
|
+
| `from_domain_not_verified` | 422 | The From domain is not a verified sending domain. |
|
|
320
|
+
| `stream_not_found` | 422 | `streamId` names no stream of this server. |
|
|
321
|
+
| `stream_archived` | 422 | The stream is archived and takes no new messages. |
|
|
322
|
+
| `stream_paused` | 422 | Sendora paused the stream after complaints; support resumes it. |
|
|
323
|
+
| `stream_not_sendable` | 422 | An inbound stream receives mail; it takes no messages and has no list. |
|
|
324
|
+
| `stream_not_broadcast` | 422 | A broadcast goes on a broadcast stream; this one is transactional. |
|
|
325
|
+
| `substitution_missing` | 422 | The content names a `{{ key }}` a message does not give. |
|
|
326
|
+
| `broadcast_not_open` | 409 | The broadcast is completed or cancelled; nothing is left to cancel. |
|
|
327
|
+
| `broadcast_not_enabled` | 422 | Broadcast is not enabled for the account: no broadcast stream, no send. |
|
|
328
|
+
| `unsubscribe_placeholder_missing` | 422 | A broadcast message lacks `{{ unsubscribe_url }}` in a part. |
|
|
329
|
+
| `list_unsubscribe_reserved` | 422 | Sendora writes List-Unsubscribe on broadcast streams; leave it out. |
|
|
330
|
+
| `recipient_suppressed` | 422 | Recipients on the suppression list; `suppressed` lists them. |
|
|
331
|
+
| `idempotency_key_mismatch` | 422 | The key was used before with a different body. |
|
|
332
|
+
| `rate_limited` | 429 | Per-minute limit; `retryAfter`, `scope`, `limit`. |
|
|
333
|
+
| `monthly_cap_reached` | 429 | The monthly cap is used up; `scope`, `cap`, `used`, `resetsAt`. |
|
|
334
|
+
| `sending_disabled` | 503 | Sending is paused for everyone; `retryAfter`. |
|
|
335
|
+
| `connection_failed` | none | The request never reached the API; `cause` holds the failure. |
|
|
336
|
+
| `timeout` | none | No answer within `timeoutMs`. |
|
|
337
|
+
| `unexpected_response` | any | An answer without a JSON error body, such as from a proxy. |
|
|
338
|
+
| `invalid_signature` | none | A webhook's signature is missing, malformed or wrong. |
|
|
339
|
+
| `stale_signature` | none | A webhook's signature is older than the tolerance. |
|
|
340
|
+
|
|
341
|
+
`error.retryable` is true when waiting and calling again could succeed.
|
|
342
|
+
Neither the token nor a request body is ever part of an error.
|
|
343
|
+
|
|
344
|
+
## Retries
|
|
345
|
+
|
|
346
|
+
Lost connections, timeouts, `rate_limited`, `sending_disabled` and server
|
|
347
|
+
errors are retried with jittered backoff, twice by default, honouring
|
|
348
|
+
`retryAfter` when it fits within five seconds of waiting in all. A longer
|
|
349
|
+
`retryAfter` is thrown at once for you to schedule. A request that creates
|
|
350
|
+
something (`tokens.create`, `webhooks.create`, `domains.create`, `replay`)
|
|
351
|
+
is repeated only after a `rate_limited` or `sending_disabled` answer, never
|
|
352
|
+
after a lost connection, so nothing is created twice. Nothing else is
|
|
353
|
+
retried. `new Sendora({ maxRetries: 0 })` turns retries off; `timeoutMs`
|
|
354
|
+
sets the limit per attempt.
|
|
355
|
+
|
|
356
|
+
## For coding agents
|
|
357
|
+
|
|
358
|
+
The package ships `skills/sendora/SKILL.md`, a short guide in the agent
|
|
359
|
+
skill format with the rules above; point your agent at it or at this
|
|
360
|
+
README. Every exported symbol carries JSDoc with an example.
|
|
361
|
+
|
|
362
|
+
## Docs
|
|
363
|
+
|
|
364
|
+
Guides and the HTTP reference: [sendora.se/docs](https://sendora.se/docs).
|
package/dist/base64.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base64.d.ts","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAEA,gHAAgH;AAChH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAalD"}
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
2
|
+
/** Standard base64 without line breaks, as the API takes attachment content; needs nothing from the runtime. */
|
|
3
|
+
export function toBase64(bytes) {
|
|
4
|
+
let out = '';
|
|
5
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
6
|
+
const a = bytes[i] ?? 0;
|
|
7
|
+
const b = bytes[i + 1];
|
|
8
|
+
const c = bytes[i + 2];
|
|
9
|
+
const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0);
|
|
10
|
+
out += ALPHABET.charAt((n >>> 18) & 63);
|
|
11
|
+
out += ALPHABET.charAt((n >>> 12) & 63);
|
|
12
|
+
out += b === undefined ? '=' : ALPHABET.charAt((n >>> 6) & 63);
|
|
13
|
+
out += c === undefined ? '=' : ALPHABET.charAt(n & 63);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=base64.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base64.js","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAAA,MAAM,QAAQ,GAAG,kEAAkE,CAAC;AAEpF,gHAAgH;AAChH,MAAM,UAAU,QAAQ,CAAC,KAAiB;IACxC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QACxC,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QACxC,GAAG,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/D,GAAG,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Transport } from './transport.ts';
|
|
2
|
+
import type { AcceptedBroadcast, Broadcast, BroadcastList, BroadcastPage, RequestOptions, SendBroadcastBody, SendBroadcastRequest, SendOptions } from './types.ts';
|
|
3
|
+
/** One message to many, on a broadcast stream of the token's server: sent as a whole, followed as a whole. */
|
|
4
|
+
export declare class BroadcastsResource {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(transport: Transport);
|
|
7
|
+
/**
|
|
8
|
+
* Stores the content once and one message per entry of `messages`, up to
|
|
9
|
+
* 50,000, on a broadcast stream. Every message is a message in the log
|
|
10
|
+
* with its own events and webhooks, sent with the recipient's own
|
|
11
|
+
* unsubscribe link where `{{ unsubscribe_url }}` stands. Addresses on the
|
|
12
|
+
* stream's suppression list are dropped and counted in `suppressed`; a
|
|
13
|
+
* list with nothing left throws `recipient_suppressed`. A larger list is
|
|
14
|
+
* several broadcasts under one tag. The SDK sets an idempotency key, so a
|
|
15
|
+
* retry answers the same broadcast; pass your own to make a retry from
|
|
16
|
+
* your side safe too.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const { broadcastId, total } = await sendora.broadcasts.send({
|
|
20
|
+
* streamId,
|
|
21
|
+
* from: { email: 'nyheter@example.se', name: 'Example AB' },
|
|
22
|
+
* subject: 'Nyheter i oktober',
|
|
23
|
+
* text: 'Hej!\n\nHär är månadens nyheter.\n\nVill du inte ha fler? {{ unsubscribe_url }}',
|
|
24
|
+
* messages: recipients.map((email) => ({ to: [email] })),
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
send(request: SendBroadcastRequest, options?: SendOptions): Promise<AcceptedBroadcast>;
|
|
28
|
+
/**
|
|
29
|
+
* The broadcast and how far it has come: `released` and `failed` move
|
|
30
|
+
* as the worker hands its messages over, and `status` is `completed`
|
|
31
|
+
* once nothing is left.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const broadcast = await sendora.broadcasts.get(broadcastId);
|
|
35
|
+
* console.log(`${broadcast.released} of ${broadcast.total} released`);
|
|
36
|
+
*/
|
|
37
|
+
get(broadcastId: string, options?: RequestOptions): Promise<Broadcast>;
|
|
38
|
+
/**
|
|
39
|
+
* One page of the server's broadcasts, newest first; pass the page's
|
|
40
|
+
* `next` as `after` for the following one.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const { broadcasts } = await sendora.broadcasts.list({ limit: 20 });
|
|
44
|
+
*/
|
|
45
|
+
list(query?: BroadcastList, options?: RequestOptions): Promise<BroadcastPage>;
|
|
46
|
+
/**
|
|
47
|
+
* Holds every message of the broadcast that has not been handed to the
|
|
48
|
+
* mail server yet; those count as `failed`. A message already on its way
|
|
49
|
+
* is delivered. A completed or cancelled broadcast throws
|
|
50
|
+
* `broadcast_not_open`.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const cancelled = await sendora.broadcasts.cancel(broadcastId);
|
|
54
|
+
* console.log(`${cancelled.failed} messages held back`);
|
|
55
|
+
*/
|
|
56
|
+
cancel(broadcastId: string, options?: RequestOptions): Promise<Broadcast>;
|
|
57
|
+
}
|
|
58
|
+
/** The body as the API takes it: attachment bytes become base64, everything else is passed through. */
|
|
59
|
+
export declare function encodeBroadcastRequest(request: SendBroadcastRequest): SendBroadcastBody;
|
|
60
|
+
//# sourceMappingURL=broadcasts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broadcasts.d.ts","sourceRoot":"","sources":["../src/broadcasts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,8GAA8G;AAC9G,qBAAa,kBAAkB;;gBAGjB,SAAS,EAAE,SAAS;IAIhC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAW1F;;;;;;;;OAQG;IACH,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,SAAS,CAAC;IAS1E;;;;;;OAMG;IACH,IAAI,CAAC,KAAK,GAAE,aAAkB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC;IAUrF;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,SAAS,CAAC;CAQ9E;AAED,uGAAuG;AACvG,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,iBAAiB,CAMvF"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { encodeAttachment } from "./email.js";
|
|
2
|
+
/** One message to many, on a broadcast stream of the token's server: sent as a whole, followed as a whole. */
|
|
3
|
+
export class BroadcastsResource {
|
|
4
|
+
#transport;
|
|
5
|
+
constructor(transport) {
|
|
6
|
+
this.#transport = transport;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Stores the content once and one message per entry of `messages`, up to
|
|
10
|
+
* 50,000, on a broadcast stream. Every message is a message in the log
|
|
11
|
+
* with its own events and webhooks, sent with the recipient's own
|
|
12
|
+
* unsubscribe link where `{{ unsubscribe_url }}` stands. Addresses on the
|
|
13
|
+
* stream's suppression list are dropped and counted in `suppressed`; a
|
|
14
|
+
* list with nothing left throws `recipient_suppressed`. A larger list is
|
|
15
|
+
* several broadcasts under one tag. The SDK sets an idempotency key, so a
|
|
16
|
+
* retry answers the same broadcast; pass your own to make a retry from
|
|
17
|
+
* your side safe too.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { broadcastId, total } = await sendora.broadcasts.send({
|
|
21
|
+
* streamId,
|
|
22
|
+
* from: { email: 'nyheter@example.se', name: 'Example AB' },
|
|
23
|
+
* subject: 'Nyheter i oktober',
|
|
24
|
+
* text: 'Hej!\n\nHär är månadens nyheter.\n\nVill du inte ha fler? {{ unsubscribe_url }}',
|
|
25
|
+
* messages: recipients.map((email) => ({ to: [email] })),
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
send(request, options = {}) {
|
|
29
|
+
return this.#transport.request({
|
|
30
|
+
method: 'POST',
|
|
31
|
+
path: '/v1/broadcasts',
|
|
32
|
+
body: encodeBroadcastRequest(request),
|
|
33
|
+
headers: { 'idempotency-key': options.idempotencyKey ?? crypto.randomUUID() },
|
|
34
|
+
idempotent: true,
|
|
35
|
+
signal: options.signal,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The broadcast and how far it has come: `released` and `failed` move
|
|
40
|
+
* as the worker hands its messages over, and `status` is `completed`
|
|
41
|
+
* once nothing is left.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const broadcast = await sendora.broadcasts.get(broadcastId);
|
|
45
|
+
* console.log(`${broadcast.released} of ${broadcast.total} released`);
|
|
46
|
+
*/
|
|
47
|
+
get(broadcastId, options = {}) {
|
|
48
|
+
return this.#transport.request({
|
|
49
|
+
method: 'GET',
|
|
50
|
+
path: `/v1/broadcasts/${encodeURIComponent(broadcastId)}`,
|
|
51
|
+
idempotent: true,
|
|
52
|
+
signal: options.signal,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One page of the server's broadcasts, newest first; pass the page's
|
|
57
|
+
* `next` as `after` for the following one.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const { broadcasts } = await sendora.broadcasts.list({ limit: 20 });
|
|
61
|
+
*/
|
|
62
|
+
list(query = {}, options = {}) {
|
|
63
|
+
return this.#transport.request({
|
|
64
|
+
method: 'GET',
|
|
65
|
+
path: '/v1/broadcasts',
|
|
66
|
+
query: { limit: query.limit, after: query.after },
|
|
67
|
+
idempotent: true,
|
|
68
|
+
signal: options.signal,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Holds every message of the broadcast that has not been handed to the
|
|
73
|
+
* mail server yet; those count as `failed`. A message already on its way
|
|
74
|
+
* is delivered. A completed or cancelled broadcast throws
|
|
75
|
+
* `broadcast_not_open`.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const cancelled = await sendora.broadcasts.cancel(broadcastId);
|
|
79
|
+
* console.log(`${cancelled.failed} messages held back`);
|
|
80
|
+
*/
|
|
81
|
+
cancel(broadcastId, options = {}) {
|
|
82
|
+
return this.#transport.request({
|
|
83
|
+
method: 'POST',
|
|
84
|
+
path: `/v1/broadcasts/${encodeURIComponent(broadcastId)}/cancel`,
|
|
85
|
+
idempotent: true,
|
|
86
|
+
signal: options.signal,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The body as the API takes it: attachment bytes become base64, everything else is passed through. */
|
|
91
|
+
export function encodeBroadcastRequest(request) {
|
|
92
|
+
const { attachments, ...fields } = request;
|
|
93
|
+
if (attachments === undefined) {
|
|
94
|
+
return fields;
|
|
95
|
+
}
|
|
96
|
+
return { ...fields, attachments: attachments.map(encodeAttachment) };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=broadcasts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broadcasts.js","sourceRoot":"","sources":["../src/broadcasts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAa9C,8GAA8G;AAC9G,MAAM,OAAO,kBAAkB;IACpB,UAAU,CAAY;IAE/B,YAAY,SAAoB;QAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,OAA6B,EAAE,UAAuB,EAAE;QAC3D,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAoB;YAChD,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,gBAAgB;YACtB,IAAI,EAAE,sBAAsB,CAAC,OAAO,CAAC;YACrC,OAAO,EAAE,EAAE,iBAAiB,EAAE,OAAO,CAAC,cAAc,IAAI,MAAM,CAAC,UAAU,EAAE,EAAE;YAC7E,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;OAQG;IACH,GAAG,CAAC,WAAmB,EAAE,UAA0B,EAAE;QACnD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAY;YACxC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,kBAAkB,kBAAkB,CAAC,WAAW,CAAC,EAAE;YACzD,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,IAAI,CAAC,QAAuB,EAAE,EAAE,UAA0B,EAAE;QAC1D,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAgB;YAC5C,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,gBAAgB;YACtB,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE;YACjD,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAmB,EAAE,UAA0B,EAAE;QACtD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAY;YACxC,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,kBAAkB,kBAAkB,CAAC,WAAW,CAAC,SAAS;YAChE,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;IACL,CAAC;CACF;AAED,uGAAuG;AACvG,MAAM,UAAU,sBAAsB,CAAC,OAA6B;IAClE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3C,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,GAAG,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;AACvE,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DomainsResource } from './domains.ts';
|
|
2
|
+
import { BroadcastsResource } from './broadcasts.ts';
|
|
3
|
+
import { EmailResource } from './email.ts';
|
|
4
|
+
import { MessagesResource } from './messages.ts';
|
|
5
|
+
import { StreamsResource } from './streams.ts';
|
|
6
|
+
import { SuppressionsResource } from './suppressions.ts';
|
|
7
|
+
import { TokensResource } from './tokens.ts';
|
|
8
|
+
import { WebhooksResource } from './webhooks.ts';
|
|
9
|
+
export declare const DEFAULT_BASE_URL = "https://api.sendora.se";
|
|
10
|
+
export interface SendoraOptions {
|
|
11
|
+
/**
|
|
12
|
+
* A server's API token, created in the dashboard. Keep it on the server;
|
|
13
|
+
* never ship it to a browser. `undefined`, as an unset environment
|
|
14
|
+
* variable gives, throws at construction rather than at the first call.
|
|
15
|
+
*/
|
|
16
|
+
token: string | undefined;
|
|
17
|
+
/** The API's origin; https://api.sendora.se unless you test against another. */
|
|
18
|
+
baseUrl?: string | undefined;
|
|
19
|
+
/** The fetch to use; the global one unless you need a proxy or a fake. */
|
|
20
|
+
fetch?: typeof fetch | undefined;
|
|
21
|
+
/** How long one attempt may take; 30 seconds by default. */
|
|
22
|
+
timeoutMs?: number | undefined;
|
|
23
|
+
/** How many times a failed call is repeated when repeating is safe; 2 by default, 0 turns retries off. */
|
|
24
|
+
maxRetries?: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The client. One instance per server token; every resource hangs off it.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* import { Sendora } from '@sendora/sdk';
|
|
31
|
+
*
|
|
32
|
+
* const sendora = new Sendora({ token: process.env.SENDORA_API_TOKEN });
|
|
33
|
+
* const { messageId } = await sendora.email.send({
|
|
34
|
+
* from: 'no-reply@example.se',
|
|
35
|
+
* to: ['anna@example.com'],
|
|
36
|
+
* subject: 'Välkommen',
|
|
37
|
+
* text: 'Hej Anna!',
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export declare class Sendora {
|
|
41
|
+
readonly email: EmailResource;
|
|
42
|
+
readonly broadcasts: BroadcastsResource;
|
|
43
|
+
readonly messages: MessagesResource;
|
|
44
|
+
readonly streams: StreamsResource;
|
|
45
|
+
readonly suppressions: SuppressionsResource;
|
|
46
|
+
readonly tokens: TokensResource;
|
|
47
|
+
readonly webhooks: WebhooksResource;
|
|
48
|
+
readonly domains: DomainsResource;
|
|
49
|
+
constructor(options: SendoraOptions);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AAGzD,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAC;IACjC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,0GAA0G;IAC1G,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,OAAO;IAClB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;gBAEtB,OAAO,EAAE,cAAc;CA+BpC"}
|