mailchannels-sdk 1.1.0 → 1.3.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,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()`.
@@ -23,6 +23,7 @@ if (error) {
23
23
  // error.message — human-readable description
24
24
  // error.type — stable string identifier (see table below)
25
25
  // error.statusCode — HTTP status code, or null for non-HTTP errors
26
+ // error.response — Response body (Object) for 4xx/5xx errors
26
27
  console.error(error.message, error.type, error.statusCode)
27
28
  return
28
29
  }
@@ -57,11 +57,12 @@ for (const bucket of data?.buckets.processed ?? []) {
57
57
 
58
58
  #### Engagement Buckets
59
59
 
60
- `data.buckets` has `open`, `click`, `openTrackingDelivered`, `clickTrackingDelivered`.
60
+ `data.buckets` has `open`, `click`, `uniqueOpen`, `uniqueClick`, `openTrackingDelivered`,
61
+ `clickTrackingDelivered`, `uniqueOpenTrackingDelivered`, `uniqueClickTrackingDelivered`.
61
62
 
62
63
  #### Performance Buckets
63
64
 
64
- `data.buckets` has `processed`, `delivered`, `bounced`.
65
+ `data.buckets` has `processed`, `delivered`, `bounced`, `complained`.
65
66
 
66
67
  #### Recipient Behaviour Buckets
67
68
 
@@ -193,3 +193,29 @@ const { data, error } = await mc.emails.queue({
193
193
  ```
194
194
 
195
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
+ ```
@@ -67,7 +67,7 @@ if (createPwdErr) { /*...*/ }
67
67
  const { data: passwords, error: listPwdErr } = await mc.subAccounts.smtpPasswords.list('clienta')
68
68
  if (listPwdErr) { /*...*/ }
69
69
 
70
- const { error: deletePwdErr } = await mc.subAccounts.smtpPasswordss.delete('clienta', storedPasswordId)
70
+ const { error: deletePwdErr } = await mc.subAccounts.smtpPasswords.delete('clienta', storedPasswordId)
71
71
  if (deletePwdErr) { /*...*/ }
72
72
  ```
73
73
 
@@ -10,8 +10,8 @@ import { MailChannels } from 'mailchannels-sdk'
10
10
 
11
11
  const mc = new MailChannels('YOUR-API-KEY')
12
12
 
13
- const { success, error } = await mc.suppressions.create({
14
- entries: [
13
+ const { success, error } = await mc.suppressions.create(
14
+ [
15
15
  {
16
16
  recipient: 'out@example.net',
17
17
  types: ['non-transactional'], // optional; defaults to non-transactional
@@ -22,8 +22,8 @@ const { success, error } = await mc.suppressions.create({
22
22
  types: ['transactional', 'non-transactional']
23
23
  }
24
24
  ],
25
- addToSubAccounts: true // parent only; copies entries to every sub-account
26
- })
25
+ { addToSubAccounts: true } // parent only; copies entries to every sub-account
26
+ )
27
27
  ```
28
28
 
29
29
  Constraints:
@@ -83,9 +83,9 @@ that recipient regardless of origin.
83
83
 
84
84
  ### Patterns
85
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.
86
+ - **Preference center opt-out**: set `types` according to the email category, e.g. `non-transactional` for marketing
87
+ emails, `transactional` for order updates, and so on.
88
88
  Configure `addToSubAccounts` depending on whether the preference applies to all sub-accounts or just the parent account.
89
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
90
+ - **Migrating from another ESP**: bulk-create with option `addToSubAccounts: true` so every tenant
91
91
  inherits the list.
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.5.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)
@@ -35,7 +35,7 @@ This library provides a simple way to interact with the [MailChannels Email API]
35
35
  ## <a name="features">🚀 Features</a>
36
36
 
37
37
  <!-- #region features -->
38
- 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.
39
39
 
40
40
  Some of the things you can do with the SDK:
41
41
 
@@ -169,6 +169,7 @@ const { data, error } = await mailchannels.emails.send({
169
169
  - Email sends and async sends
170
170
  - Domain checks
171
171
  - DKIM key create, list, rotate, and update
172
+ - Custom tracking domain create, list, update, and delete
172
173
  - Webhook enrollment, listing, validation, signing key lookup, and batch inspection
173
174
  - Sub-account lifecycle, API keys, SMTP passwords, limits, and usage
174
175
  - Engagement, performance, recipient behaviour, sender, volume, and usage metrics
@@ -179,6 +180,7 @@ const { data, error } = await mailchannels.emails.send({
179
180
  - State is in-memory only and is reset when the process stops
180
181
  - Any non-empty `X-API-Key` is accepted, with separate in-memory state per API key
181
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
182
184
 
183
185
  The next planned expansion is outbound webhook delivery so client applications can test webhook ingestion flows against the simulator as well.
184
186
  <!-- #endregion simulator -->
@@ -55,6 +55,8 @@ const createMetricsBuckets = (count) => [{
55
55
  const createAccountState = (apiKey) => ({
56
56
  apiKey,
57
57
  customerHandle: createId("customer"),
58
+ limit: { sends: 1e5 },
59
+ customTrackingDomains: [],
58
60
  dkimKeysByDomain: /* @__PURE__ */ new Map(),
59
61
  messages: [],
60
62
  subAccounts: /* @__PURE__ */ new Map(),
@@ -101,6 +103,16 @@ const createDkimKey = (domain, selector, overrides = {}) => {
101
103
  };
102
104
  };
103
105
  const listDkimKeys = (account, domain) => account.dkimKeysByDomain.get(domain) || [];
106
+ const createCustomTrackingDomain = ({ hostname, name, scope, status = "active" }) => ({
107
+ created_at: currentTimestamp(),
108
+ hostname,
109
+ name,
110
+ scope,
111
+ status
112
+ });
113
+ const findCustomTrackingDomain = (account, hostname, scope) => {
114
+ return account.customTrackingDomains.find((domain) => domain.hostname === hostname && domain.scope === scope);
115
+ };
104
116
  const recordWebhookBatch = (account, webhook, eventCount, status = "2xx_response", statusCode = 200) => {
105
117
  account.webhookBatches.unshift({
106
118
  batch_id: account.nextIds.batch++,
@@ -125,6 +137,7 @@ const collectMessages = (account, filters = {}) => {
125
137
  };
126
138
  const summarizeMessages = (messages) => ({
127
139
  bounced: messages.reduce((total, message) => total + message.bounced, 0),
140
+ complained: messages.reduce((total, message) => total + message.complained, 0),
128
141
  click: messages.reduce((total, message) => total + message.click, 0),
129
142
  clickTrackingDelivered: messages.reduce((total, message) => total + message.clickTrackingDelivered, 0),
130
143
  delivered: messages.reduce((total, message) => total + message.delivered, 0),
@@ -198,6 +211,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
198
211
  account.messages.push({
199
212
  bounced: 0,
200
213
  campaignId: body?.campaign_id || "uncategorized",
214
+ complained: 0,
201
215
  click: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
202
216
  clickTrackingDelivered: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
203
217
  delivered: 1,
@@ -240,11 +254,12 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
240
254
  }
241
255
  if (method === "POST" && url.pathname === "/tx/v1/check-domain") {
242
256
  const domain = body?.domain || "example.com";
257
+ const dkimSettings = body?.dkim_settings?.length ? body.dkim_settings : listDkimKeys(account, domain).map((key) => ({
258
+ dkim_domain: key.domain,
259
+ dkim_selector: key.selector
260
+ }));
243
261
  sendJson(response, 200, { check_results: {
244
- dkim: (body?.dkim_settings?.length ? body.dkim_settings : listDkimKeys(account, domain).map((key) => ({
245
- dkim_domain: key.domain,
246
- dkim_selector: key.selector
247
- }))).map((setting) => ({
262
+ dkim: dkimSettings.map((setting) => ({
248
263
  dkim_domain: setting.dkim_domain || domain,
249
264
  dkim_key_status: setting.dkim_private_key ? "provided" : listDkimKeys(account, setting.dkim_domain || domain).find((key) => key.selector === setting.dkim_selector)?.status || "active",
250
265
  dkim_selector: setting.dkim_selector || "default",
@@ -321,6 +336,71 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
321
336
  sendNoContent(response);
322
337
  return;
323
338
  }
339
+ if (url.pathname === "/tx/v1/custom-tracking-domains") {
340
+ if (method === "POST") {
341
+ const { hostname, name, scope } = body || {};
342
+ if (!hostname || !name || !scope) {
343
+ sendJson(response, 400, { error: "Invalid request body." });
344
+ return;
345
+ }
346
+ const hasDuplicateName = account.customTrackingDomains.some((domain) => domain.name === name);
347
+ const hasDuplicateHostnameAndScope = Boolean(findCustomTrackingDomain(account, hostname, scope));
348
+ if (hasDuplicateName || hasDuplicateHostnameAndScope) {
349
+ sendJson(response, 409, { error: "Custom tracking domain already exists." });
350
+ return;
351
+ }
352
+ const domain = createCustomTrackingDomain({
353
+ hostname,
354
+ name,
355
+ scope
356
+ });
357
+ account.customTrackingDomains.push(domain);
358
+ sendJson(response, 201, clone(domain));
359
+ return;
360
+ }
361
+ if (method === "GET") {
362
+ const name = url.searchParams.get("name");
363
+ const status = url.searchParams.get("status");
364
+ const scope = url.searchParams.get("scope");
365
+ const offset = Number(url.searchParams.get("offset") || "0");
366
+ const limit = Number(url.searchParams.get("limit") || "100");
367
+ let domains = account.customTrackingDomains;
368
+ if (name) domains = domains.filter((domain) => domain.name === name);
369
+ if (status) domains = domains.filter((domain) => domain.status === status);
370
+ if (scope) domains = domains.filter((domain) => domain.scope === scope);
371
+ sendJson(response, 200, {
372
+ custom_tracking_domains: clone(domains.slice(offset, offset + limit)),
373
+ total: domains.length
374
+ });
375
+ return;
376
+ }
377
+ }
378
+ const customTrackingDomainMatch = url.pathname.match(/^\/tx\/v1\/custom-tracking-domains\/([^/]+)\/([^/]+)$/);
379
+ if (customTrackingDomainMatch) {
380
+ const [, hostname, scope] = customTrackingDomainMatch;
381
+ const targetDomain = findCustomTrackingDomain(account, hostname, scope);
382
+ if (!targetDomain) {
383
+ sendJson(response, 404, { error: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.` });
384
+ return;
385
+ }
386
+ if (method === "PATCH") {
387
+ if (body?.name) {
388
+ if (account.customTrackingDomains.some((domain) => domain !== targetDomain && domain.name === body.name)) {
389
+ sendJson(response, 409, { error: "Name already used by another domain." });
390
+ return;
391
+ }
392
+ targetDomain.name = body.name;
393
+ }
394
+ if (body?.status) targetDomain.status = body.status;
395
+ sendJson(response, 200, clone(targetDomain));
396
+ return;
397
+ }
398
+ if (method === "DELETE") {
399
+ account.customTrackingDomains = account.customTrackingDomains.filter((domain) => domain !== targetDomain);
400
+ sendNoContent(response);
401
+ return;
402
+ }
403
+ }
324
404
  if (url.pathname === "/tx/v1/webhook" && method === "POST") {
325
405
  const endpoint = url.searchParams.get("endpoint");
326
406
  if (!endpoint) {
@@ -360,19 +440,20 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
360
440
  sendJson(response, 404, { error: "No webhooks found for the account." });
361
441
  return;
362
442
  }
443
+ const results = Array.from(account.webhooks).map((webhook) => {
444
+ recordWebhookBatch(account, webhook, 1);
445
+ return {
446
+ result: "passed",
447
+ webhook,
448
+ response: {
449
+ body: "simulated validation ok",
450
+ status: 200
451
+ }
452
+ };
453
+ });
363
454
  sendJson(response, 200, {
364
455
  all_passed: true,
365
- results: Array.from(account.webhooks).map((webhook) => {
366
- recordWebhookBatch(account, webhook, 1);
367
- return {
368
- result: "passed",
369
- webhook,
370
- response: {
371
- body: "simulated validation ok",
372
- status: 200
373
- }
374
- };
375
- })
456
+ results
376
457
  });
377
458
  return;
378
459
  }
@@ -383,7 +464,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
383
464
  const webhook = url.searchParams.get("webhook");
384
465
  const limit = Number(url.searchParams.get("limit") || "500");
385
466
  const offset = Number(url.searchParams.get("offset") || "0");
386
- sendJson(response, 200, { webhook_batches: account.webhookBatches.filter((batch) => {
467
+ const batches = account.webhookBatches.filter((batch) => {
387
468
  if (createdAfter && batch.created_at < createdAfter) return false;
388
469
  if (createdBefore && batch.created_at >= createdBefore) return false;
389
470
  if (webhook && batch.webhook !== webhook) return false;
@@ -391,7 +472,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
391
472
  if (!statuses.map((status) => `${status}_response`.replace("no_response_response", "no_response")).includes(batch.status)) return false;
392
473
  }
393
474
  return true;
394
- }).slice(offset, offset + limit) });
475
+ });
476
+ sendJson(response, 200, { webhook_batches: batches.slice(offset, offset + limit) });
395
477
  return;
396
478
  }
397
479
  const webhookBatchResendMatch = url.pathname.match(/^\/tx\/v1\/webhook-batch\/(\d+)\/resend$/);
@@ -519,16 +601,20 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
519
601
  return;
520
602
  }
521
603
  if (suffix === "usage" && method === "GET") {
604
+ const subLimitSends = subAccount.limit?.sends ?? -1;
605
+ const monthlyLimit = subLimitSends === -1 ? account.limit.sends : subLimitSends;
522
606
  sendJson(response, 200, {
523
607
  period_end_date: currentTimestamp(),
524
608
  period_start_date: new Date((/* @__PURE__ */ new Date()).setDate(1)).toISOString(),
525
- total_usage: subAccount.usage
609
+ total_usage: subAccount.usage,
610
+ monthly_limit: monthlyLimit
526
611
  });
527
612
  return;
528
613
  }
529
614
  }
530
615
  if (url.pathname === "/tx/v1/metrics/engagement" && method === "GET") {
531
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
616
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
617
+ const summary = summarizeMessages(messages);
532
618
  sendJson(response, 200, {
533
619
  buckets: {
534
620
  click: createMetricsBuckets(summary.click),
@@ -546,11 +632,14 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
546
632
  return;
547
633
  }
548
634
  if (url.pathname === "/tx/v1/metrics/performance" && method === "GET") {
549
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
635
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
636
+ const summary = summarizeMessages(messages);
550
637
  sendJson(response, 200, {
551
638
  bounced: summary.bounced,
639
+ complained: summary.complained,
552
640
  buckets: {
553
641
  bounced: createMetricsBuckets(summary.bounced),
642
+ complained: createMetricsBuckets(summary.complained),
554
643
  delivered: createMetricsBuckets(summary.delivered),
555
644
  processed: createMetricsBuckets(summary.processed)
556
645
  },
@@ -562,7 +651,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
562
651
  return;
563
652
  }
564
653
  if (url.pathname === "/tx/v1/metrics/recipient-behaviour" && method === "GET") {
565
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
654
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
655
+ const summary = summarizeMessages(messages);
566
656
  sendJson(response, 200, {
567
657
  buckets: {
568
658
  unsubscribe_delivered: createMetricsBuckets(summary.unsubscribeDelivered),
@@ -576,7 +666,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
576
666
  return;
577
667
  }
578
668
  if (url.pathname === "/tx/v1/metrics/volume" && method === "GET") {
579
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
669
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
670
+ const summary = summarizeMessages(messages);
580
671
  sendJson(response, 200, {
581
672
  buckets: {
582
673
  delivered: createMetricsBuckets(summary.delivered),
@@ -633,10 +724,21 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
633
724
  return;
634
725
  }
635
726
  if (url.pathname === "/tx/v1/usage" && method === "GET") {
727
+ let totalUsage = account.messages.length;
728
+ let monthlyLimit = account.limit.sends;
729
+ if (scopeHandle) {
730
+ const subAccount = account.subAccounts.get(scopeHandle);
731
+ if (subAccount) {
732
+ totalUsage = subAccount.usage;
733
+ const subLimitSends = subAccount.limit?.sends ?? -1;
734
+ monthlyLimit = subLimitSends === -1 ? account.limit.sends : subLimitSends;
735
+ }
736
+ }
636
737
  sendJson(response, 200, {
637
738
  period_end_date: currentTimestamp(),
638
739
  period_start_date: new Date((/* @__PURE__ */ new Date()).setDate(1)).toISOString(),
639
- total_usage: account.messages.length
740
+ total_usage: totalUsage,
741
+ monthly_limit: monthlyLimit
640
742
  });
641
743
  return;
642
744
  }
@@ -678,7 +780,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
678
780
  }
679
781
  notFound(response);
680
782
  } catch (error) {
681
- sendText(response, 500, error instanceof Error ? error.message : "Simulator error");
783
+ const message = error instanceof Error ? error.message : "Simulator error";
784
+ sendText(response, 500, message);
682
785
  }
683
786
  };
684
787
  return {