shipmail 0.1.19 → 0.1.21
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 +355 -106
- package/dist/index.cjs +141 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -4
- package/dist/index.d.ts +223 -4
- package/dist/index.js +141 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,219 +1,468 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# Shipmail TypeScript SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/shipmail)
|
|
4
|
+
[](https://www.npmjs.com/package/shipmail)
|
|
5
|
+
[](https://bundlephobia.com/package/shipmail)
|
|
6
|
+
[](https://www.npmjs.com/package/shipmail)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
Official TypeScript SDK for the [Shipmail](https://shipmail.to) API. Zero runtime dependencies. Native `fetch`. Full TypeScript types. ESM and CommonJS.
|
|
10
|
+
|
|
11
|
+
**Runtimes**: Node.js 18+, Bun, Deno. Webhook verification uses `node:crypto` (enable `nodejs_compat` on Cloudflare Workers if you verify webhooks there).
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Configuration](#configuration)
|
|
18
|
+
- [Domains](#domains)
|
|
19
|
+
- [Mailboxes](#mailboxes)
|
|
20
|
+
- [Messages](#messages)
|
|
21
|
+
- [Threads](#threads)
|
|
22
|
+
- [Webhooks](#webhooks)
|
|
23
|
+
- [Suppressions](#suppressions)
|
|
24
|
+
- [Status](#status)
|
|
25
|
+
- [Pagination](#pagination)
|
|
26
|
+
- [Webhook verification](#webhook-verification)
|
|
27
|
+
- [Per-request options](#per-request-options)
|
|
28
|
+
- [Idempotency](#idempotency)
|
|
29
|
+
- [Cancellation](#cancellation)
|
|
30
|
+
- [Custom fetch and proxies](#custom-fetch-and-proxies)
|
|
31
|
+
- [Errors](#errors)
|
|
32
|
+
- [Retries](#retries)
|
|
33
|
+
- [Bundling](#bundling)
|
|
34
|
+
- [Testing](#testing)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
- [Links](#links)
|
|
37
|
+
|
|
38
|
+
## Install
|
|
6
39
|
|
|
7
40
|
```bash
|
|
41
|
+
bun add shipmail
|
|
42
|
+
# or
|
|
8
43
|
npm install shipmail
|
|
9
44
|
# or
|
|
10
|
-
|
|
45
|
+
pnpm add shipmail
|
|
11
46
|
```
|
|
12
47
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Quick Start
|
|
48
|
+
## Quick start
|
|
16
49
|
|
|
17
|
-
```
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
const client = new ShipMail("sm_live_...");
|
|
50
|
+
```ts
|
|
51
|
+
import { ShipMailClient } from "shipmail";
|
|
21
52
|
|
|
22
|
-
|
|
23
|
-
const domain = await client.domains.create({ name: "example.com" });
|
|
53
|
+
const shipmail = new ShipMailClient({ apiKey: process.env.SHIPMAIL_API_KEY! });
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
const message = await client.messages.send({
|
|
55
|
+
const message = await shipmail.messages.send({
|
|
27
56
|
mailbox_id: "mbx_...",
|
|
28
|
-
to: [{ address: "user@example.com" }],
|
|
57
|
+
to: [{ address: "user@example.com", name: "User" }],
|
|
29
58
|
subject: "Hello",
|
|
30
|
-
|
|
59
|
+
text: "Hi there",
|
|
60
|
+
html: "<p>Hi there</p>",
|
|
31
61
|
});
|
|
32
62
|
```
|
|
33
63
|
|
|
34
|
-
|
|
64
|
+
The SDK does not auto-read environment variables. Pass the key explicitly.
|
|
35
65
|
|
|
36
|
-
|
|
37
|
-
import { ShipMailClient } from "shipmail";
|
|
66
|
+
You can also pass a key string directly:
|
|
38
67
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
baseUrl: "https://shipmail.to/api/v1", // default
|
|
42
|
-
maxRetries: 2, // default, retries on 5xx and 429
|
|
43
|
-
timeout: 30_000, // default, in milliseconds
|
|
44
|
-
});
|
|
68
|
+
```ts
|
|
69
|
+
const shipmail = new ShipMailClient("sm_live_...");
|
|
45
70
|
```
|
|
46
71
|
|
|
47
|
-
##
|
|
72
|
+
## Configuration
|
|
48
73
|
|
|
49
|
-
|
|
74
|
+
```ts
|
|
75
|
+
const shipmail = new ShipMailClient({
|
|
76
|
+
apiKey: process.env.SHIPMAIL_API_KEY!,
|
|
77
|
+
baseUrl: "https://shipmail.to/api/v1",
|
|
78
|
+
maxRetries: 2,
|
|
79
|
+
timeout: 30_000,
|
|
80
|
+
fetch: customFetch,
|
|
81
|
+
defaultHeaders: { "x-app-name": "my-app" },
|
|
82
|
+
});
|
|
83
|
+
```
|
|
50
84
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
| Option | Type | Default | Description |
|
|
86
|
+
| ---------------- | ------------------------ | ---------------------------- | --------------------------------------------------------------- |
|
|
87
|
+
| `apiKey` | `string` | required | Shipmail API key (`sm_live_...`). |
|
|
88
|
+
| `baseUrl` | `string` | `https://shipmail.to/api/v1` | API base URL. |
|
|
89
|
+
| `maxRetries` | `number` | `2` | Retry count on 5xx and 429. Total attempts is `maxRetries + 1`. |
|
|
90
|
+
| `timeout` | `number` | `30_000` | Per-request timeout in ms. |
|
|
91
|
+
| `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation. |
|
|
92
|
+
| `defaultHeaders` | `Record<string, string>` | `{}` | Headers added to every request. |
|
|
93
|
+
|
|
94
|
+
## Domains
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await shipmail.domains.create({ name: "example.com" });
|
|
98
|
+
await shipmail.domains.list({ limit: 10 });
|
|
99
|
+
await shipmail.domains.get("dom_...");
|
|
100
|
+
await shipmail.domains.update("dom_...", { catch_all_mailbox_id: "mbx_..." });
|
|
101
|
+
await shipmail.domains.delete("dom_...");
|
|
102
|
+
await shipmail.domains.verify("dom_...");
|
|
103
|
+
await shipmail.domains.search({ keyword: "example" });
|
|
104
|
+
await shipmail.domains.register({
|
|
105
|
+
name: "example.com",
|
|
106
|
+
years: 1,
|
|
107
|
+
contact: {
|
|
108
|
+
first_name: "Jane",
|
|
109
|
+
last_name: "Doe",
|
|
110
|
+
address1: "1 Main St",
|
|
111
|
+
/* ... */
|
|
112
|
+
},
|
|
113
|
+
});
|
|
58
114
|
```
|
|
59
115
|
|
|
60
|
-
|
|
116
|
+
## Mailboxes
|
|
61
117
|
|
|
62
|
-
```
|
|
63
|
-
|
|
118
|
+
```ts
|
|
119
|
+
await shipmail.mailboxes.create({
|
|
64
120
|
domain_id: "dom_...",
|
|
65
121
|
address: "hello",
|
|
66
122
|
display_name: "Hello",
|
|
67
123
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await
|
|
124
|
+
await shipmail.mailboxes.list({ domain_id: "dom_..." });
|
|
125
|
+
await shipmail.mailboxes.get("mbx_...");
|
|
126
|
+
await shipmail.mailboxes.update("mbx_...", { display_name: "New Name" });
|
|
127
|
+
await shipmail.mailboxes.resetPassword("mbx_...", { password: "NewPassword1" });
|
|
128
|
+
const folders = await shipmail.mailboxes.listFolders("mbx_...");
|
|
129
|
+
const folder = await shipmail.mailboxes.createFolder("mbx_...", { name: "VIP" });
|
|
130
|
+
await shipmail.mailboxes.updateFolder("mbx_...", folder.id, { name: "VIP Clients" });
|
|
131
|
+
await shipmail.mailboxes.deleteFolder("mbx_...", folder.id);
|
|
132
|
+
const identities = await shipmail.mailboxes.listIdentities("mbx_...");
|
|
133
|
+
const rules = await shipmail.mailboxes.getRules("mbx_...");
|
|
134
|
+
await shipmail.mailboxes.updateRules("mbx_...", { rules: rules.rules });
|
|
135
|
+
await shipmail.mailboxes.updateSpamFilter("mbx_...", { threshold: 8 });
|
|
136
|
+
await shipmail.mailboxes.delete("mbx_...");
|
|
137
|
+
|
|
138
|
+
await shipmail.mailboxes.updateAutoReply("mbx_...", {
|
|
139
|
+
enabled: true,
|
|
140
|
+
subject: "Out of office",
|
|
141
|
+
body: "Back on Monday.",
|
|
142
|
+
from_date: "2026-06-01",
|
|
143
|
+
to_date: "2026-06-07",
|
|
144
|
+
});
|
|
72
145
|
```
|
|
73
146
|
|
|
74
|
-
|
|
147
|
+
## Messages
|
|
75
148
|
|
|
76
|
-
```
|
|
77
|
-
|
|
149
|
+
```ts
|
|
150
|
+
await shipmail.messages.send({
|
|
78
151
|
mailbox_id: "mbx_...",
|
|
79
|
-
to: [{ address: "user@example.com"
|
|
152
|
+
to: [{ address: "user@example.com" }],
|
|
80
153
|
cc: [{ address: "cc@example.com" }],
|
|
81
154
|
subject: "Hello",
|
|
82
|
-
|
|
83
|
-
|
|
155
|
+
text: "Hi there",
|
|
156
|
+
html: "<p>Hi there</p>",
|
|
84
157
|
});
|
|
85
158
|
|
|
86
|
-
|
|
159
|
+
await shipmail.messages.list({ mailbox_id: "mbx_...", limit: 25 });
|
|
160
|
+
await shipmail.messages.get("msg_...");
|
|
161
|
+
|
|
162
|
+
await shipmail.messages.reply("msg_...", {
|
|
163
|
+
to: [{ address: "user@example.com" }],
|
|
164
|
+
text: "Thanks for your email.",
|
|
165
|
+
});
|
|
87
166
|
```
|
|
88
167
|
|
|
89
|
-
|
|
168
|
+
## Threads
|
|
90
169
|
|
|
91
|
-
```
|
|
92
|
-
const threads = await
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
170
|
+
```ts
|
|
171
|
+
const threads = await shipmail.threads.list({ mailbox_id: "mbx_..." });
|
|
172
|
+
const threadId = threads.data[0].id;
|
|
173
|
+
const thread = await shipmail.threads.get(threadId);
|
|
174
|
+
|
|
175
|
+
await shipmail.threads.reply(threadId, {
|
|
176
|
+
text: "Thanks for your email.",
|
|
97
177
|
});
|
|
98
178
|
```
|
|
99
179
|
|
|
100
|
-
|
|
180
|
+
## Webhooks
|
|
101
181
|
|
|
102
|
-
```
|
|
103
|
-
const webhook = await
|
|
182
|
+
```ts
|
|
183
|
+
const webhook = await shipmail.webhooks.create({
|
|
104
184
|
url: "https://example.com/webhook",
|
|
105
185
|
events: ["message.received", "message.sent"],
|
|
106
|
-
description: "
|
|
186
|
+
description: "Incoming email handler",
|
|
107
187
|
});
|
|
108
|
-
// webhook.secret is only
|
|
188
|
+
// webhook.secret is returned only on creation. Store it now.
|
|
189
|
+
|
|
190
|
+
await shipmail.webhooks.list();
|
|
191
|
+
await shipmail.webhooks.get("whk_...");
|
|
192
|
+
await shipmail.webhooks.update("whk_...", { active: false });
|
|
193
|
+
await shipmail.webhooks.delete("whk_...");
|
|
194
|
+
|
|
195
|
+
await shipmail.webhooks.rotateSecret("whk_...");
|
|
196
|
+
await shipmail.webhooks.test("whk_...");
|
|
197
|
+
await shipmail.webhooks.listDeliveries("whk_...");
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Supported event types:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
message.received
|
|
204
|
+
message.sent
|
|
205
|
+
message.delivered
|
|
206
|
+
message.bounced
|
|
207
|
+
message.complained
|
|
208
|
+
domain.verified
|
|
209
|
+
domain.verification_failed
|
|
210
|
+
domain.degraded
|
|
211
|
+
org.reputation_warning
|
|
212
|
+
org.sending_throttled
|
|
213
|
+
org.sending_suspended
|
|
214
|
+
org.reputation_recovered
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Suppressions
|
|
109
218
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
219
|
+
```ts
|
|
220
|
+
await shipmail.suppressions.list({ limit: 25 });
|
|
221
|
+
await shipmail.suppressions.remove("user@example.com");
|
|
222
|
+
```
|
|
114
223
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
224
|
+
Auto-paginate:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
for await (const item of shipmail.suppressions.listAutoPaginating({ limit: 100 })) {
|
|
228
|
+
console.log(item.email_address, item.reason);
|
|
229
|
+
}
|
|
118
230
|
```
|
|
119
231
|
|
|
120
|
-
|
|
232
|
+
## Status
|
|
121
233
|
|
|
122
|
-
```
|
|
123
|
-
const status = await
|
|
234
|
+
```ts
|
|
235
|
+
const status = await shipmail.status.get();
|
|
124
236
|
```
|
|
125
237
|
|
|
126
238
|
## Pagination
|
|
127
239
|
|
|
128
|
-
List methods return
|
|
240
|
+
List methods return `{ data, pagination }` with cursor-based pagination:
|
|
129
241
|
|
|
130
|
-
```
|
|
131
|
-
const page = await
|
|
132
|
-
|
|
133
|
-
|
|
242
|
+
```ts
|
|
243
|
+
const page = await shipmail.domains.list({ limit: 10 });
|
|
244
|
+
page.data; // Domain[]
|
|
245
|
+
page.pagination; // { next_cursor, has_more }
|
|
134
246
|
|
|
135
|
-
// Fetch next page
|
|
136
247
|
if (page.pagination.has_more) {
|
|
137
|
-
const next = await
|
|
248
|
+
const next = await shipmail.domains.list({
|
|
138
249
|
cursor: page.pagination.next_cursor,
|
|
139
250
|
limit: 10,
|
|
140
251
|
});
|
|
141
252
|
}
|
|
142
253
|
```
|
|
143
254
|
|
|
144
|
-
Auto-
|
|
255
|
+
Auto-paginate over all pages:
|
|
145
256
|
|
|
146
|
-
```
|
|
147
|
-
for await (const domain of
|
|
257
|
+
```ts
|
|
258
|
+
for await (const domain of shipmail.domains.listAutoPaginating({ limit: 25 })) {
|
|
148
259
|
console.log(domain.name);
|
|
149
260
|
}
|
|
150
261
|
```
|
|
151
262
|
|
|
152
|
-
|
|
263
|
+
`listAutoPaginating` is available on `domains`, `mailboxes`, `messages`, `threads`, `webhooks`, `webhooks.listDeliveriesAutoPaginating`, and `suppressions`.
|
|
264
|
+
|
|
265
|
+
## Webhook verification
|
|
153
266
|
|
|
154
267
|
Verify incoming webhook signatures without instantiating a client:
|
|
155
268
|
|
|
156
|
-
```
|
|
269
|
+
```ts
|
|
157
270
|
import { verifyWebhook, WebhookVerificationError } from "shipmail";
|
|
158
271
|
|
|
159
272
|
try {
|
|
160
273
|
const event = await verifyWebhook(rawBody, request.headers, webhookSecret);
|
|
161
|
-
|
|
162
|
-
|
|
274
|
+
event.event_type; // typed WebhookEventType union
|
|
275
|
+
event.data;
|
|
163
276
|
} catch (err) {
|
|
164
277
|
if (err instanceof WebhookVerificationError) {
|
|
165
|
-
|
|
278
|
+
// signature mismatch, missing header, expired timestamp, etc.
|
|
166
279
|
}
|
|
167
280
|
}
|
|
168
281
|
```
|
|
169
282
|
|
|
170
|
-
|
|
283
|
+
### Next.js Route Handler example
|
|
284
|
+
|
|
285
|
+
App Router consumes `request.text()` to get the raw body. Do not parse to JSON before verifying.
|
|
171
286
|
|
|
172
|
-
|
|
287
|
+
```ts
|
|
288
|
+
// app/api/webhooks/shipmail/route.ts
|
|
289
|
+
import { verifyWebhook, WebhookVerificationError } from "shipmail";
|
|
173
290
|
|
|
174
|
-
|
|
291
|
+
export async function POST(request: Request) {
|
|
292
|
+
const rawBody = await request.text();
|
|
293
|
+
const secret = process.env.SHIPMAIL_WEBHOOK_SECRET!;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const event = await verifyWebhook(rawBody, request.headers, secret);
|
|
297
|
+
// handle event...
|
|
298
|
+
return new Response("ok");
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (err instanceof WebhookVerificationError) {
|
|
301
|
+
return new Response("invalid signature", { status: 401 });
|
|
302
|
+
}
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Per-request options
|
|
309
|
+
|
|
310
|
+
Every method accepts a final `options` argument:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
type MethodOptions = {
|
|
314
|
+
timeout?: number;
|
|
315
|
+
signal?: AbortSignal;
|
|
316
|
+
headers?: Record<string, string>;
|
|
317
|
+
idempotencyKey?: string;
|
|
318
|
+
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
await shipmail.messages.send(params, {
|
|
323
|
+
timeout: 5_000,
|
|
324
|
+
headers: { "x-trace-id": traceId },
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Idempotency
|
|
329
|
+
|
|
330
|
+
Pass `idempotencyKey` on mutating calls to make them safe to retry:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
await shipmail.messages.send(
|
|
334
|
+
{
|
|
335
|
+
mailbox_id: "mbx_...",
|
|
336
|
+
to: [{ address: "user@example.com" }],
|
|
337
|
+
subject: "Receipt",
|
|
338
|
+
text: "Thanks for your purchase.",
|
|
339
|
+
},
|
|
340
|
+
{ idempotencyKey: `receipt-${orderId}` },
|
|
341
|
+
);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The SDK adds the key as the `Idempotency-Key` header. Reuse the same key to retry without sending a duplicate email. Keys are scoped per API key.
|
|
345
|
+
|
|
346
|
+
## Cancellation
|
|
347
|
+
|
|
348
|
+
Pass an `AbortSignal` to cancel an in-flight request. The signal also cancels SDK-internal retries:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
const controller = new AbortController();
|
|
352
|
+
setTimeout(() => controller.abort(), 2_000);
|
|
353
|
+
|
|
354
|
+
await shipmail.messages.send(params, { signal: controller.signal });
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Custom fetch and proxies
|
|
358
|
+
|
|
359
|
+
Inject a custom fetch implementation for proxies, observability, or testing:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
const shipmail = new ShipMailClient({
|
|
363
|
+
apiKey: process.env.SHIPMAIL_API_KEY!,
|
|
364
|
+
fetch: async (url, init) => {
|
|
365
|
+
const start = Date.now();
|
|
366
|
+
const res = await fetch(url, init);
|
|
367
|
+
metrics.histogram("shipmail.fetch.duration_ms", Date.now() - start);
|
|
368
|
+
return res;
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The custom fetch receives the same arguments as the global `fetch` and must return a `Response`.
|
|
374
|
+
|
|
375
|
+
## Errors
|
|
376
|
+
|
|
377
|
+
The SDK throws typed errors that map to HTTP responses. All inherit from `ShipMailError`:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
175
380
|
import {
|
|
176
381
|
ShipMailError,
|
|
177
382
|
AuthenticationError,
|
|
178
383
|
AuthorizationError,
|
|
179
384
|
ValidationError,
|
|
180
385
|
NotFoundError,
|
|
181
|
-
RateLimitError,
|
|
182
386
|
ConflictError,
|
|
387
|
+
RateLimitError,
|
|
388
|
+
QuotaExceededError,
|
|
183
389
|
InternalServerError,
|
|
184
390
|
ConnectionError,
|
|
185
391
|
} from "shipmail";
|
|
186
392
|
|
|
187
393
|
try {
|
|
188
|
-
await
|
|
394
|
+
await shipmail.messages.send(params);
|
|
189
395
|
} catch (err) {
|
|
190
396
|
if (err instanceof ValidationError) {
|
|
191
|
-
|
|
192
|
-
|
|
397
|
+
err.message;
|
|
398
|
+
err.details; // field-level validation errors
|
|
193
399
|
}
|
|
194
400
|
if (err instanceof RateLimitError) {
|
|
195
|
-
|
|
401
|
+
err.retryAfter; // seconds
|
|
196
402
|
}
|
|
197
403
|
if (err instanceof ShipMailError) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
404
|
+
err.status; // HTTP status
|
|
405
|
+
err.type; // error type string
|
|
406
|
+
err.requestId; // include this when contacting support
|
|
407
|
+
err.retryable;
|
|
202
408
|
}
|
|
409
|
+
throw err;
|
|
203
410
|
}
|
|
204
411
|
```
|
|
205
412
|
|
|
413
|
+
| Error | When |
|
|
414
|
+
| --------------------- | -------------------------------------------------------------- |
|
|
415
|
+
| `AuthenticationError` | 401. Bad or missing API key. |
|
|
416
|
+
| `AuthorizationError` | 403. Key lacks permission for the resource. |
|
|
417
|
+
| `ValidationError` | 400 or 422. See `details` for per-field errors. |
|
|
418
|
+
| `NotFoundError` | 404. |
|
|
419
|
+
| `ConflictError` | 409. Resource already exists or state conflict. |
|
|
420
|
+
| `RateLimitError` | 429. Read `retryAfter` (seconds). |
|
|
421
|
+
| `QuotaExceededError` | 402. Plan or sending quota exceeded. |
|
|
422
|
+
| `InternalServerError` | 5xx. Retried automatically up to `maxRetries`. |
|
|
423
|
+
| `ConnectionError` | Network error, timeout, or DNS failure. Retried automatically. |
|
|
424
|
+
|
|
206
425
|
## Retries
|
|
207
426
|
|
|
208
|
-
The SDK
|
|
427
|
+
The SDK retries on `5xx`, `429`, and connection errors with exponential backoff and jitter. `Retry-After` is honored when present. Default is 2 retries (3 total attempts).
|
|
209
428
|
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
429
|
+
```ts
|
|
430
|
+
new ShipMailClient({ apiKey, maxRetries: 0 }); // disable retries
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Retries respect any `AbortSignal` you pass via `MethodOptions.signal`.
|
|
434
|
+
|
|
435
|
+
## Bundling
|
|
436
|
+
|
|
437
|
+
The package ships ESM and CommonJS via `exports`, with `"sideEffects": false` for tree-shaking. Importing a single resource pulls in only what it needs. The published bundle has no runtime dependencies.
|
|
438
|
+
|
|
439
|
+
## Testing
|
|
440
|
+
|
|
441
|
+
Mock by injecting a custom `fetch` at construction time:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
const shipmail = new ShipMailClient({
|
|
445
|
+
apiKey: "sm_live_test",
|
|
446
|
+
fetch: async () =>
|
|
447
|
+
new Response(JSON.stringify({ id: "msg_123", status: "queued" }), {
|
|
448
|
+
status: 200,
|
|
449
|
+
headers: { "content-type": "application/json" },
|
|
450
|
+
}),
|
|
214
451
|
});
|
|
215
452
|
```
|
|
216
453
|
|
|
454
|
+
This is the recommended pattern for unit tests. No HTTP interception or mocking library required.
|
|
455
|
+
|
|
217
456
|
## License
|
|
218
457
|
|
|
219
|
-
MIT
|
|
458
|
+
[MIT](./LICENSE).
|
|
459
|
+
|
|
460
|
+
## Links
|
|
461
|
+
|
|
462
|
+
- [Shipmail docs](https://shipmail.to/docs)
|
|
463
|
+
- [Quick start](https://shipmail.to/docs/quick-start)
|
|
464
|
+
- [API reference](https://shipmail.to/docs/api)
|
|
465
|
+
- [TypeScript SDK guide](https://shipmail.to/docs/sdks/typescript)
|
|
466
|
+
- [`shipmail-mcp` (MCP server)](https://www.npmjs.com/package/shipmail-mcp)
|
|
467
|
+
- [Changelog](./CHANGELOG.md)
|
|
468
|
+
- [Issues](https://github.com/jcoulaud/ShipMail/issues)
|