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
+ # Suppressions
2
+
3
+ A suppression list keeps known-bad or opted-out recipients out of future sends. MailChannels
4
+ suppresses by recipient + suppression type + source.
5
+
6
+ ### Create Entries
7
+
8
+ ```ts
9
+ import { MailChannels } from 'mailchannels-sdk'
10
+
11
+ const mc = new MailChannels('YOUR-API-KEY')
12
+
13
+ const { success, error } = await mc.suppressions.create({
14
+ entries: [
15
+ {
16
+ recipient: 'out@example.net',
17
+ types: ['non-transactional'], // optional; defaults to non-transactional
18
+ notes: 'Imported from preference center' // optional, max 1024 chars
19
+ },
20
+ {
21
+ recipient: 'complainer@example.net',
22
+ types: ['transactional', 'non-transactional']
23
+ }
24
+ ],
25
+ addToSubAccounts: true // parent only; copies entries to every sub-account
26
+ })
27
+ ```
28
+
29
+ Constraints:
30
+
31
+ - **Atomic**: either every entry in the request is added, or none are.
32
+ - A single request can carry at most **1000** entries combined across the parent and any
33
+ sub-accounts.
34
+ - Each `recipient` is up to 255 characters; `notes` up to 1024.
35
+ - `types` items must be `'transactional'` or `'non-transactional'`.
36
+ - If any entry already exists, the API returns `conflict_error`.
37
+
38
+ All entries created via this endpoint have an inherent source of `'api'`.
39
+ The endpoint does not have a field to set the source value.
40
+
41
+ ### List Entries
42
+
43
+ ```ts
44
+ const { data, error } = await mc.suppressions.list({
45
+ recipient: 'recipient@example.net', // exact-match filter
46
+ source: 'api', // see source values below
47
+ createdAfter: '2026-04-01',
48
+ createdBefore: '2026-05-01T00:00:00Z',
49
+ limit: 100, // 1..1000, default 1000
50
+ offset: 0
51
+ })
52
+
53
+ // Date objects are also accepted
54
+ const { data: recent } = await mc.suppressions.list({
55
+ createdAfter: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
56
+ createdBefore: new Date()
57
+ })
58
+ ```
59
+
60
+ `source` values:
61
+
62
+ - `'api'` — explicitly created via the API.
63
+ - `'unsubscribe_link'` — recipient used the hosted unsubscribe URL.
64
+ - `'list_unsubscribe'` — recipient used a one-click `List-Unsubscribe` header.
65
+ - `'hard_bounce'` — bounced as undeliverable.
66
+ - `'spam_complaint'` — recipient reported spam at their provider.
67
+
68
+ Date formats accepted: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM:SSZ`, or a `Date` object.
69
+
70
+ ### Delete An Entry
71
+
72
+ Warning:
73
+ Do not remove entries from the suppression list if the recipient has not explicitly opted back in.
74
+ This may cause deliverability issues and violate anti-spam laws and our policies.
75
+
76
+ ```ts
77
+ const { error: delApiErr } = await mc.suppressions.delete('recipient@example.net', 'api')
78
+ const { error: delAllErr } = await mc.suppressions.delete('recipient@example.net', 'all') // all sources
79
+ ```
80
+
81
+ If `source` is omitted it defaults to `'api'`. Use `'all'` to remove every suppression for
82
+ that recipient regardless of origin.
83
+
84
+ ### Patterns
85
+
86
+ - **Preference center opt-out**: set `type` according to the email category, e.g. `non-transactional` for marketing
87
+ emails, `transactional` for order updates, and so on.
88
+ Configure `addToSubAccounts` depending on whether the preference applies to all sub-accounts or just the parent account.
89
+ Add a note to indicate the source, e.g. "Opted out via preference center".
90
+ - **Migrating from another ESP**: bulk-create with `addToSubAccounts: true` so every tenant
91
+ inherits the list.
@@ -0,0 +1,97 @@
1
+ # Mustache Templates
2
+
3
+ MailChannels templates are **send-payload fields**, not a template CRUD resource. There is
4
+ no "create template" endpoint. To use a template:
5
+
6
+ 1. Set `template: { type: 'mustache', data: { ... } }` on the root options.
7
+ 2. Provide per-recipient variable overrides in each personalization's `template.data`.
8
+
9
+ The only supported template type is `'mustache'`.
10
+
11
+ ### Single Recipient
12
+
13
+ ```ts
14
+ import { MailChannels } from 'mailchannels-sdk'
15
+
16
+ const mc = new MailChannels('YOUR-API-KEY')
17
+
18
+ const { data, error } = await mc.emails.queue({
19
+ from: 'sender@example.com',
20
+ to: 'jane@example.net',
21
+ subject: 'Hello {{name}}',
22
+ text: 'Hello {{name}}',
23
+ html: '<p>Hello {{name}}</p>',
24
+ template: {
25
+ type: 'mustache',
26
+ data: { name: 'Jane' }
27
+ }
28
+ })
29
+ ```
30
+
31
+ ### Multiple Recipients With Per-Recipient Variables
32
+
33
+ Each personalization renders independently with its own variables. Root-level
34
+ `template.data` is the base; per-personalization `template.data` is merged on top
35
+ (personalization wins on conflict):
36
+
37
+ ```ts
38
+ const { data, error } = await mc.emails.queue({
39
+ from: 'sender@example.com',
40
+ subject: 'Hi {{name}}',
41
+ text: 'Hi {{name}}, you are on the {{plan}} plan.',
42
+ template: {
43
+ type: 'mustache',
44
+ data: { plan: 'Free' } // root default
45
+ },
46
+ personalizations: [
47
+ {
48
+ to: 'jane@example.net',
49
+ template: { data: { name: 'Jane', plan: 'Pro' } } // overrides plan
50
+ },
51
+ {
52
+ to: 'alex@example.net',
53
+ template: { data: { name: 'Alex' } } // inherits plan: 'Free'
54
+ }
55
+ ]
56
+ })
57
+ ```
58
+
59
+ ### Allowed Template Variable Value Types
60
+
61
+ Keys are strings. Values may be:
62
+
63
+ - `string`
64
+ - `boolean`
65
+ - `number`
66
+ - array of any of the above
67
+ - plain object (nested maps with string keys and any of the above as values)
68
+
69
+ `null`, `undefined`, and class instances are rejected with a `validation_error` before
70
+ the request leaves the client.
71
+
72
+ ### Subject Templates
73
+
74
+ The root `subject` field is **also** mustache-rendered when a `template` is set. There is
75
+ no separate subject template configuration.
76
+
77
+ ### Preview Without Sending
78
+
79
+ Use `dryRun: true` on `emails.send()` to render and validate without delivering:
80
+
81
+ ```ts
82
+ const { data, error } = await mc.emails.send(
83
+ {
84
+ from: 'sender@example.com',
85
+ to: 'recipient@example.net',
86
+ subject: 'Hello {{name}}',
87
+ text: 'Hello {{name}}',
88
+ template: { type: 'mustache', data: { name: 'World' } }
89
+ },
90
+ true // dryRun
91
+ )
92
+
93
+ // data.rendered is a string[] — one rendered message per personalization
94
+ ```
95
+
96
+ Dry-run is the right way to confirm that a template renders correctly before shipping.
97
+ `emails.queue()` does not support dry-run.
@@ -0,0 +1,95 @@
1
+ # Testing
2
+
3
+ The SDK ships a built-in API simulator that runs a local HTTP server mimicking the real
4
+ MailChannels API. Use it for integration tests without hitting the live API.
5
+
6
+ ### Starting The Simulator
7
+
8
+ Start the simulator as a background process before your test run:
9
+
10
+ ```bash
11
+ npx mailchannels-sdk simulate --port 8787 --silent
12
+ ```
13
+
14
+ | Flag | Env var | Default |
15
+ | --- | --- | --- |
16
+ | `-p` / `--port` | `MAILCHANNELS_SIMULATOR_PORT` | `8787` |
17
+ | `-h` / `--host` | `MAILCHANNELS_SIMULATOR_HOST` | (system default) |
18
+ | `-s` / `--silent` | — | `false` |
19
+
20
+ In CI or npm scripts, use `start-server-and-test` or `concurrently` to manage the process
21
+ alongside your test runner:
22
+
23
+ ```bash
24
+ # package.json script example
25
+ "test:integration": "start-server-and-test 'npx mailchannels-sdk simulate --silent' http://localhost:8787 vitest"
26
+ ```
27
+
28
+ ### Example Test (Vitest / Jest)
29
+
30
+ Point the `MailChannels` client at the running simulator via `baseUrl`:
31
+
32
+ ```ts
33
+ import { describe, it, expect } from 'vitest'
34
+ import { MailChannels } from 'mailchannels-sdk'
35
+
36
+ // Simulator must already be running: npx mailchannels-sdk simulate --port 8787 --silent
37
+ const mc = new MailChannels('test-key', { baseUrl: 'http://localhost:8787' })
38
+
39
+ it('queues an email', async () => {
40
+ const { data, error } = await mc.emails.queue({
41
+ from: 'sender@example.com',
42
+ to: 'recipient@example.net',
43
+ subject: 'Test',
44
+ text: 'Hello'
45
+ })
46
+
47
+ expect(error).toBeNull()
48
+ expect(data?.requestId).toBeDefined()
49
+ })
50
+ ```
51
+
52
+ ### Client-Side Validation Without A Simulator
53
+
54
+ The SDK validates payloads before making any HTTP call — `validation_error` results are
55
+ returned synchronously (no network required). Tests for client-side validation rules don't
56
+ need the simulator at all:
57
+
58
+ ```ts
59
+ it('rejects reserved headers', async () => {
60
+ const mc = new MailChannels('test-key', { baseUrl: 'http://localhost:9999' })
61
+
62
+ const { error } = await mc.emails.queue({
63
+ from: 'sender@example.com',
64
+ to: 'recipient@example.net',
65
+ subject: 'Test',
66
+ text: 'Hello',
67
+ headers: { 'From': 'evil@attacker.example' } // reserved header
68
+ })
69
+
70
+ expect(error?.type).toBe('validation_error')
71
+ })
72
+ ```
73
+
74
+ No network call is made because the error is caught before `_fetch` runs.
75
+
76
+ ### Dry Run For Template Assertions
77
+
78
+ Use `emails.send(options, true)` (dry-run) against the real API in a staging pipeline to
79
+ assert templates render correctly before shipping:
80
+
81
+ ```ts
82
+ const { data, error } = await mc.emails.send(
83
+ {
84
+ from: 'sender@example.com',
85
+ to: 'preview@example.net',
86
+ subject: 'Hi {{name}}',
87
+ text: 'Hi {{name}}',
88
+ template: { type: 'mustache', data: { name: 'Alice' } }
89
+ },
90
+ true
91
+ )
92
+
93
+ expect(error).toBeNull()
94
+ expect(data?.rendered?.[0]).toContain('Hi Alice')
95
+ ```
@@ -0,0 +1,73 @@
1
+ # Unsubscribe
2
+
3
+ MailChannels offers two complementary unsubscribe mechanisms — an in-body unsubscribe link,
4
+ and the standard `List-Unsubscribe` / `List-Unsubscribe-Post` headers. They are not
5
+ alternatives. A non-transactional (bulk / marketing / notification) send should use
6
+ **both**: the headers let inbox providers surface a one-click "Unsubscribe" button in their
7
+ UI, and the in-body link is what recipients see and click inside the message body. Major
8
+ inbox providers (Gmail, Yahoo, etc.) effectively require both for bulk senders.
9
+
10
+ Both mechanisms require the message to have **exactly one recipient per personalization**
11
+ and to be **DKIM-signed**.
12
+
13
+ ### In-Body Unsubscribe Link
14
+
15
+ Use the literal placeholder string `{{mc-unsubscribe-url}}` inside a mustache HTML body.
16
+ MailChannels substitutes a hosted one-click unsubscribe URL at render time.
17
+
18
+ ```ts
19
+ const { data, error } = await mc.emails.queue({
20
+ from: 'sender@example.com',
21
+ to: 'recipient@example.net',
22
+ subject: 'Newsletter',
23
+ html: `
24
+ <p>Hello!</p>
25
+ <p><a href="{{mc-unsubscribe-url}}">Unsubscribe</a></p>
26
+ `,
27
+ template: { type: 'mustache' }
28
+ })
29
+ ```
30
+
31
+ The `template` field must be present for `{{mc-unsubscribe-url}}` to be substituted.
32
+
33
+ ### `List-Unsubscribe` Headers (Non-Transactional)
34
+
35
+ Setting `transactional: false` tells MailChannels to add `List-Unsubscribe` and
36
+ `List-Unsubscribe-Post` headers automatically. These headers are what inbox providers read
37
+ to render their "Unsubscribe" button next to the sender name.
38
+
39
+ **Always include the in-body link in the same payload** so the message is fully compliant
40
+ on both surfaces:
41
+
42
+ ```ts
43
+ const { data, error } = await mc.emails.queue({
44
+ from: 'sender@example.com',
45
+ to: 'recipient@example.net',
46
+ subject: 'Marketing message',
47
+ html: `
48
+ <p>Today's update…</p>
49
+ <p><a href="{{mc-unsubscribe-url}}">Unsubscribe</a></p>
50
+ `,
51
+ template: { type: 'mustache' },
52
+ transactional: false,
53
+ dkim: {
54
+ domain: 'example.com',
55
+ selector: 'mcdkim'
56
+ }
57
+ })
58
+ ```
59
+
60
+ If `transactional: false` is set but a personalization has more than one recipient,
61
+ the SDK returns a `validation_error` before making any HTTP call.
62
+
63
+ ### When To Use Which
64
+
65
+ | Message type | In-body link | `transactional: false` headers |
66
+ | --- | --- | --- |
67
+ | Transactional (receipts, password resets, confirmations) | No | No (keep the default `true`). |
68
+ | Bulk / marketing / newsletter / notification | **Yes** | **Yes** — combine both in the same payload. |
69
+
70
+ There is essentially no situation where you'd want headers but not the in-body link; if
71
+ you're sending non-transactional mail, you need both.
72
+
73
+ Both modes require DKIM signing. See [dkim](dkim.md) for how to create the underlying key.
@@ -0,0 +1,174 @@
1
+ # Webhooks
2
+
3
+ MailChannels posts batched delivery events to a URL you register. Events cover both
4
+ `emails.send()` and `emails.queue()` sends and use the same payload shape.
5
+
6
+ ### Enroll And Manage
7
+
8
+ ```ts
9
+ import { MailChannels } from 'mailchannels-sdk'
10
+
11
+ const mc = new MailChannels('YOUR-API-KEY')
12
+
13
+ const { error: createError } = await mc.webhooks.create('https://example.com/mailchannels/events')
14
+ if (createError) { /*...*/ }
15
+
16
+ const { data: webhooks, error: listError } = await mc.webhooks.list() // enrolled URLs
17
+ if (listError) { /*...*/ }
18
+
19
+ const { error: deleteError } = await mc.webhooks.deleteAll() // removes ALL enrolled webhooks
20
+ if (deleteError) { /*...*/ }
21
+ ```
22
+
23
+ There is no per-URL delete — `deleteAll()` removes every enrolled webhook for the account.
24
+ Enroll the replacement first if you're swapping URLs.
25
+
26
+ If the endpoint is already enrolled, `create()` returns `conflict_error`.
27
+
28
+ ### Validate
29
+
30
+ `mc.webhooks.validate()` sends a synthetic test request to **every** enrolled webhook and
31
+ reports each one's response. Useful as a deploy check.
32
+
33
+ ```ts
34
+ const { data, error } = await mc.webhooks.validate('deploy-smoke-test') // requestId optional, max 28 chars
35
+ if (error) {
36
+ console.error('Webhook validation failed:', error.message)
37
+ return
38
+ }
39
+
40
+ if (data?.allPassed) {
41
+ console.log('All webhooks responded with 2xx')
42
+ }
43
+ for (const entry of data?.results ?? []) {
44
+ console.log(entry.webhook, entry.result, entry.response)
45
+ }
46
+ ```
47
+
48
+ The test payload carries `event: 'test'` and a hardcoded sender of `test@mailchannels.com`.
49
+
50
+ ### Inspect Batches
51
+
52
+ `mc.webhooks.batches()` returns up to 500 batch summaries with status, status code,
53
+ duration, and event count. Use it to investigate failed deliveries.
54
+
55
+ ```ts
56
+ const { data, error } = await mc.webhooks.batches({
57
+ statuses: ['4xx', '5xx', 'no_response'], // '1xx' | '2xx' | '3xx' | '4xx' | '5xx' | 'no_response'
58
+ createdAfter: '2026-05-20',
59
+ createdBefore: '2026-05-25', // range cannot exceed 31 days
60
+ webhook: 'https://example.com/mailchannels/events',
61
+ limit: 500, // 1..500, default 500
62
+ offset: 0
63
+ })
64
+
65
+ // Date objects are also accepted
66
+ const { data: recent } = await mc.webhooks.batches({
67
+ createdAfter: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
68
+ createdBefore: new Date()
69
+ })
70
+ ```
71
+
72
+ Time formats accepted: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM:SSZ`, or a `Date` object. If neither `createdAfter`
73
+ nor `createdBefore` is set, the default range is the last 3 days.
74
+
75
+ ### Resend A Batch
76
+
77
+ ```ts
78
+ const { data, error } = await mc.webhooks.resendBatch(12345)
79
+
80
+ console.log(data?.statusCode, data?.duration)
81
+ ```
82
+
83
+ A successful call means the resend attempt completed — not that your webhook returned 2xx.
84
+ Check `data.statusCode` to see what your endpoint actually returned.
85
+
86
+ ### Verify Incoming Webhooks (Crucial)
87
+
88
+ MailChannels signs every webhook request with an Ed25519 signature. `Webhooks.verify()`
89
+ does the full verification — content digest, freshness, and signature — in one call.
90
+ **Always verify before processing.**
91
+
92
+ ```ts
93
+ import { Webhooks } from 'mailchannels-sdk'
94
+
95
+ // In your HTTP handler (example using Node.js / Express):
96
+ app.post('/mailchannels/events', express.raw({ type: '*/*' }), async (req, res) => {
97
+ const { data, error } = await Webhooks.verify({
98
+ payload: req.body.toString(), // raw string body — do NOT pass the parsed JSON object
99
+ headers: req.headers as Record<string, string>
100
+ })
101
+
102
+ if (error) {
103
+ return res.status(401).send('Invalid signature')
104
+ }
105
+
106
+ // data is a typed array of webhook events
107
+ for (const event of data) {
108
+ console.log(event.event, event.email, event.requestId)
109
+ }
110
+
111
+ res.sendStatus(200)
112
+ })
113
+ ```
114
+
115
+ `Webhooks.verify()` is a **static method** — you can call it without a `MailChannels`
116
+ instance (no API key needed). It is also available as an instance method on
117
+ `mc.webhooks.verify()`.
118
+
119
+ `verify()` checks all three things in one call:
120
+
121
+ 1. **Content-Digest** — SHA-256 of the raw body matches the header.
122
+ 2. **Freshness** — `created` timestamp is within the default replay window (less than 300 s old)
123
+ 3. **Signature** — Ed25519 verifies against the public key for the given `keyId`.
124
+
125
+ **`payload` must be the raw request body string.** Do not pass the parsed JSON object or
126
+ the signature will never match. Use `req.body.toString()` (Express with raw middleware),
127
+ `await request.text()` (Fetch API / Hono / Cloudflare Workers), or the framework equivalent.
128
+
129
+ #### Supplying The Public Key Manually
130
+
131
+ By default `verify()` fetches and caches the public key automatically from MailChannels.
132
+ You can supply it yourself to avoid the outbound call:
133
+
134
+ ```ts
135
+ const { data: keyData, error: getKeyErr } = await mc.webhooks.getSigningKey(keyId)
136
+ if (getKeyErr) { /*...*/ }
137
+
138
+ const { data, error } = await Webhooks.verify({
139
+ payload: rawBody,
140
+ headers,
141
+ publicKey: keyData?.key,
142
+ cache: false // disable built-in caching when you manage the key yourself
143
+ })
144
+ if (error) { /*...*/ }
145
+ ```
146
+
147
+ Cache the key — it only changes on rotation, and MailChannels may publish multiple active
148
+ keys at once during a rollover. Always fetch by the `keyId` from the incoming request
149
+ rather than holding a single "current" key.
150
+
151
+ ### Event Payload Shape
152
+
153
+ After successful verification `data` is typed as an array of webhook events. Common shared
154
+ fields:
155
+
156
+ | Field | Type | Notes |
157
+ | --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------|
158
+ | `event` | `string` | `'processed'` / `'delivered'` / `'open'` / `'click'` / `'hard-bounced'` / `'soft-bounced'` / `'dropped'` / `'complained'` / `'unsubscribed'` / `'test'` |
159
+ | `email` | `string` | Sender email address. |
160
+ | `customerHandle` | `string` | Your MailChannels account (or sub-account) handle. |
161
+ | `timestamp` | `number` | Unix timestamp. |
162
+ | `requestId` | `string` | Correlates to the `requestId` from `queue()` / `send()`. |
163
+ | `smtpId` | `string` | SMTP message ID. |
164
+ | `campaignId` | `string?` | Present when `campaignId` was set on the send. |
165
+ | `recipients` | `string[]?` | All recipients in the personalization. |
166
+ | `status` | `string?` | The SMTP status code received for the message. Use for system logic. Present on `hard-bounced`, `soft-bounced` events. |
167
+ | `reason` | `string?` | A human readable explanation of the status code. Do not use for system logic. Present on `hard-bounced`, `soft-bounced` events. |
168
+ | `url` / `userAgent` / `ip` | `string?` | Present on `click` / `open` events. |
169
+
170
+ ### Responding
171
+
172
+ Return any 2xx status code quickly. MailChannels treats anything else as a failure and may
173
+ retry. Keep your handler thin: enqueue the event and return, then do the actual processing
174
+ on a worker.
package/README.md CHANGED
@@ -9,12 +9,12 @@
9
9
  [![TypeScript][typescript-src]][typescript-href]
10
10
  [![Node.js][node-src]][node-href]
11
11
 
12
- > Built and tested against Email API `1.0.0`
12
+ > Built and tested against Email API `1.4.0`
13
13
 
14
- Node.js SDK to integrate [MailChannels Email API](https://docs.mailchannels.net/email-api) into your JavaScript or TypeScript server-side applications.
14
+ Node.js SDK to integrate [MailChannels Email API](https://docs.mailchannels.com/email-api) into your JavaScript or TypeScript server-side applications.
15
15
 
16
16
  <!-- #region overview -->
17
- This library provides a simple way to interact with the [MailChannels Email API](https://docs.mailchannels.net/email-api). It is written in TypeScript and can be used in both JavaScript and TypeScript projects and in different runtimes.
17
+ This library provides a simple way to interact with the [MailChannels Email API](https://docs.mailchannels.com/email-api). It is written in TypeScript and can be used in both JavaScript and TypeScript projects and in different runtimes.
18
18
  <!-- #endregion overview -->
19
19
 
20
20
  - [✨ Release Notes](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-js/src/HEAD/CHANGELOG.md)
@@ -28,13 +28,14 @@ This library provides a simple way to interact with the [MailChannels Email API]
28
28
  - 📚 [Usage](#usage)
29
29
  - 📐 [Naming Conventions](#naming-conventions)
30
30
  - 🧪 [Local simulator](#local-simulator)
31
+ - 🤖 [Using with an AI agent](#using-with-an-ai-agent)
31
32
  - ⚖️ [License](#license)
32
33
  - 💻 [Development](#development)
33
34
 
34
35
  ## <a name="features">🚀 Features</a>
35
36
 
36
37
  <!-- #region features -->
37
- This SDK fully supports all features and operations available in the [MailChannels Email API](https://docs.mailchannels.net/email-api). It is actively maintained to ensure compatibility and to quickly add support for new API features as they are released.
38
+ This SDK fully supports all features and operations available in the [MailChannels Email API](https://docs.mailchannels.com/email-api). It is actively maintained to ensure compatibility and to quickly add support for new API features as they are released.
38
39
 
39
40
  Some of the things you can do with the SDK:
40
41
 
@@ -168,6 +169,7 @@ const { data, error } = await mailchannels.emails.send({
168
169
  - Email sends and async sends
169
170
  - Domain checks
170
171
  - DKIM key create, list, rotate, and update
172
+ - Custom tracking domain create, list, update, and delete
171
173
  - Webhook enrollment, listing, validation, signing key lookup, and batch inspection
172
174
  - Sub-account lifecycle, API keys, SMTP passwords, limits, and usage
173
175
  - Engagement, performance, recipient behaviour, sender, volume, and usage metrics
@@ -178,10 +180,74 @@ const { data, error } = await mailchannels.emails.send({
178
180
  - State is in-memory only and is reset when the process stops
179
181
  - Any non-empty `X-API-Key` is accepted, with separate in-memory state per API key
180
182
  - Webhook responses are simulated locally, but the simulator does not yet emit real webhook callbacks to your application
183
+ - Custom tracking domain verification is simulated, but the simulator does not yet check DNS records
181
184
 
182
185
  The next planned expansion is outbound webhook delivery so client applications can test webhook ingestion flows against the simulator as well.
183
186
  <!-- #endregion simulator -->
184
187
 
188
+ ## <a name="using-with-an-ai-agent">🤖 Using with an AI agent</a>
189
+
190
+ This repository ships a complete agent skill at
191
+ [`.agents/skills/mailchannels-js/`](.agents/skills/mailchannels-js/).
192
+ It teaches an AI coding agent how to use the `mailchannels-sdk` package
193
+ correctly by breaking the documentation down into focused
194
+ per-topic files with a decision tree so the agent loads only the
195
+ parts the task actually needs.
196
+
197
+ Layout:
198
+
199
+ ```
200
+ .agents/skills/mailchannels-js/
201
+ ├── SKILL.md # entry point: scope, decision tree, conventions
202
+ └── resources/ # focused recipes (sending, attachments, webhooks, …)
203
+ ```
204
+
205
+ Every file is plain Markdown. The skill works with any agent that can be
206
+ pointed at a directory of context files — Claude Code, Cursor, Codex CLI,
207
+ Aider, Continue, and similar tools all consume it without modification.
208
+
209
+ ### Install
210
+
211
+ The skill ships inside the `mailchannels-sdk` npm package. Use `npm pack` to
212
+ download the tarball, extract it, and copy the skill directory wherever your
213
+ agent looks for skills, rules, or context files:
214
+
215
+ ```bash
216
+ # Make a temp directory to hold the tarball and extracted files:
217
+ mkdir /tmp/mc-js-sdk
218
+
219
+ # Pin the version to match the SDK you have installed — omit the pin to
220
+ # grab the latest release:
221
+ npm pack --pack-destination /tmp/mc-js-sdk mailchannels-sdk
222
+
223
+ # Extract the tarball:
224
+ tar -xzf /tmp/mc-js-sdk/mailchannels-sdk-*.tgz -C /tmp/mc-js-sdk
225
+
226
+ # Replace the destination with your agent's path:
227
+ mkdir -p <your-agent's-skills-dir>
228
+ cp -r /tmp/mc-js-sdk/package/.agents/skills/mailchannels-js <your-agent's-skills-dir>/
229
+
230
+ # Clean up:
231
+ rm -r /tmp/mc-js-sdk
232
+ ```
233
+
234
+ Re-run the same commands when you upgrade the SDK so the skill stays in step
235
+ with the installed version.
236
+
237
+ Common destinations:
238
+
239
+ | Agent | Where to put it |
240
+ | --- | --- |
241
+ | Claude Code | `.claude/skills/` (project) or `~/.claude/skills/` (user) |
242
+ | Cursor | `.cursor/rules/` (or attach files inline with `@`) |
243
+ | Codex CLI | referenced from the project's `AGENTS.md` |
244
+ | Aider | referenced from the conventions file in `.aider.conf.yml` |
245
+ | Continue | registered as a custom context provider |
246
+
247
+ If your tool isn't listed, look for the equivalent of "skill", "rule",
248
+ "context bundle", or "conventions file" — any mechanism that lets the agent
249
+ read a directory of Markdown will work.
250
+
185
251
  ## <a name="license">⚖️ License</a>
186
252
 
187
253
  [MIT License](https://bitbucket.org/mailchannels/mailchannels-email-api-sdk-js/src/HEAD/LICENSE)