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.
- package/.agents/skills/mailchannels-js/SKILL.md +91 -0
- package/.agents/skills/mailchannels-js/resources/attachments.md +113 -0
- package/.agents/skills/mailchannels-js/resources/clients-and-transport.md +98 -0
- package/.agents/skills/mailchannels-js/resources/custom-headers.md +64 -0
- package/.agents/skills/mailchannels-js/resources/custom-tracking-domains.md +83 -0
- package/.agents/skills/mailchannels-js/resources/dkim.md +148 -0
- package/.agents/skills/mailchannels-js/resources/domain-checks.md +100 -0
- package/.agents/skills/mailchannels-js/resources/error-handling.md +143 -0
- package/.agents/skills/mailchannels-js/resources/metrics-and-usage.md +114 -0
- package/.agents/skills/mailchannels-js/resources/overview.md +91 -0
- package/.agents/skills/mailchannels-js/resources/sending.md +221 -0
- package/.agents/skills/mailchannels-js/resources/sub-accounts.md +129 -0
- package/.agents/skills/mailchannels-js/resources/suppressions.md +91 -0
- package/.agents/skills/mailchannels-js/resources/templates.md +97 -0
- package/.agents/skills/mailchannels-js/resources/testing.md +95 -0
- package/.agents/skills/mailchannels-js/resources/unsubscribe.md +73 -0
- package/.agents/skills/mailchannels-js/resources/webhooks.md +174 -0
- package/README.md +70 -4
- package/dist/_chunks/simulator.mjs +111 -23
- package/dist/mailchannels.d.mts +853 -561
- package/dist/mailchannels.mjs +861 -457
- package/package.json +10 -9
|
@@ -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).
|