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 CHANGED
@@ -1,219 +1,468 @@
1
- # ShipMail TypeScript SDK
2
-
3
- Official TypeScript SDK for the [ShipMail](https://shipmail.to) API. Zero runtime dependencies. Works with Node.js 18+, Bun, and Deno.
4
-
5
- ## Installation
1
+ # Shipmail TypeScript SDK
2
+
3
+ [![npm version](https://img.shields.io/npm/v/shipmail.svg)](https://www.npmjs.com/package/shipmail)
4
+ [![npm downloads](https://img.shields.io/npm/dm/shipmail.svg)](https://www.npmjs.com/package/shipmail)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/shipmail.svg)](https://bundlephobia.com/package/shipmail)
6
+ [![types](https://img.shields.io/npm/types/shipmail.svg)](https://www.npmjs.com/package/shipmail)
7
+ [![license](https://img.shields.io/npm/l/shipmail.svg)](./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
- bun add shipmail
45
+ pnpm add shipmail
11
46
  ```
12
47
 
13
- Requires Node.js 18+ (uses native `fetch` and `crypto`).
14
-
15
- ## Quick Start
48
+ ## Quick start
16
49
 
17
- ```typescript
18
- import ShipMail from "shipmail";
19
-
20
- const client = new ShipMail("sm_live_...");
50
+ ```ts
51
+ import { ShipMailClient } from "shipmail";
21
52
 
22
- // Create a domain
23
- const domain = await client.domains.create({ name: "example.com" });
53
+ const shipmail = new ShipMailClient({ apiKey: process.env.SHIPMAIL_API_KEY! });
24
54
 
25
- // Send an email
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
- body_text: "Hi there",
59
+ text: "Hi there",
60
+ html: "<p>Hi there</p>",
31
61
  });
32
62
  ```
33
63
 
34
- ## Configuration
64
+ The SDK does not auto-read environment variables. Pass the key explicitly.
35
65
 
36
- ```typescript
37
- import { ShipMailClient } from "shipmail";
66
+ You can also pass a key string directly:
38
67
 
39
- const client = new ShipMailClient({
40
- apiKey: "sm_live_...",
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
- ## Resources
72
+ ## Configuration
48
73
 
49
- ### Domains
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
- ```typescript
52
- const domain = await client.domains.create({ name: "example.com" });
53
- const domains = await client.domains.list({ limit: 10 });
54
- const domain = await client.domains.get("dom_...");
55
- const updated = await client.domains.update("dom_...", { catch_all_mailbox_id: "mbx_..." });
56
- await client.domains.delete("dom_...");
57
- const result = await client.domains.verify("dom_...");
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
- ### Mailboxes
116
+ ## Mailboxes
61
117
 
62
- ```typescript
63
- const mailbox = await client.mailboxes.create({
118
+ ```ts
119
+ await shipmail.mailboxes.create({
64
120
  domain_id: "dom_...",
65
121
  address: "hello",
66
122
  display_name: "Hello",
67
123
  });
68
- const mailboxes = await client.mailboxes.list({ domain_id: "dom_..." });
69
- const mailbox = await client.mailboxes.get("mbx_...");
70
- const updated = await client.mailboxes.update("mbx_...", { display_name: "New Name" });
71
- await client.mailboxes.delete("mbx_...");
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
- ### Messages
147
+ ## Messages
75
148
 
76
- ```typescript
77
- const message = await client.messages.send({
149
+ ```ts
150
+ await shipmail.messages.send({
78
151
  mailbox_id: "mbx_...",
79
- to: [{ address: "user@example.com", name: "User" }],
152
+ to: [{ address: "user@example.com" }],
80
153
  cc: [{ address: "cc@example.com" }],
81
154
  subject: "Hello",
82
- body_html: "<p>Hi there</p>",
83
- body_text: "Hi there",
155
+ text: "Hi there",
156
+ html: "<p>Hi there</p>",
84
157
  });
85
158
 
86
- const message = await client.messages.get("msg_...");
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
- ### Threads
168
+ ## Threads
90
169
 
91
- ```typescript
92
- const threads = await client.threads.list({ mailbox_id: "mbx_..." });
93
- const thread = await client.threads.get("thd_...");
94
- const reply = await client.threads.reply("thd_...", {
95
- body_text: "Thanks for your email",
96
- to: [{ address: "user@example.com" }],
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
- ### Webhooks
180
+ ## Webhooks
101
181
 
102
- ```typescript
103
- const webhook = await client.webhooks.create({
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: "My webhook",
186
+ description: "Incoming email handler",
107
187
  });
108
- // webhook.secret is only available at creation time
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
- const webhooks = await client.webhooks.list();
111
- const webhook = await client.webhooks.get("whk_...");
112
- const updated = await client.webhooks.update("whk_...", { active: false });
113
- await client.webhooks.delete("whk_...");
219
+ ```ts
220
+ await shipmail.suppressions.list({ limit: 25 });
221
+ await shipmail.suppressions.remove("user@example.com");
222
+ ```
114
223
 
115
- const rotated = await client.webhooks.rotateSecret("whk_...");
116
- const test = await client.webhooks.test("whk_...");
117
- const deliveries = await client.webhooks.listDeliveries("whk_...");
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
- ### Status
232
+ ## Status
121
233
 
122
- ```typescript
123
- const status = await client.status.get();
234
+ ```ts
235
+ const status = await shipmail.status.get();
124
236
  ```
125
237
 
126
238
  ## Pagination
127
239
 
128
- List methods return a paginated response with cursor-based pagination:
240
+ List methods return `{ data, pagination }` with cursor-based pagination:
129
241
 
130
- ```typescript
131
- const page = await client.domains.list({ limit: 10 });
132
- console.log(page.data); // Domain[]
133
- console.log(page.pagination); // { next_cursor, has_more }
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 client.domains.list({
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-pagination iterates through all pages automatically:
255
+ Auto-paginate over all pages:
145
256
 
146
- ```typescript
147
- for await (const domain of client.domains.listAutoPaginating({ limit: 25 })) {
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
- ## Webhook Verification
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
- ```typescript
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
- console.log(event.event_type); // e.g., "message.received"
162
- console.log(event.data);
274
+ event.event_type; // typed WebhookEventType union
275
+ event.data;
163
276
  } catch (err) {
164
277
  if (err instanceof WebhookVerificationError) {
165
- return new Response("Invalid signature", { status: 401 });
278
+ // signature mismatch, missing header, expired timestamp, etc.
166
279
  }
167
280
  }
168
281
  ```
169
282
 
170
- ## Error Handling
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
- The SDK throws typed errors that map to API error responses:
287
+ ```ts
288
+ // app/api/webhooks/shipmail/route.ts
289
+ import { verifyWebhook, WebhookVerificationError } from "shipmail";
173
290
 
174
- ```typescript
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 client.domains.create({ name: "" });
394
+ await shipmail.messages.send(params);
189
395
  } catch (err) {
190
396
  if (err instanceof ValidationError) {
191
- console.log(err.message); // Error message
192
- console.log(err.details); // Field-level validation errors
397
+ err.message;
398
+ err.details; // field-level validation errors
193
399
  }
194
400
  if (err instanceof RateLimitError) {
195
- console.log(err.retryAfter); // Seconds to wait
401
+ err.retryAfter; // seconds
196
402
  }
197
403
  if (err instanceof ShipMailError) {
198
- console.log(err.status); // HTTP status code
199
- console.log(err.type); // Error type string
200
- console.log(err.requestId); // Request ID for support
201
- console.log(err.retryable); // Whether the request can be retried
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 automatically retries on 5xx errors and 429 (rate limit) responses with exponential backoff and jitter. Configure with `maxRetries` (default: 2, meaning up to 3 total attempts).
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
- ```typescript
211
- const client = new ShipMailClient({
212
- apiKey: "sm_live_...",
213
- maxRetries: 0, // Disable retries
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)