mailchannels-sdk 1.0.0 → 1.2.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.
@@ -0,0 +1,143 @@
1
+ # Error Handling
2
+
3
+ The JS SDK uses a **result-based** error style. Every method returns either a
4
+ `DataResponse<T>` or a `SuccessResponse` — the SDK never throws for API errors.
5
+
6
+ ```ts
7
+ type DataResponse<T> =
8
+ | { data: T; error: null }
9
+ | { data: null; error: ErrorResponse }
10
+
11
+ type SuccessResponse = {
12
+ success: boolean
13
+ error: ErrorResponse | null
14
+ }
15
+ ```
16
+
17
+ Always destructure and check `error` before using the result:
18
+
19
+ ```ts
20
+ const { data, error } = await mc.emails.queue({ ... })
21
+
22
+ if (error) {
23
+ // error.message — human-readable description
24
+ // error.type — stable string identifier (see table below)
25
+ // error.statusCode — HTTP status code, or null for non-HTTP errors
26
+ // error.response — Response body (Object) for 4xx/5xx errors
27
+ console.error(error.message, error.type, error.statusCode)
28
+ return
29
+ }
30
+ // data is non-null here
31
+ console.log(data.requestId)
32
+ ```
33
+
34
+ ### Error Types
35
+
36
+ | `error.type` | HTTP status | Meaning |
37
+ | --- | --- | --- |
38
+ | `'invalid_request_error'` | 400 | Bad payload, validation errors. |
39
+ | `'authentication_error'` | 401 | Invalid or missing API key. |
40
+ | `'permission_error'` | 403 | Feature not enabled or account limits. |
41
+ | `'not_found'` | 404 | Resource not found. |
42
+ | `'conflict_error'` | 409 | Duplicate webhook, duplicate suppression, etc. |
43
+ | `'payload_too_large_error'` | 413 | Over the 30 MB limit. |
44
+ | `'unprocessable_entity_error'` | 422 | Semantically invalid request. |
45
+ | `'rate_limit_error'` | 429 | Too many requests — slow down. |
46
+ | `'internal_server_error'` | 500 | Generic server-side failure. |
47
+ | `'validation_error'` | `null` | Client-side validation failed before any HTTP call. |
48
+ | `'application_error'` | `null` | Unexpected JS/network error (e.g. fetch failed). |
49
+ | `'api_error'` | varies | Fallback for unmapped status codes. |
50
+
51
+ ### Handling Patterns
52
+
53
+ The `message` field is a human-readable description of the error, useful for logging and debugging,
54
+ but it should not be parsed or used for control flow as it may change without warning. Instead, use the `type` field for error handling logic.
55
+
56
+ ```ts
57
+ const { data, error } = await mc.emails.queue(message)
58
+
59
+ if (error) {
60
+ switch (error.type) {
61
+ case 'payload_too_large_error':
62
+ // Split recipient list or shrink attachments, then retry.
63
+ break
64
+ case 'rate_limit_error':
65
+ // Back off and retry — check error.statusCode === 429.
66
+ break
67
+ case 'invalid_request_error':
68
+ case 'validation_error':
69
+ // Application bug — fix the payload, don't retry blindly.
70
+ console.error('Invalid payload:', error.message)
71
+ break
72
+ case 'authentication_error':
73
+ // Wrong API key. Don't retry.
74
+ console.error('Invalid authentication:', error.message)
75
+ break
76
+ case 'permission_error':
77
+ // Feature gated or sub-account suspended. Don't retry.
78
+ console.error('Permission denied:', error.message)
79
+ break
80
+ case 'internal_server_error':
81
+ case 'application_error':
82
+ // Transient — retry with exponential backoff.
83
+ break
84
+ default:
85
+ console.error('Unexpected error:', error.message)
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### Client-Side Validation Errors (`validation_error`)
91
+
92
+ These produce `error.type === 'validation_error'` and `error.statusCode === null` before
93
+ any HTTP call:
94
+
95
+ - Missing `from`, `to` / `personalizations`, `subject`, body content.
96
+ - Empty collections.
97
+ - Reserved headers in `headers`.
98
+ - Non-string header values.
99
+ - `campaignId` too long or containing spaces.
100
+ - `personalizations` with `transactional: false` and multiple recipients.
101
+ - Invalid DKIM field combinations.
102
+
103
+ Failing early gives a precise error rather than a vague 400.
104
+
105
+ ### Retrying and Idempotency
106
+
107
+ | Error type | Safe to retry? |
108
+ | --- | --- |
109
+ | `invalid_request_error`, `authentication_error`, `permission_error`, `conflict_error`, `payload_too_large_error`, `validation_error` | **No** — caller problem; fix the input or credentials. |
110
+ | `rate_limit_error` | Yes, after backing off. |
111
+ | `internal_server_error`, `application_error` | Yes, with exponential backoff. |
112
+
113
+ The API has no idempotency keys, so retrying `emails.send()` or `emails.queue()` after a
114
+ transient failure can produce a duplicate send. Build idempotency into the caller (e.g. a
115
+ unique `campaignId` + recipient-set check) when at-most-once delivery matters.
116
+
117
+ ### SDK Exceptions
118
+
119
+ `new MailChannels('')` (empty or missing key) throws synchronously:
120
+
121
+ ```ts
122
+ try {
123
+ const mc = new MailChannels(process.env.MAILCHANNELS_API_KEY)
124
+ // ...
125
+ }
126
+ catch (err) {
127
+ // err.message === 'Missing MailChannels API key.'
128
+ }
129
+ ```
130
+
131
+ `Attachment.fromBlob` (invalid object type) throws when called synchronously:
132
+
133
+ ```ts
134
+ try {
135
+ const attachment = await Attachment.fromBlob(blob1, { filename: "file.pdf" })
136
+ // ...
137
+ }
138
+ catch (err) {
139
+ // err.message === 'Unable to create attachment: expected a Blob.'
140
+ }
141
+ ```
142
+
143
+ Almost all other failure modes surface through the `error` field in the returned result object.
@@ -0,0 +1,114 @@
1
+ # Metrics and Usage
2
+
3
+ The metrics module exposes the operational view of your traffic. Usage exposes
4
+ billing-period totals.
5
+
6
+ ### Time-Series Metrics
7
+
8
+ The four time-series methods share the same `MetricsOptions` parameter shape: optional
9
+ `startTime`, `endTime`, `campaignId`, and `interval`.
10
+
11
+ ```ts
12
+ import { MailChannels } from 'mailchannels-sdk'
13
+
14
+ const mc = new MailChannels('YOUR-API-KEY')
15
+
16
+ const { data, error } = await mc.metrics.volume({
17
+ startTime: '2026-04-01',
18
+ endTime: '2026-05-01T00:00:00Z',
19
+ interval: 'day', // 'hour' | 'day' | 'week' | 'month' (default 'day')
20
+ campaignId: 'welcome' // optional filter
21
+ })
22
+
23
+ // Date objects are also accepted
24
+ const { data: last7d, error: error7days } = await mc.metrics.volume({
25
+ startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
26
+ endTime: new Date(),
27
+ interval: 'day'
28
+ })
29
+ if (error) { /*...*/ }
30
+
31
+ // Same parameter shape — pass the same options to any of these:
32
+ const { data: engagement, error: engErr } = await mc.metrics.engagement({ interval: 'day' })
33
+ const { data: perf, error: perfErr } = await mc.metrics.performance({ interval: 'day' })
34
+ const { data: behaviour, error: behErr } = await mc.metrics.recipientBehaviour({ interval: 'day' })
35
+ ```
36
+
37
+ Time formats accepted: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM:SSZ`, or a `Date` object. Defaults: `startTime` is
38
+ one month ago, `endTime` is now.
39
+
40
+ #### Buckets
41
+
42
+ Each response includes both totals **and** a `buckets` object grouped by metric name, each
43
+ containing a list of `{ count, periodStart }` rows aligned to `interval`.
44
+
45
+ ```ts
46
+ const { data, error } = await mc.metrics.volume({ interval: 'day' })
47
+ if (error) { /*...*/ }
48
+
49
+ for (const bucket of data?.buckets.processed ?? []) {
50
+ console.log(bucket.periodStart, bucket.count)
51
+ }
52
+ ```
53
+
54
+ #### Volume Buckets
55
+
56
+ `data.buckets` has `processed`, `delivered`, `dropped`.
57
+
58
+ #### Engagement Buckets
59
+
60
+ `data.buckets` has `open`, `click`, `uniqueOpen`, `uniqueClick`, `openTrackingDelivered`,
61
+ `clickTrackingDelivered`, `uniqueOpenTrackingDelivered`, `uniqueClickTrackingDelivered`.
62
+
63
+ #### Performance Buckets
64
+
65
+ `data.buckets` has `processed`, `delivered`, `bounced`, `complained`.
66
+
67
+ #### Recipient Behaviour Buckets
68
+
69
+ `data.buckets` has `unsubscribed`, `unsubscribeDelivered`.
70
+
71
+ ### Sender Metrics
72
+
73
+ `mc.metrics.senders()` lists per-sender totals grouped either by campaign or sub-account.
74
+
75
+ ```ts
76
+ const { data, error } = await mc.metrics.senders(
77
+ 'campaigns', // required: 'campaigns' | 'sub-accounts'
78
+ {
79
+ startTime: '2026-04-01',
80
+ endTime: '2026-05-01',
81
+ limit: 50, // 1..1000, default 10
82
+ offset: 0,
83
+ sortOrder: 'desc' // 'asc' | 'desc', default desc (by processed + dropped)
84
+ }
85
+ )
86
+ if (error) { /*...*/ }
87
+
88
+ for (const sender of data?.senders ?? []) {
89
+ console.log(sender.name, sender.processed, sender.bounced)
90
+ }
91
+ ```
92
+
93
+ Senders with **zero traffic in the time range are omitted** from the response.
94
+
95
+ ### Usage
96
+
97
+ ```ts
98
+ const { data: usage, error } = await mc.metrics.usage()
99
+ if (error) { /*...*/ }
100
+
101
+ console.log(usage?.total, usage?.startDate, usage?.endDate)
102
+ ```
103
+
104
+ For a specific sub-account, use `mc.subAccounts.getUsage(handle)` instead. See
105
+ [sub-accounts](sub-accounts.md).
106
+
107
+ ### Common Patterns
108
+
109
+ - **Daily dashboard**: `mc.metrics.volume({ interval: 'day' })` plus
110
+ `mc.metrics.engagement({ interval: 'day' })` over the same range.
111
+ - **Campaign post-mortem**: pass `campaignId` on each call, then read totals and buckets.
112
+ - **Top senders by sub-account**: `mc.metrics.senders('sub-accounts', { sortOrder: 'desc' })`.
113
+ - **Billing reconciliation**: `mc.metrics.usage()` plus each sub-account's
114
+ `mc.subAccounts.getUsage(handle)` should sum within the parent's limit.
@@ -0,0 +1,91 @@
1
+ # SDK Overview
2
+
3
+ The `mailchannels-sdk` npm package exports a `MailChannels` class. Instantiate it once with
4
+ your API key and use the attached module properties for every operation.
5
+
6
+ ```ts
7
+ import { MailChannels } from 'mailchannels-sdk'
8
+
9
+ const mc = new MailChannels(process.env.MAILCHANNELS_API_KEY)
10
+
11
+ // Send email
12
+ const { data, error } = await mc.emails.queue({
13
+ from: 'sender@example.com',
14
+ to: 'recipient@example.net',
15
+ subject: 'Hello',
16
+ text: 'Plain text body.'
17
+ })
18
+ ```
19
+
20
+ ### Module Inventory
21
+
22
+ | Property | What it does |
23
+ | --- | --- |
24
+ | `mc.emails` | Send (`send`) and queue (`queue`) email. |
25
+ | `mc.domains` | Check domain auth posture; manage hosted DKIM keys via `mc.domains.dkim`. |
26
+ | `mc.webhooks` | Enroll, validate, inspect batches, verify signatures. |
27
+ | `mc.subAccounts` | Manage sub-accounts (multi-tenant). |
28
+ | `mc.metrics` | Volume, engagement, performance, recipient-behaviour, sender metrics. |
29
+ | `mc.suppressions` | List, create, delete suppression entries. |
30
+
31
+ ### Key Concept: Personalizations
32
+
33
+ A single `emails.send()` or `emails.queue()` call can produce **many individual messages**.
34
+ The `personalizations` array is the advanced form: each entry is one
35
+ fully-resolved outgoing message with its own `to` / `cc` / `bcc`, per-recipient template
36
+ variables, header overrides, and so on. The top-level `from` / `subject` / `html` / `text`
37
+ act as defaults that each personalization can override.
38
+
39
+ For simple sends the shorthand fields (`to`, `cc`, `bcc`, `html`, `text`) cover most cases
40
+ without needing to write out personalizations explicitly.
41
+
42
+ ### `send` vs `queue`
43
+
44
+ | Method | Server behaviour | When to use |
45
+ | --- | --- |-----------------------------------------------------------------------------------|
46
+ | `emails.send()` | Synchronous — full per-recipient processing in the request. Returns per-personalization `results` with `messageId` and `status`. Supports `dryRun`. | When you need the rendered output back immediately for small numbers of messages. |
47
+ | `emails.queue()` | Queued — per-recipient work runs in the background. Returns `requestId` and `queuedAt` immediately. | Most production sends: web handlers, background workers, high throughput. |
48
+
49
+ `emails.sendAsync()` is a deprecated alias for `queue()` — use `queue()` in new code.
50
+
51
+ ### Response Shape
52
+
53
+ Every SDK method returns one of two shapes:
54
+
55
+ ```ts
56
+ // Methods that return data:
57
+ type DataResponse<T> =
58
+ | { data: T; error: null }
59
+ | { data: null; error: ErrorResponse }
60
+
61
+ // Methods that confirm an operation:
62
+ type SuccessResponse = {
63
+ success: boolean
64
+ error: ErrorResponse | null
65
+ }
66
+ ```
67
+
68
+ Always check `error` before using `data` or `success`:
69
+
70
+ ```ts
71
+ const { data, error } = await mc.emails.queue({ ... })
72
+ if (error) {
73
+ console.error(error.message, error.type, error.statusCode)
74
+ return
75
+ }
76
+ console.log(data.requestId)
77
+ ```
78
+
79
+ ### Configuration
80
+
81
+ ```ts
82
+ const mc = new MailChannels('YOUR-API-KEY', {
83
+ baseUrl: 'https://api.mailchannels.net', // rarely needed
84
+ timeout: 120000, // ms, default 120s; false to disable
85
+ retry: false, // ofetch retry options
86
+ signal: controller.signal // AbortSignal for cancellation
87
+ })
88
+ ```
89
+
90
+ Missing API key throws synchronously at construction time. All
91
+ other errors surface as `error` in the returned result object.
@@ -0,0 +1,221 @@
1
+ # Sending Email
2
+
3
+ Use `mc.emails.queue()` as the default. Switch to `mc.emails.send()` only when you
4
+ specifically need immediate results for a small number of messages, or the dry-run preview.
5
+
6
+ ### Quickest Send
7
+
8
+ ```ts
9
+ import { MailChannels } from 'mailchannels-sdk'
10
+
11
+ const mc = new MailChannels('YOUR-API-KEY')
12
+
13
+ const { data, error } = await mc.emails.queue({
14
+ from: 'sender@example.com',
15
+ to: 'recipient@example.net',
16
+ subject: 'Hello',
17
+ text: 'Plain text body.',
18
+ html: '<p>HTML body.</p>'
19
+ })
20
+
21
+ if (error) throw new Error(error.message)
22
+ console.log(data.requestId)
23
+ ```
24
+
25
+ Provide both `text` and `html` whenever possible — receiving clients prefer the last
26
+ matching content type and a plain-text fallback improves deliverability.
27
+
28
+ ### Recipient Formats
29
+
30
+ All recipient fields (`to`, `from`, `cc`, `bcc`, `replyTo`, `envelopeFrom`) accept multiple formats
31
+ interchangeably. Note: `from`, `replyTo`, `envelopeFrom` must be single
32
+ recipient values (a string or an object) and do NOT accept array:
33
+
34
+ ```ts
35
+ const r1 = 'recipient@example.net' // string
36
+ const r2 = 'Jane Smith <recipient@example.net>' // display-name string
37
+ const r3 = { email: 'recipient@example.net', name: 'Jane Smith' } // object
38
+ const r4 = ['a@example.net', { email: 'b@example.net', name: 'Bob' }] // array
39
+ ```
40
+
41
+ ### Per-Recipient Personalization
42
+
43
+ `personalizations` is the advanced form — each entry is one fully-rendered outgoing message
44
+ with optional per-recipient overrides:
45
+
46
+ ```ts
47
+ const { data, error } = await mc.emails.queue({
48
+ from: 'sender@example.com',
49
+ subject: 'Your update',
50
+ text: 'Hello!',
51
+ personalizations: [
52
+ {
53
+ to: 'alice@example.net',
54
+ subject: "Alice's update" // overrides root subject
55
+ },
56
+ {
57
+ to: 'bob@example.net' // inherits root subject
58
+ }
59
+ ]
60
+ })
61
+ ```
62
+
63
+ Per-personalization overridable fields: `to`, `cc`, `bcc`, `from`, `subject`, `replyTo`,
64
+ `envelopeFrom`, `headers`, `dkim`, `template.data`.
65
+
66
+ ### Body Content Fields
67
+
68
+ At least one of `html`, `text`, or `content` is required. The shorthand fields are the
69
+ simplest form; `content` is the explicit array form for more control.
70
+
71
+ | Field | Type | When to use |
72
+ | --- | --- | --- |
73
+ | `html` | `string` | HTML body. Shorthand for a `text/html` content part. |
74
+ | `text` | `string` | Plain text body. Shorthand for a `text/plain` content part. |
75
+ | `content` | `EmailsSendContent[]` | Explicit list of `{ type, value }` parts. Required when using non-standard MIME types or when you need strict ordering. |
76
+
77
+ Mixing rules: you cannot include a `text/html` entry in `content` when `html` is also set,
78
+ and cannot include `text/plain` when `text` is also set.
79
+
80
+ ```ts
81
+ // Shorthand — simplest form for most sends
82
+ const { data, error } = await mc.emails.queue({
83
+ from: 'sender@example.com',
84
+ to: 'recipient@example.net',
85
+ subject: 'Hello',
86
+ text: 'Plain text fallback.',
87
+ html: '<p>HTML body.</p>'
88
+ })
89
+
90
+ // Explicit content array — use when you need a non-standard MIME type
91
+ // or want precise control over part ordering
92
+ const { data: data1, error: err1 } = await mc.emails.queue({
93
+ from: 'sender@example.com',
94
+ to: 'recipient@example.net',
95
+ subject: 'Hello',
96
+ content: [
97
+ { type: 'text/plain', value: 'Plain text fallback.' },
98
+ { type: 'text/html', value: '<p>HTML body.</p>' }
99
+ ]
100
+ })
101
+
102
+ // Mixed — shorthand + extra content parts (no type collision)
103
+ const { data: data2, error: err2 } = await mc.emails.queue({
104
+ from: 'sender@example.com',
105
+ to: 'recipient@example.net',
106
+ subject: 'Hello',
107
+ text: 'Plain text fallback.',
108
+ html: '<p>HTML body.</p>',
109
+ content: [
110
+ { type: 'text/calendar', value: iCalString } // additional part; no text/plain or text/html here
111
+ ]
112
+ })
113
+ ```
114
+
115
+ ### Dry Run (send endpoint only)
116
+
117
+ `emails.send()` supports `dryRun: true`. The API validates and renders the message without
118
+ delivering it. Useful for asserting templates render before launch:
119
+
120
+ ```ts
121
+ const { data, error } = await mc.emails.send(
122
+ {
123
+ from: 'sender@example.com',
124
+ to: 'recipient@example.net',
125
+ subject: 'Preview {{name}}',
126
+ text: 'Hello {{name}}',
127
+ template: { type: 'mustache', data: { name: 'World' } }
128
+ },
129
+ true // dryRun
130
+ )
131
+ if (error) { /* ... */ }
132
+
133
+ // data.rendered is a string[] — one rendered message per personalization
134
+ console.log(data?.rendered?.[0])
135
+ ```
136
+
137
+ `emails.queue()` does **not** support dry-run.
138
+
139
+ ### When To Pick Which
140
+
141
+ | Situation | Method |
142
+ |------------------------------------------------------------------| --- |
143
+ | Web request handler, background worker, high throughput | `mc.emails.queue()` |
144
+ | Sending to many recipients in one call | `mc.emails.queue()` |
145
+ | Need the rendered message for inspection | `mc.emails.send(options, true)` |
146
+ | Need `status` results immediately for a small number of messages | `mc.emails.send()` |
147
+
148
+ ### send() Response Details
149
+
150
+ ```ts
151
+ const { data, error } = await mc.emails.send({ ... })
152
+ if (error) { /* ... */ }
153
+
154
+ data?.results?.forEach(r => {
155
+ console.log(r.messageId, r.status, r.reason)
156
+ // r.status is 'sent' or 'failed'
157
+ // 'sent' is a temporary status; final outcome arrives via webhooks
158
+ })
159
+ ```
160
+
161
+ ### Common Pitfalls
162
+
163
+ - **Reserved headers**: don't set `From`, `To`, `Subject`, `Reply-To`, `Message-ID`,
164
+ `Content-Type`, `DKIM-Signature`, etc. in `headers`. Use the payload fields instead.
165
+ See [custom-headers](custom-headers.md).
166
+ - **Payload size limit**: 30 MB total (headers + body + attachments). The API returns
167
+ `payload_too_large_error` with `statusCode: 413`.
168
+ - **Per-personalization limits**: up to 1000 `to` recipients per personalization, 1000
169
+ personalizations per call, 1000 attachments per call.
170
+ - **Non-transactional sends** require exactly one recipient per personalization and DKIM
171
+ signing. See [unsubscribe](unsubscribe.md).
172
+ - **`campaignId`**: ≤ 48 UTF-8 characters, no spaces. Validated before the request leaves
173
+ the client.
174
+ - **Click tracking** only rewrites `<a>` tags whose URL is non-empty, starts with
175
+ `http`/`https`, and does not have `clicktracking="off"`.
176
+ - **No idempotency keys**: retrying after a transient failure can duplicate sends. Build
177
+ idempotency into the caller (e.g. a unique `campaignId` + recipient-set check) when
178
+ at-most-once delivery matters.
179
+
180
+ ### Tracking
181
+
182
+ ```ts
183
+ const { data, error } = await mc.emails.queue({
184
+ from: 'sender@example.com',
185
+ to: 'recipient@example.net',
186
+ subject: 'Tracked',
187
+ html: '<p>Hello <a href="https://example.com">click here</a></p>',
188
+ tracking: {
189
+ open: { enable: true },
190
+ click: { enable: true }
191
+ }
192
+ })
193
+ ```
194
+
195
+ Open and click tracking require a subscription that supports them.
196
+
197
+ ### Custom Tracking Domains
198
+
199
+ Pass `customDomainName` on the `tracking` field (or on `unsubscribe`) to route those
200
+ links through a domain you registered instead of the shared MailChannels domain.
201
+ The name must match an **active** custom tracking domain registered for that
202
+ scope — see [custom-tracking-domains](custom-tracking-domains.md) for
203
+ registration and DNS verification.
204
+
205
+ ```ts
206
+ const { data, error } = await mc.emails.queue({
207
+ from: 'sender@example.com',
208
+ to: 'recipient@example.net',
209
+ subject: 'Tracked with a custom domain',
210
+ html: "<p>Hello <a href='https://example.com'>click here</a></p>",
211
+ tracking: {
212
+ click: {
213
+ enable: true,
214
+ customDomainName: 'newsletter-clicks'
215
+ }
216
+ },
217
+ unsubscribe: {
218
+ customDomainName: 'newsletter-unsubscribe'
219
+ }
220
+ })
221
+ ```
@@ -0,0 +1,129 @@
1
+ # Sub-Accounts
2
+
3
+ Sub-accounts are first-class on MailChannels. Use them for tenants, customers, or isolated
4
+ senders so that one customer's reputation, limits, and bad traffic don't contaminate the
5
+ parent account or other tenants.
6
+
7
+ > Sub-accounts are only available on parent accounts on the 100K and higher plans.
8
+
9
+ ### Handles
10
+
11
+ A handle uniquely identifies a sub-account. Rules:
12
+
13
+ - 3–128 characters.
14
+ - **Lowercase alphanumeric only.** No hyphens, underscores, or uppercase.
15
+ - Unique per parent account.
16
+ - If omitted on create, a random handle is generated.
17
+
18
+ ### Lifecycle
19
+
20
+ ```ts
21
+ import { MailChannels } from 'mailchannels-sdk'
22
+
23
+ const mc = new MailChannels('PARENT-ACCOUNT-API-KEY')
24
+
25
+ // Create
26
+ const { data: sub, error: createError } = await mc.subAccounts.create('Client A', 'clienta')
27
+ if (createError) { /*...*/ }
28
+
29
+ // List (paginated; default limit 1000)
30
+ const { data: page, error: listErr } = await mc.subAccounts.list({ limit: 100, offset: 0 })
31
+ if (listErr) { /*...*/ }
32
+
33
+
34
+ // Suspend / activate
35
+ const { error: suspendErr } = await mc.subAccounts.suspend('clienta')
36
+ if (suspendErr) { /*...*/ }
37
+
38
+ const { error: activateErr } = await mc.subAccounts.activate('clienta')
39
+ if (activateErr) { /*...*/ }
40
+
41
+ // Delete
42
+ const { error: deleteError } = await mc.subAccounts.delete('clienta')
43
+ if (deleteError) { /*...*/ }
44
+ ```
45
+
46
+ ### Credentials
47
+
48
+ Each sub-account has its own API keys and SMTP passwords.
49
+
50
+ ```ts
51
+ // API keys
52
+ const { data: createdKey, error: createErr } = await mc.subAccounts.apiKeys.create('clienta')
53
+ if (createErr) { /*...*/ }
54
+ // Store createdKey.key immediately — only returned once
55
+
56
+ const { data: keys, error: listErr } = await mc.subAccounts.apiKeys.list('clienta')
57
+ if (listErr) { /*...*/ }
58
+
59
+ const { error: deleteErr } = await mc.subAccounts.apiKeys.delete('clienta', storedKeyId)
60
+ if (deleteErr) { /*...*/ }
61
+
62
+ // SMTP passwords
63
+ const { data: createdPwd, error: createPwdErr } = await mc.subAccounts.smtpPasswords.create('clienta')
64
+ if (createPwdErr) { /*...*/ }
65
+ // Store createdPwd.smtpPassword immediately — only returned once
66
+
67
+ const { data: passwords, error: listPwdErr } = await mc.subAccounts.smtpPasswords.list('clienta')
68
+ if (listPwdErr) { /*...*/ }
69
+
70
+ const { error: deletePwdErr } = await mc.subAccounts.smtpPasswords.delete('clienta', storedPasswordId)
71
+ if (deletePwdErr) { /*...*/ }
72
+ ```
73
+
74
+ **Listed keys and passwords are redacted.** The full secret is only returned once, at
75
+ create time. Store it immediately or rotate. Each sub-account has server-side caps on how
76
+ many API keys and SMTP passwords it can hold — once at the cap, create returns
77
+ `unprocessable_entity_error`; delete an unused credential first.
78
+
79
+ ### Limits
80
+
81
+ Per-sub-account monthly send caps. A sub-account without a limit inherits the parent's
82
+ capacity.
83
+
84
+ ```ts
85
+ const { error: setLimitErr } = await mc.subAccounts.limits.set('clienta', { sends: 100_000 })
86
+ if (setLimitErr) { /*...*/ }
87
+
88
+ const { data: limit, error: getLimitErr } = await mc.subAccounts.limits.get('clienta')
89
+ if (getLimitErr) { /*...*/ }
90
+
91
+ const { error: deleteLimitErr } = await mc.subAccounts.limits.delete('clienta') // back to inheriting parent
92
+ if (deleteLimitErr) { /*...*/ }
93
+ ```
94
+
95
+ ### Usage
96
+
97
+ ```ts
98
+ const { data: parentUsage } = await mc.metrics.usage()
99
+ const { data: subUsage } = await mc.subAccounts.getUsage('clienta')
100
+
101
+ console.log(parentUsage?.total, parentUsage?.startDate, parentUsage?.endDate)
102
+ ```
103
+
104
+ `mc.metrics.usage()` is for the parent account. Use `mc.subAccounts.getUsage(handle)` for
105
+ one specific sub-account.
106
+
107
+ ### Sending As A Sub-Account
108
+
109
+ Create a separate `MailChannels` instance with the sub-account's API key:
110
+
111
+ ```ts
112
+ const subClient = new MailChannels('SUB-ACCOUNT-API-KEY')
113
+
114
+ const { error } = await subClient.emails.queue({
115
+ from: 'sender@client.example',
116
+ to: 'recipient@example.net',
117
+ subject: 'From a tenant',
118
+ text: 'Hello'
119
+ })
120
+ if (error) { /*...*/ }
121
+ ```
122
+
123
+ This keeps the account boundary explicit in code and avoids hard-to-debug issues where the
124
+ wrong key is used at the wrong call site.
125
+
126
+ ### Suppressions And Sub-Accounts
127
+
128
+ When creating suppressions on the parent, set `addToSubAccounts: true` to also copy entries
129
+ into every sub-account. See [suppressions](suppressions.md).