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,91 @@
1
+ ---
2
+ name: mailchannels-js
3
+ description: JavaScript/TypeScript SDK for the MailChannels Email API (npm `mailchannels-sdk`; `import { MailChannels } from 'mailchannels-sdk'`). Use when sending or queueing email, working with attachments, mustache templates, unsubscribe, custom headers, DKIM keys, sub-accounts, domain checks, metrics, usage, suppressions, or webhooks (incl. signature verification); also configuring clients, testing with the built-in simulator, and error handling. Skip when (a) developing the SDK itself — defer to the repo's root AGENTS.md; (b) the user is in a non-JavaScript/TypeScript language — MailChannels publishes separate SDKs for other ecosystems and the JS types/patterns here don't apply.
4
+ ---
5
+
6
+ # MailChannels JavaScript SDK
7
+
8
+ This skill helps you write **JavaScript/TypeScript** code that uses the `mailchannels-sdk`
9
+ package (npm: `mailchannels-sdk`, importable as `import { MailChannels } from 'mailchannels-sdk'`).
10
+ It is specific to the official JavaScript SDK — MailChannels publishes separate SDKs for
11
+ other languages and their APIs differ.
12
+
13
+ > **Scope.** This skill is for *consuming* the JS SDK from application code. Working **on**
14
+ > the SDK (adding features, fixing bugs, releasing the package) is a different job.
15
+ > If the user is working in a Python, Go, Ruby, PHP, Rust, or shell context, this skill does not apply.
16
+
17
+ ## How To Use This Skill
18
+
19
+ The body of each topic lives in [`resources/`](resources/). Read this file for context and
20
+ the decision tree, then load only the resource files that match the task. Don't preload
21
+ everything.
22
+
23
+ ### Decision Tree
24
+
25
+ Start at the top; descend until you hit a leaf, then read the linked resource. If multiple
26
+ branches apply (e.g. "send + attachments + templates"), read each leaf.
27
+
28
+ ```
29
+ You're about to write JS/TS SDK code. What does it need to do?
30
+
31
+ ├── Get oriented — what is the SDK, what are the entry points?
32
+ │ → resources/overview.md
33
+
34
+ ├── Set up the client (API keys, base URL, options, lifecycle)
35
+ │ → resources/clients-and-transport.md
36
+
37
+ ├── Send email
38
+ │ → resources/sending.md
39
+ │ ├── …with file / bytes / URL / inline-image attachments?
40
+ │ │ → resources/attachments.md
41
+ │ ├── …with a mustache template + per-recipient data?
42
+ │ │ → resources/templates.md
43
+ │ ├── …with an unsubscribe link or List-Unsubscribe header?
44
+ │ │ → resources/unsubscribe.md
45
+ │ └── …with custom X-… headers?
46
+ │ → resources/custom-headers.md
47
+
48
+ ├── Manage hosted DKIM keys (create / list / rotate / revoke)
49
+ │ → resources/dkim.md
50
+
51
+ ├── Validate a sender domain's auth posture (DKIM/SPF/Lockdown/DNS)
52
+ │ → resources/domain-checks.md
53
+
54
+ ├── Multi-tenant work — sub-accounts, their credentials, limits, usage
55
+ │ → resources/sub-accounts.md
56
+
57
+ ├── Query analytics (volume, engagement, performance, senders) or
58
+ │ current-period usage
59
+ │ → resources/metrics-and-usage.md
60
+
61
+ ├── Manage the suppression list (list, create, delete)
62
+ │ → resources/suppressions.md
63
+
64
+ ├── Receive or manage delivery-event webhooks (enroll, validate,
65
+ │ inspect batches, verify signatures)
66
+ │ → resources/webhooks.md
67
+
68
+ ├── Catch / log / retry SDK or API errors
69
+ │ → resources/error-handling.md
70
+
71
+ └── Write tests without hitting the real API
72
+ → resources/testing.md
73
+ ```
74
+
75
+ ## Style Conventions In The Resources
76
+
77
+ - The example code in the resources are largely compatible with both JavaScript and TypeScript.
78
+ Double check before copying that you don't need to strip type annotations or alter imports.
79
+ - The JS SDK uses a **result-based** error style: every method returns `{ data, error }`
80
+ (`DataResponse<T>`) or `{ success, error }` (`SuccessResponse`). Check `error` before
81
+ using `data`. The SDK never throws for API errors — only for missing API key at
82
+ construction time.
83
+ - All methods are `async` and return Promises. There are no sync variants.
84
+ - All recipient and sender addresses use the IANA-reserved `example.com` / `example.net`
85
+ domains. Replace before running.
86
+
87
+ ## Beyond This Skill
88
+
89
+ When a resource leaves something ambiguous, the SDK source itself is the final authority.
90
+ The published `mailchannels-sdk` package on npm also ships with its own README and TypeScript
91
+ type definitions — hover-docs in your editor are authoritative for parameter shapes.
@@ -0,0 +1,113 @@
1
+ # Attachments
2
+
3
+ MailChannels expects every attachment to be Base64-encoded with a `filename` and ideally a
4
+ MIME `type`. Use the `Attachment` static helpers — they encode for you and infer the MIME
5
+ type from the filename.
6
+
7
+ ### From In-Memory Bytes
8
+
9
+ `fromBytes` is synchronous and accepts any `ArrayBuffer` or `Uint8Array`:
10
+
11
+ ```ts
12
+ import { MailChannels, Attachment } from 'mailchannels-sdk'
13
+
14
+ const mc = new MailChannels('YOUR-API-KEY')
15
+
16
+ const report = Attachment.fromBytes(
17
+ new Uint8Array([...csvBytes]),
18
+ {
19
+ filename: 'report.csv',
20
+ type: 'text/csv' // optional; inferred from filename when omitted
21
+ }
22
+ )
23
+
24
+ // Also accepts ArrayBuffer:
25
+ const pdf = Attachment.fromBytes(arrayBuffer, { filename: 'doc.pdf' })
26
+
27
+ const { data, error } = await mc.emails.queue({
28
+ from: 'billing@example.com',
29
+ to: 'recipient@example.net',
30
+ subject: 'Your invoice',
31
+ text: 'See attached.',
32
+ attachments: [report, pdf]
33
+ })
34
+ ```
35
+
36
+ ### From A Blob
37
+
38
+ `fromBlob` is async and accepts any Web API `Blob`. The Blob's own `type` property is used
39
+ as the MIME type unless you override it in options:
40
+
41
+ ```ts
42
+ const blob = new Blob(['Hello world'], { type: 'text/plain' })
43
+ const attachment = await Attachment.fromBlob(blob, { filename: 'hello.txt' })
44
+ ```
45
+
46
+ ### Inline Images (CID References)
47
+
48
+ Pass `disposition: 'inline'` and a `contentId` to embed an image inside the HTML body:
49
+
50
+ ```ts
51
+ const logo = Attachment.fromBytes(bytes, {
52
+ filename: 'logo.png',
53
+ disposition: 'inline',
54
+ contentId: 'company-logo'
55
+ })
56
+
57
+ const { data, error } = await mc.emails.queue({
58
+ from: 'sender@example.com',
59
+ to: 'recipient@example.net',
60
+ subject: 'Inline image',
61
+ html: "<img src='cid:company-logo' alt='Company logo'>",
62
+ attachments: [logo]
63
+ })
64
+ ```
65
+
66
+ ### Attachment Options
67
+
68
+ Both `fromBytes` and `fromBlob` accept an `AttachmentOptions` object:
69
+
70
+ | Field | Type | Notes |
71
+ | --- | --- | --- |
72
+ | `filename` | `string` | Required. MIME type is inferred from it when `type` is omitted. |
73
+ | `type` | `string` | MIME type. Inferred from `filename` if omitted. For `fromBlob`, defaults to the Blob's own `type`. |
74
+ | `contentId` | `string` | For `cid:` inline image references. |
75
+ | `disposition` | `'attachment' \| 'inline'` | Defaults to `'attachment'`. |
76
+
77
+ ### Awaiting Attachments Lazily
78
+
79
+ The `attachments` field accepts `(EmailsSendAttachment | Promise<EmailsSendAttachment>)[]`,
80
+ so you can pass unresolved promises and the SDK will await them before sending:
81
+
82
+ ```ts
83
+ const { data, error } = await mc.emails.queue({
84
+ from: 'sender@example.com',
85
+ to: 'recipient@example.net',
86
+ subject: 'Reports',
87
+ text: 'Reports attached.',
88
+ attachments: [
89
+ Attachment.fromBlob(blob1, { filename: 'q1.pdf' }), // Promise<...>
90
+ Attachment.fromBlob(blob2, { filename: 'q2.pdf' }) // Promise<...>
91
+ ]
92
+ })
93
+ ```
94
+
95
+ ### Limits
96
+
97
+ - Up to **1000 attachments** per send.
98
+ - Combined message + headers + attachments must stay under **30 MB**. Exceeding this
99
+ returns `payload_too_large_error` (HTTP 413) in the `error` field.
100
+
101
+ ### Raw Attachment Shape
102
+
103
+ If you need to construct one by hand:
104
+
105
+ ```ts
106
+ const attachment: EmailsSendAttachment = {
107
+ content: Buffer.from(new Uint8Array(bytes)).toString("base64"),
108
+ filename: 'report.pdf',
109
+ type: 'application/pdf' // recommended but optional
110
+ }
111
+ ```
112
+
113
+ `content` and `filename` are required. `type` is recommended.
@@ -0,0 +1,98 @@
1
+ # Clients and Transport
2
+
3
+ The JS SDK requires explicit client instantiation — there is no module-level singleton or
4
+ environment-variable auto-configuration. Create one `MailChannels` instance per credential
5
+ set and reuse it for the lifetime of the process.
6
+
7
+ ```ts
8
+ import { MailChannels } from 'mailchannels-sdk'
9
+ import process from 'node:process'
10
+
11
+ const mc = new MailChannels(process.env.MAILCHANNELS_API_KEY)
12
+ ```
13
+
14
+ A missing or empty API key throws synchronously at construction:
15
+
16
+ ```ts
17
+ new MailChannels('') // throws Error: "Missing MailChannels API key."
18
+ ```
19
+
20
+ ### Constructor Options
21
+
22
+ ```ts
23
+ // new MailChannels(apiKey: string, options?: MailChannelsClientOptions)
24
+ ```
25
+
26
+ | Option | Type | Default | Notes |
27
+ | --- | --- | --- | --- |
28
+ | `baseUrl` | `string` | `https://api.mailchannels.net` | Only override if MailChannels gives you a specific alternate endpoint (sandbox, regional URL). |
29
+ | `timeout` | `number \| false` | `120000` (ms) | Pass `false` to disable the timeout entirely. |
30
+ | `retry` | `boolean \| number \| RetryOptions` | `false` | `ofetch` retry options. |
31
+ | `signal` | `AbortSignal` | `undefined` | Propagated to every fetch call made by this instance. |
32
+
33
+ ### Multi-Tenant: One Client Per Credential
34
+
35
+ Never share a parent key and a sub-account key on the same instance. Create a separate
36
+ `MailChannels` for each credential:
37
+
38
+ ```ts
39
+ const parent = new MailChannels(process.env.PARENT_API_KEY)
40
+ const tenantA = new MailChannels(process.env.TENANT_A_API_KEY)
41
+ const tenantB = new MailChannels(process.env.TENANT_B_API_KEY)
42
+
43
+ const { data: accounts, error: listErr } = await parent.subAccounts.list()
44
+ if (listErr) { /*...*/ }
45
+
46
+ const { data: queueA, error: queueErrA } = await tenantA.emails.queue({ ... })
47
+ if (queueErrA) { /*...*/ }
48
+
49
+ const { data: queueB, error: queueErrB } = await tenantB.emails.queue({ ... })
50
+ if (queueErrB) { /*...*/ }
51
+ ```
52
+
53
+ ### Request Cancellation
54
+
55
+ Pass an `AbortSignal` to cancel in-flight requests:
56
+
57
+ ```ts
58
+ const controller = new AbortController()
59
+
60
+ const mc = new MailChannels(apiKey, { signal: controller.signal })
61
+
62
+ // Later:
63
+ controller.abort()
64
+ ```
65
+
66
+ ### Headers Sent On Every Request
67
+
68
+ The SDK sets these on every request and they cannot be overridden via `MailChannelsClientOptions`:
69
+
70
+ - `X-API-Key: <your key>`
71
+ - `Accept: application/json`
72
+ - `Content-Type: application/json`
73
+ - `User-Agent: mailchannels-node/<version>`
74
+
75
+ ### Extending The Client
76
+
77
+ The `MailChannels` class extends the public `MailChannelsClient` base. If you need to
78
+ intercept, proxy, or instrument requests you can subclass `MailChannelsClient` and override
79
+ `_fetch<T>()` (a `protected` method).
80
+
81
+ ```ts
82
+ import { MailChannelsClient } from 'mailchannels-sdk'
83
+
84
+ class InstrumentedClient extends MailChannelsClient {
85
+ protected override async _fetch<T>(path: string, options: FetchOptions<'json'>) {
86
+ const start = performance.now()
87
+ try {
88
+ const result = await super._fetch<T>(path, options)
89
+ recordLatency(path, performance.now() - start)
90
+ return result
91
+ }
92
+ catch (err) {
93
+ recordError(path)
94
+ throw err
95
+ }
96
+ }
97
+ }
98
+ ```
@@ -0,0 +1,64 @@
1
+ # Custom Headers
2
+
3
+ Use `headers` for application-specific metadata, tracking IDs, campaign tags, or any
4
+ custom `X-…` header.
5
+
6
+ ### Root vs Per-Personalization
7
+
8
+ - **Root `headers`**: applied to every personalization.
9
+ - **Per-personalization `headers`**: applied only to that personalization.
10
+ - **If the same header appears in both, the personalization value wins.**
11
+
12
+ ```ts
13
+ const { data, error } = await mc.emails.queue({
14
+ from: 'sender@example.com',
15
+ subject: 'Hello',
16
+ text: 'Hello',
17
+ headers: {
18
+ 'X-Campaign-ID': 'newsletter-2026-05',
19
+ 'X-Project': 'growth'
20
+ },
21
+ personalizations: [
22
+ {
23
+ to: 'alice@example.net',
24
+ headers: { 'X-Tier': 'pro' }
25
+ },
26
+ {
27
+ to: 'bob@example.net',
28
+ headers: { 'X-Tier': 'free', 'X-Campaign-ID': 'growth-override' }
29
+ }
30
+ ]
31
+ })
32
+ ```
33
+
34
+ ### Reserved Headers — Never Set These
35
+
36
+ MailChannels controls a fixed set of message headers. The SDK pre-validates and returns a
37
+ `validation_error` client-side before any request that tries to set one. Header names are
38
+ checked case-insensitively, so `From`, `from`, and `FROM` are all rejected.
39
+
40
+ | Reserved header | Set via payload field |
41
+ | --- | --- |
42
+ | `From` | `from` |
43
+ | `To` | `personalizations[].to` |
44
+ | `CC` | `personalizations[].cc` or root `cc` |
45
+ | `BCC` | `personalizations[].bcc` or root `bcc` |
46
+ | `Subject` | `subject` (root) or `personalizations[].subject` |
47
+ | `Reply-To` | `replyTo` (root) or `personalizations[].replyTo` |
48
+ | `DKIM-Signature` | `dkim.domain` / `dkim.selector` / `dkim.privateKey` |
49
+ | `Content-Type`, `Content-Transfer-Encoding` | `content[].type` and encoding the API chooses |
50
+ | `Message-ID` | Returned in `results[].messageId` |
51
+ | `Authentication-Results`, `Received` | Generated by the API |
52
+
53
+ This list mirrors the SDK's `RESERVED_HEADER_NAMES` set — keep them in sync if MailChannels
54
+ changes the list upstream.
55
+
56
+ ### Case Sensitivity
57
+
58
+ Header names are treated case-insensitively by MailChannels. If two keys differ only by
59
+ case, only one is used and the choice is unspecified. Pick one canonical casing per header.
60
+
61
+ ### Values
62
+
63
+ Both keys and values must be strings. A non-string value returns a `validation_error`
64
+ before the request leaves the client.
@@ -0,0 +1,83 @@
1
+ # Custom Tracking Domains
2
+
3
+ Register your own hostname for click tracking, open tracking, or unsubscribe
4
+ links instead of MailChannels' shared domains. Once a domain is `active`,
5
+ select it by `name` via `customDomainName` on the send payload — see
6
+ [sending.md](sending.md#custom-tracking-domains).
7
+
8
+ ## Register A Domain
9
+
10
+ ```ts
11
+ import { MailChannels } from 'mailchannels-sdk'
12
+
13
+ const mc = new MailChannels('YOUR-API-KEY')
14
+
15
+ const { data, error } = await mc.domains.customTracking.create(
16
+ 'newsletter-clicks', // label used at send time, ^[a-z0-9-]+$, max 64
17
+ 'click.example.com',
18
+ 'click' // click | open | unsubscribe
19
+ )
20
+ ```
21
+
22
+ Before this can complete, `hostname` needs a CNAME record pointing to
23
+ `links.mailchannels.net`.
24
+
25
+ ## DNS Verification Is A Two-Step, Non-Error Flow
26
+
27
+ `create()` and `update()` (when re-activating a disabled domain) return one
28
+ of two shapes in `data` — both are 2xx responses, not errors:
29
+
30
+ - **Registered**: `dnsSetupRequired: false` with `name`, `hostname`, `scope`,
31
+ `status`, `createdAt` are all present..
32
+ - **Pending**: `dnsSetupRequired: true` with `token`, `txtRecordName`,
33
+ `txtRecordValue`, `instructions` is present (a CNAME-only failure can omit the TXT
34
+ fields; a fresh registration includes all of them).
35
+
36
+ Add the TXT record alongside the CNAME, wait for propagation, then call
37
+ `create()` again with the same `name`/`hostname`/`scope`.
38
+
39
+ If DNS still isn't in place on retry, MailChannels responds `422` instead of
40
+ `202` — that's a real error status, so the SDK returns an `error` object.
41
+ The same pending-verification body is on `error.response`:
42
+
43
+ ```ts
44
+ const { data, error } = await mc.domains.customTracking.create(
45
+ 'newsletter-clicks',
46
+ 'click.example.com',
47
+ 'click'
48
+ )
49
+
50
+ if (error) {
51
+ if (error.statusCode === 422 && error.response) {
52
+ console.log(error.response.instructions)
53
+ }
54
+ return;
55
+ }
56
+ ```
57
+
58
+ ## List, Update, Delete
59
+
60
+ ```ts
61
+ // List
62
+ const { data: listData, error: listError } = await mc.domains.customTracking.list({
63
+ scope: 'click', // optional filter
64
+ status: 'active', // optional filter: active | disabled
65
+ limit: 100,
66
+ offset: 0
67
+ })
68
+
69
+ // Update
70
+ const { data: updateData, error: updateError } = await mc.domains.customTracking.update(
71
+ 'click.example.com',
72
+ 'click',
73
+ { status: 'disabled' } // or { name: 'new-label' } to rename
74
+ )
75
+
76
+ // Delete
77
+ const { success, error: deleteError } = await mc.domains.customTracking.delete('click.example.com', 'click')
78
+ ```
79
+
80
+ Deleting a domain takes effect immediately — tracking links and unsubscribe
81
+ URLs already sent using that domain stop working right away. Re-activating a
82
+ domain whose DNS verification has since lapsed goes through the same
83
+ pending-result flow as `create()`.
@@ -0,0 +1,148 @@
1
+ # DKIM
2
+
3
+ MailChannels can host the **private** DKIM key for you. You publish the **public** key in
4
+ the domain's DNS as a TXT record. The public DNS record is **not** hosted by MailChannels —
5
+ you must publish it yourself in whatever DNS provider holds the zone.
6
+
7
+ ### Create A Hosted Key
8
+
9
+ ```ts
10
+ import { MailChannels } from 'mailchannels-sdk'
11
+
12
+ const mc = new MailChannels('YOUR-API-KEY')
13
+
14
+ const { data, error } = await mc.domains.dkim.create('example.com', {
15
+ selector: 'mcdkim',
16
+ algorithm: 'rsa', // only 'rsa' is currently supported
17
+ length: 2048 // 1024 or 2048; default 2048; multiples of 1024
18
+ })
19
+
20
+ if (error) throw new Error(error.message)
21
+
22
+ for (const record of data?.dnsRecords ?? []) {
23
+ console.log(record.name, record.type, record.value)
24
+ // e.g. mcdkim._domainkey.example.com TXT "v=DKIM1; k=rsa; p=MIIBIj..."
25
+ }
26
+ ```
27
+
28
+ The returned `data.dnsRecords` contains TXT records you must publish in DNS.
29
+
30
+ ### List, Filter, And Include DNS Records
31
+
32
+ ```ts
33
+ const { data, error } = await mc.domains.dkim.list('example.com', {
34
+ selector: 'mcdkim', // optional; returns at most one
35
+ status: 'active', // 'active' | 'retired' | 'revoked' | 'rotated'
36
+ includeDnsRecord: true, // include suggested DNS record per key
37
+ limit: 10,
38
+ offset: 0
39
+ })
40
+ ```
41
+
42
+ ### Key Lifecycle
43
+
44
+ | Status | Meaning |
45
+ | --- | --- |
46
+ | `active` | Currently used for signing. |
47
+ | `rotated` | Being rotated out. Still valid for signing for a 3-day grace period; auto-changes to `retired` 2 weeks after rotation. |
48
+ | `retired` | No longer in use. |
49
+ | `revoked` | Marked compromised. Stop using immediately. |
50
+
51
+ #### Update status directly
52
+
53
+ ```ts
54
+ const { success, error } = await mc.domains.dkim.updateStatus('example.com', {
55
+ selector: 'mcdkim',
56
+ status: 'revoked' // 'revoked' | 'retired' | 'rotated'
57
+ })
58
+ ```
59
+
60
+ Only `active` keys can move to `rotated`. Only `revoked`, `retired`, and `rotated` are
61
+ valid update targets.
62
+
63
+ #### Rotate (recommended for routine rollover)
64
+
65
+ ```ts
66
+ const { data, error } = await mc.domains.dkim.rotate('example.com', 'mcdkim', {
67
+ newKey: { selector: 'mcdkim2' }
68
+ })
69
+
70
+ // data.new is the new key info; data.rotated is the old one.
71
+ // data.rotated.gracePeriodExpiresAt says when signing with it stops.
72
+ ```
73
+
74
+ Rotation:
75
+
76
+ 1. Marks the existing key `rotated` (still valid for **3 days** — cut-off in
77
+ `gracePeriodExpiresAt`).
78
+ 2. Creates a new active key with the new selector, reusing the same algorithm and key
79
+ length.
80
+ 3. The rotated key is auto-retired **2 weeks** after rotation.
81
+
82
+ **Publish the new DNS record before `gracePeriodExpiresAt`** or emails signed with the new
83
+ key will fail DKIM at receiving providers.
84
+
85
+ ### Sending With A Hosted Key
86
+
87
+ ```ts
88
+ const { data, error } = await mc.emails.queue({
89
+ from: 'sender@example.com',
90
+ to: 'recipient@example.net',
91
+ subject: 'Signed',
92
+ text: 'Signed by hosted DKIM.',
93
+ dkim: {
94
+ domain: 'example.com',
95
+ selector: 'mcdkim'
96
+ }
97
+ })
98
+ ```
99
+
100
+ If `dkim.selector` is set without `dkim.domain`, MailChannels takes the domain from the
101
+ `from` address.
102
+
103
+ ### Sending With A Customer-Managed Key
104
+
105
+ If you keep the private key yourself, pass it Base64-encoded (PEM headers are stripped
106
+ automatically):
107
+
108
+ ```ts
109
+ const { data, error } = await mc.emails.queue({
110
+ from: 'sender@example.com',
111
+ to: 'recipient@example.net',
112
+ subject: 'Signed',
113
+ text: 'Signed by my own DKIM key.',
114
+ dkim: {
115
+ domain: 'example.com',
116
+ selector: 'mcdkim',
117
+ privateKey: '<base64-encoded-or-PEM-private-key>'
118
+ }
119
+ })
120
+ ```
121
+
122
+ `dkim` can also be set per-personalization to override the root value.
123
+
124
+ ### DNS Publication
125
+
126
+ MailChannels does not host the public DKIM DNS record — you do. The SDK returns the exact
127
+ record to publish in `data.dnsRecords`:
128
+
129
+ | Field | Value |
130
+ | --- | --- |
131
+ | `name` | DNS host. Always `{selector}._domainkey.{domain}`. |
132
+ | `type` | Always `TXT`. |
133
+ | `value` | Public key material. **Publish verbatim** — do not re-wrap, strip quotes, or split lines. |
134
+
135
+ #### Required Steps
136
+
137
+ 1. **Create** with `mc.domains.dkim.create(domain, { selector })`. Capture `dnsRecords`.
138
+ 2. **Resolve the DNS zone** in your provider's API.
139
+ 3. **Create or update** the TXT record at `record.name` with `record.value`. Use a short
140
+ TTL (300 s) so future rotations propagate quickly.
141
+ 4. **Wait for propagation**, then verify with `mc.domains.check(domain, { dkim: [{ selector }] })`.
142
+ The `dkim[].verdict` must be `'passed'` before sending real traffic.
143
+ 5. **For rotations**, leave the old TXT record in place until `gracePeriodExpiresAt`.
144
+
145
+ ### Selector Format
146
+
147
+ - 1–63 characters.
148
+ - Lowercase letters, numbers, and `-` work everywhere. Avoid `_` in the selector itself.
@@ -0,0 +1,100 @@
1
+ # Domain Checks
2
+
3
+ Use `mc.domains.check()` to confirm a sender domain is properly authenticated before
4
+ sending from it. The check covers four things:
5
+
6
+ - **DKIM** — does the domain publish a DKIM public key matching the selectors MailChannels
7
+ signs with?
8
+ - **SPF** — is MailChannels in the domain's SPF record?
9
+ - **Sender-domain DNS** — does the domain have at least one `A` or `MX` record? Receivers
10
+ reject mail without either as **SDNF** ("Sender Domain Not Found").
11
+ - **Domain Lockdown** — a MailChannels feature that ties a sending domain to your account
12
+ so other MailChannels customers cannot spoof it.
13
+
14
+ ### Basic Check
15
+
16
+ ```ts
17
+ import { MailChannels } from 'mailchannels-sdk'
18
+
19
+ const mc = new MailChannels('YOUR-API-KEY')
20
+
21
+ const { data, error } = await mc.domains.check('example.com')
22
+
23
+ if (error) throw new Error(error.message)
24
+
25
+ console.log(data?.spf?.verdict) // 'passed' | 'failed' | …
26
+ console.log(data?.domainLockdown?.verdict) // 'passed' | 'failed'
27
+ console.log(data?.senderDomain?.verdict) // 'passed' | 'failed'
28
+ console.log(data?.dkim) // array of per-selector results
29
+ console.log(data?.references) // support links if anything failed
30
+ ```
31
+
32
+ ### With Specific DKIM Settings
33
+
34
+ If you don't pass `dkim`, MailChannels uses all stored keys for the domain. To target
35
+ specific selectors:
36
+
37
+ ```ts
38
+ const { data, error } = await mc.domains.check('example.com', {
39
+ dkim: [
40
+ {
41
+ domain: 'example.com',
42
+ selector: 'mcdkim-2025'
43
+ },
44
+ {
45
+ domain: 'example.com',
46
+ selector: 'mcdkim-2026'
47
+ }
48
+ ]
49
+ })
50
+ ```
51
+
52
+ You can pass up to **10** DKIM settings per call.
53
+
54
+ If you only need to provide a single DKIM setting, you may pass it as an object instead
55
+ of an array:
56
+
57
+ ```ts
58
+ const { data, error } = await mc.domains.check('example.com', {
59
+ dkim: {
60
+ domain: 'example.com',
61
+ selector: 'mcdkim'
62
+ }
63
+ })
64
+ ```
65
+
66
+ #### DKIM Settings Resolution Rules
67
+
68
+ | Provided fields | Behavior |
69
+ | --- | --- |
70
+ | `domain`, `selector`, `privateKey` all present | Verify with the provided key. |
71
+ | `domain`, `selector` | Use the stored private key for that domain + selector. |
72
+ | `domain` only | Use **all** stored keys for that domain. |
73
+ | `selector` only | Use the `domain` from the request body. |
74
+ | `privateKey` set | `selector` is required too. |
75
+ | `dkim` empty / absent | Use all stored keys for the request domain. |
76
+
77
+ ### With A Sender ID (Domain Lockdown)
78
+
79
+ If your lockdown record uses `senderid=` or `sidw=` fields, pass the sender identity:
80
+
81
+ ```ts
82
+ const { data, error } = await mc.domains.check('example.com', {
83
+ senderId: 'example|domain|example.com'
84
+ })
85
+ ```
86
+
87
+ If your lockdown record uses `auth=` (account-wide authorization), omit `senderId`.
88
+
89
+ ### Verdicts Reference
90
+
91
+ - DKIM, Domain Lockdown, A, and MX verdicts: `'passed'` or `'failed'`.
92
+ - SPF has a richer set: `'passed'`, `'failed'`, `'soft failed'`, `'temporary error'`,
93
+ `'permanent error'`, `'neutral'`, `'none'`, `'unknown'`.
94
+ - `senderDomain` passes if **either** the A or MX check passes.
95
+
96
+ ### When To Use
97
+
98
+ - During onboarding for every new sending domain.
99
+ - In CI after rotating DKIM keys, to verify the new DNS record propagated.
100
+ - In a periodic monitoring job (verdicts can drift if DNS changes).