shipmail 0.1.20 → 0.1.22

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,471 @@
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_...", {
130
+ name: "VIP",
131
+ parent_id: null,
132
+ });
133
+ await shipmail.mailboxes.updateFolder("mbx_...", folder.id, { name: "VIP Clients" });
134
+ await shipmail.mailboxes.deleteFolder("mbx_...", folder.id);
135
+ const identities = await shipmail.mailboxes.listIdentities("mbx_...");
136
+ const rules = await shipmail.mailboxes.getRules("mbx_...");
137
+ await shipmail.mailboxes.updateRules("mbx_...", { rules: rules.rules });
138
+ await shipmail.mailboxes.updateSpamFilter("mbx_...", { threshold: 8 });
139
+ await shipmail.mailboxes.delete("mbx_...");
140
+
141
+ await shipmail.mailboxes.updateAutoReply("mbx_...", {
142
+ enabled: true,
143
+ subject: "Out of office",
144
+ body: "Back on Monday.",
145
+ from_date: "2026-06-01",
146
+ to_date: "2026-06-07",
147
+ });
72
148
  ```
73
149
 
74
- ### Messages
150
+ ## Messages
75
151
 
76
- ```typescript
77
- const message = await client.messages.send({
152
+ ```ts
153
+ await shipmail.messages.send({
78
154
  mailbox_id: "mbx_...",
79
- to: [{ address: "user@example.com", name: "User" }],
155
+ to: [{ address: "user@example.com" }],
80
156
  cc: [{ address: "cc@example.com" }],
81
157
  subject: "Hello",
82
- body_html: "<p>Hi there</p>",
83
- body_text: "Hi there",
158
+ text: "Hi there",
159
+ html: "<p>Hi there</p>",
84
160
  });
85
161
 
86
- const message = await client.messages.get("msg_...");
162
+ await shipmail.messages.list({ mailbox_id: "mbx_...", limit: 25 });
163
+ await shipmail.messages.get("msg_...");
164
+
165
+ await shipmail.messages.reply("msg_...", {
166
+ to: [{ address: "user@example.com" }],
167
+ text: "Thanks for your email.",
168
+ });
87
169
  ```
88
170
 
89
- ### Threads
171
+ ## Threads
90
172
 
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" }],
173
+ ```ts
174
+ const threads = await shipmail.threads.list({ mailbox_id: "mbx_..." });
175
+ const threadId = threads.data[0].id;
176
+ const thread = await shipmail.threads.get(threadId);
177
+
178
+ await shipmail.threads.reply(threadId, {
179
+ text: "Thanks for your email.",
97
180
  });
98
181
  ```
99
182
 
100
- ### Webhooks
183
+ ## Webhooks
101
184
 
102
- ```typescript
103
- const webhook = await client.webhooks.create({
185
+ ```ts
186
+ const webhook = await shipmail.webhooks.create({
104
187
  url: "https://example.com/webhook",
105
188
  events: ["message.received", "message.sent"],
106
- description: "My webhook",
189
+ description: "Incoming email handler",
107
190
  });
108
- // webhook.secret is only available at creation time
191
+ // webhook.secret is returned only on creation. Store it now.
192
+
193
+ await shipmail.webhooks.list();
194
+ await shipmail.webhooks.get("whk_...");
195
+ await shipmail.webhooks.update("whk_...", { active: false });
196
+ await shipmail.webhooks.delete("whk_...");
197
+
198
+ await shipmail.webhooks.rotateSecret("whk_...");
199
+ await shipmail.webhooks.test("whk_...");
200
+ await shipmail.webhooks.listDeliveries("whk_...");
201
+ ```
202
+
203
+ Supported event types:
204
+
205
+ ```
206
+ message.received
207
+ message.sent
208
+ message.delivered
209
+ message.bounced
210
+ message.complained
211
+ domain.verified
212
+ domain.verification_failed
213
+ domain.degraded
214
+ org.reputation_warning
215
+ org.sending_throttled
216
+ org.sending_suspended
217
+ org.reputation_recovered
218
+ ```
219
+
220
+ ## Suppressions
221
+
222
+ ```ts
223
+ await shipmail.suppressions.list({ limit: 25 });
224
+ await shipmail.suppressions.remove("user@example.com");
225
+ ```
109
226
 
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_...");
227
+ Auto-paginate:
114
228
 
115
- const rotated = await client.webhooks.rotateSecret("whk_...");
116
- const test = await client.webhooks.test("whk_...");
117
- const deliveries = await client.webhooks.listDeliveries("whk_...");
229
+ ```ts
230
+ for await (const item of shipmail.suppressions.listAutoPaginating({ limit: 100 })) {
231
+ console.log(item.email_address, item.reason);
232
+ }
118
233
  ```
119
234
 
120
- ### Status
235
+ ## Status
121
236
 
122
- ```typescript
123
- const status = await client.status.get();
237
+ ```ts
238
+ const status = await shipmail.status.get();
124
239
  ```
125
240
 
126
241
  ## Pagination
127
242
 
128
- List methods return a paginated response with cursor-based pagination:
243
+ List methods return `{ data, pagination }` with cursor-based pagination:
129
244
 
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 }
245
+ ```ts
246
+ const page = await shipmail.domains.list({ limit: 10 });
247
+ page.data; // Domain[]
248
+ page.pagination; // { next_cursor, has_more }
134
249
 
135
- // Fetch next page
136
250
  if (page.pagination.has_more) {
137
- const next = await client.domains.list({
251
+ const next = await shipmail.domains.list({
138
252
  cursor: page.pagination.next_cursor,
139
253
  limit: 10,
140
254
  });
141
255
  }
142
256
  ```
143
257
 
144
- Auto-pagination iterates through all pages automatically:
258
+ Auto-paginate over all pages:
145
259
 
146
- ```typescript
147
- for await (const domain of client.domains.listAutoPaginating({ limit: 25 })) {
260
+ ```ts
261
+ for await (const domain of shipmail.domains.listAutoPaginating({ limit: 25 })) {
148
262
  console.log(domain.name);
149
263
  }
150
264
  ```
151
265
 
152
- ## Webhook Verification
266
+ `listAutoPaginating` is available on `domains`, `mailboxes`, `messages`, `threads`, `webhooks`, `webhooks.listDeliveriesAutoPaginating`, and `suppressions`.
267
+
268
+ ## Webhook verification
153
269
 
154
270
  Verify incoming webhook signatures without instantiating a client:
155
271
 
156
- ```typescript
272
+ ```ts
157
273
  import { verifyWebhook, WebhookVerificationError } from "shipmail";
158
274
 
159
275
  try {
160
276
  const event = await verifyWebhook(rawBody, request.headers, webhookSecret);
161
- console.log(event.event_type); // e.g., "message.received"
162
- console.log(event.data);
277
+ event.event_type; // typed WebhookEventType union
278
+ event.data;
163
279
  } catch (err) {
164
280
  if (err instanceof WebhookVerificationError) {
165
- return new Response("Invalid signature", { status: 401 });
281
+ // signature mismatch, missing header, expired timestamp, etc.
282
+ }
283
+ }
284
+ ```
285
+
286
+ ### Next.js Route Handler example
287
+
288
+ App Router consumes `request.text()` to get the raw body. Do not parse to JSON before verifying.
289
+
290
+ ```ts
291
+ // app/api/webhooks/shipmail/route.ts
292
+ import { verifyWebhook, WebhookVerificationError } from "shipmail";
293
+
294
+ export async function POST(request: Request) {
295
+ const rawBody = await request.text();
296
+ const secret = process.env.SHIPMAIL_WEBHOOK_SECRET!;
297
+
298
+ try {
299
+ const event = await verifyWebhook(rawBody, request.headers, secret);
300
+ // handle event...
301
+ return new Response("ok");
302
+ } catch (err) {
303
+ if (err instanceof WebhookVerificationError) {
304
+ return new Response("invalid signature", { status: 401 });
305
+ }
306
+ throw err;
166
307
  }
167
308
  }
168
309
  ```
169
310
 
170
- ## Error Handling
311
+ ## Per-request options
312
+
313
+ Every method accepts a final `options` argument:
314
+
315
+ ```ts
316
+ type MethodOptions = {
317
+ timeout?: number;
318
+ signal?: AbortSignal;
319
+ headers?: Record<string, string>;
320
+ idempotencyKey?: string;
321
+ };
322
+ ```
323
+
324
+ ```ts
325
+ await shipmail.messages.send(params, {
326
+ timeout: 5_000,
327
+ headers: { "x-trace-id": traceId },
328
+ });
329
+ ```
330
+
331
+ ## Idempotency
332
+
333
+ Pass `idempotencyKey` on mutating calls to make them safe to retry:
334
+
335
+ ```ts
336
+ await shipmail.messages.send(
337
+ {
338
+ mailbox_id: "mbx_...",
339
+ to: [{ address: "user@example.com" }],
340
+ subject: "Receipt",
341
+ text: "Thanks for your purchase.",
342
+ },
343
+ { idempotencyKey: `receipt-${orderId}` },
344
+ );
345
+ ```
346
+
347
+ 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.
348
+
349
+ ## Cancellation
350
+
351
+ Pass an `AbortSignal` to cancel an in-flight request. The signal also cancels SDK-internal retries:
352
+
353
+ ```ts
354
+ const controller = new AbortController();
355
+ setTimeout(() => controller.abort(), 2_000);
356
+
357
+ await shipmail.messages.send(params, { signal: controller.signal });
358
+ ```
171
359
 
172
- The SDK throws typed errors that map to API error responses:
360
+ ## Custom fetch and proxies
173
361
 
174
- ```typescript
362
+ Inject a custom fetch implementation for proxies, observability, or testing:
363
+
364
+ ```ts
365
+ const shipmail = new ShipMailClient({
366
+ apiKey: process.env.SHIPMAIL_API_KEY!,
367
+ fetch: async (url, init) => {
368
+ const start = Date.now();
369
+ const res = await fetch(url, init);
370
+ metrics.histogram("shipmail.fetch.duration_ms", Date.now() - start);
371
+ return res;
372
+ },
373
+ });
374
+ ```
375
+
376
+ The custom fetch receives the same arguments as the global `fetch` and must return a `Response`.
377
+
378
+ ## Errors
379
+
380
+ The SDK throws typed errors that map to HTTP responses. All inherit from `ShipMailError`:
381
+
382
+ ```ts
175
383
  import {
176
384
  ShipMailError,
177
385
  AuthenticationError,
178
386
  AuthorizationError,
179
387
  ValidationError,
180
388
  NotFoundError,
181
- RateLimitError,
182
389
  ConflictError,
390
+ RateLimitError,
391
+ QuotaExceededError,
183
392
  InternalServerError,
184
393
  ConnectionError,
185
394
  } from "shipmail";
186
395
 
187
396
  try {
188
- await client.domains.create({ name: "" });
397
+ await shipmail.messages.send(params);
189
398
  } catch (err) {
190
399
  if (err instanceof ValidationError) {
191
- console.log(err.message); // Error message
192
- console.log(err.details); // Field-level validation errors
400
+ err.message;
401
+ err.details; // field-level validation errors
193
402
  }
194
403
  if (err instanceof RateLimitError) {
195
- console.log(err.retryAfter); // Seconds to wait
404
+ err.retryAfter; // seconds
196
405
  }
197
406
  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
407
+ err.status; // HTTP status
408
+ err.type; // error type string
409
+ err.requestId; // include this when contacting support
410
+ err.retryable;
202
411
  }
412
+ throw err;
203
413
  }
204
414
  ```
205
415
 
416
+ | Error | When |
417
+ | --------------------- | -------------------------------------------------------------- |
418
+ | `AuthenticationError` | 401. Bad or missing API key. |
419
+ | `AuthorizationError` | 403. Key lacks permission for the resource. |
420
+ | `ValidationError` | 400 or 422. See `details` for per-field errors. |
421
+ | `NotFoundError` | 404. |
422
+ | `ConflictError` | 409. Resource already exists or state conflict. |
423
+ | `RateLimitError` | 429. Read `retryAfter` (seconds). |
424
+ | `QuotaExceededError` | 402. Plan or sending quota exceeded. |
425
+ | `InternalServerError` | 5xx. Retried automatically up to `maxRetries`. |
426
+ | `ConnectionError` | Network error, timeout, or DNS failure. Retried automatically. |
427
+
206
428
  ## Retries
207
429
 
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).
430
+ 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
431
 
210
- ```typescript
211
- const client = new ShipMailClient({
212
- apiKey: "sm_live_...",
213
- maxRetries: 0, // Disable retries
432
+ ```ts
433
+ new ShipMailClient({ apiKey, maxRetries: 0 }); // disable retries
434
+ ```
435
+
436
+ Retries respect any `AbortSignal` you pass via `MethodOptions.signal`.
437
+
438
+ ## Bundling
439
+
440
+ 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.
441
+
442
+ ## Testing
443
+
444
+ Mock by injecting a custom `fetch` at construction time:
445
+
446
+ ```ts
447
+ const shipmail = new ShipMailClient({
448
+ apiKey: "sm_live_test",
449
+ fetch: async () =>
450
+ new Response(JSON.stringify({ id: "msg_123", status: "queued" }), {
451
+ status: 200,
452
+ headers: { "content-type": "application/json" },
453
+ }),
214
454
  });
215
455
  ```
216
456
 
457
+ This is the recommended pattern for unit tests. No HTTP interception or mocking library required.
458
+
217
459
  ## License
218
460
 
219
- MIT
461
+ [MIT](./LICENSE).
462
+
463
+ ## Links
464
+
465
+ - [Shipmail docs](https://shipmail.to/docs)
466
+ - [Quick start](https://shipmail.to/docs/quick-start)
467
+ - [API reference](https://shipmail.to/docs/api)
468
+ - [TypeScript SDK guide](https://shipmail.to/docs/sdks/typescript)
469
+ - [`shipmail-mcp` (MCP server)](https://www.npmjs.com/package/shipmail-mcp)
470
+ - [Changelog](./CHANGELOG.md)
471
+ - [Issues](https://github.com/jcoulaud/ShipMail/issues)