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,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.
|
|
12
|
+
> Built and tested against Email API `1.4.0`
|
|
13
13
|
|
|
14
|
-
Node.js SDK to integrate [MailChannels Email API](https://docs.mailchannels.
|
|
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.
|
|
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.
|
|
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)
|