TypeScript-first wrapper around the MailChannels Email API with runtime validation, DKIM enforcement, and ergonomic helpers. The client targets Node.js 18+ and reuses the built-in Fetch API, so there are no production dependencies.
ℹ️ Before sending email you must provision an SMTP password, generate an API key with the
apiscope, and configure Domain Lockdown records as documented by MailChannels.1
- Installation
- Quick start
- Features
- MailChannels setup guide
- Per-recipient overrides
- Attachments
- Error handling
- Testing
- API surface
- Footnotes
# pnpm
pnpm add @jconet-ltd/mailchannels-client
# npm
npm install @jconet-ltd/mailchannels-client
# Yarn
yarn add @jconet-ltd/mailchannels-client
# Bun
bun add @jconet-ltd/mailchannels-clientimport { MailChannelsClient } from "@jconet-ltd/mailchannels-client";
const client = new MailChannelsClient({
apiKey: process.env.MAILCHANNELS_API_KEY ?? "",
dkim: {
domain: "example.com",
selector: "mcdkim",
privateKey: process.env.MAILCHANNELS_DKIM_PRIVATE_KEY ?? "",
},
});
await client.sendEmail({
personalizations: [
{
to: [{ email: "recipient@example.net", name: "Sakura Tanaka" }],
},
],
from: { email: "sender@example.com", name: "Priya Patel" },
subject: "Testing Email API",
content: [
{ type: "text/plain", value: "Hi Sakura. This is just a test from Priya." },
{
type: "text/html",
value: "<p>Hi Sakura.<br>This is just a test from Priya.</p>",
},
],
});The client injects the DKIM defaults into both the request body and every personalization, ensuring compliance with MailChannels' DKIM requirements even when you do not set them manually.
Under the hood the client issues a POST request to https://api.mailchannels.net/tx/v1/send with your API key in the X-Api-Key header, exactly as described in the MailChannels sending guide.2
- Required DKIM configuration enforced at construction time to keep every request compliant.3
- Strong TypeScript definitions that mirror the MailChannels
/sendpayload.5 - Runtime validation of key fields (personalizations, content, DKIM) before the network hop.
- Optional dry-run support (
?dry-run=true) so you can validate payloads without delivering mail.6 - Detailed error metadata surfaced via
MailChannelsError, including request identifiers, retry hints, headers, and the original payload. - Attachment helper validation around MIME type, filename, and Base64 encoding.7
- Built-in Fetch integration with an escape hatch for custom implementations (tests, polyfills).
-
Create an account. Sign up for MailChannels, verify your email address, and add valid billing details as prompted.1
-
Complete the authentication prerequisites. From the console create an SMTP password, generate an API key with the
apiscope, and publish Domain Lockdown (_mailchannels) TXT records for every sending domain.2 -
Provision DKIM. MailChannels requires DKIM signatures for modern deliverability. You can:
-
Generate a private key, derive the public key, and publish it at
selector._domainkey.yourdomain. Sample OpenSSL commands:3openssl genrsa 2048 | tee priv_key.pem \ | openssl rsa -outform der \ | openssl base64 -A > priv_key.txt echo -n "v=DKIM1;p=" > pub_key_record.txt openssl rsa -in priv_key.pem -pubout -outform der \ | openssl base64 -A >> pub_key_record.txt
Publish the contents of
pub_key_record.txtas a TXT record at<selector>._domainkey.<yourdomain>. -
Or call the MailChannels DKIM APIs (
POST /tx/v1/domains/{domain}/dkim-keys, etc.) to generate and activate key pairs directly, then publish the returned DNS record.3
-
-
Record your defaults. Capture the following details for the domain you will sign with:
dkim_domain– typically the same domain as yourFromaddress for DMARC alignment.dkim_selector– the label you used in DNS (for examplemcdkim).dkim_private_key– the Base64-encoded private key (contents ofpriv_key.txtif you used the OpenSSL recipe above).
Once the DNS changes propagate you are ready to send signed traffic through the /send endpoint.4
You can still customise DKIM (and other headers) per recipient. Values defined inside a personalization override the client defaults for that specific message.
import { MailChannelsClient } from "@jconet-ltd/mailchannels-client";
import type { SendEmailRequest } from "@jconet-ltd/mailchannels-client";
const client = new MailChannelsClient({
apiKey: "YOUR-API-KEY",
dkim: {
domain: "example.com",
selector: "mcdkim",
privateKey: "BASE64_PRIVATE_KEY",
},
});
const message: SendEmailRequest = {
personalizations: [
{
to: [{ email: "banana-lover123@example.com" }],
subject: "BANANAS ARE ON SALE",
dynamic_template_data: { discountCode: "BANANA-BOAT" },
},
{
to: [{ email: "vip@example.com" }],
subject: "Exclusive VIP Pricing",
dkim_selector: "vipselector",
dkim_private_key: "BASE64_VIP_KEY",
},
],
from: { email: "news@example.com", name: "Example News" },
template_id: "spring-sale",
content: [
{
type: "text/plain",
value: "Plain-text fallback for clients that do not render HTML.",
},
{
type: "text/html",
value:
"<html><body><p>Check the sale in your personalized template.</p></body></html>",
},
],
metadata: { campaign: "spring-2025" },
};
await client.sendEmail(message, { dryRun: true });Attachments must be Base64 encoded and accompanied by a MIME type plus filename. The client checks these fields before sending.
import { MailChannelsClient } from "@jconet-ltd/mailchannels-client";
import { promises as fs } from "node:fs";
const client = new MailChannelsClient({
apiKey: "YOUR-API-KEY",
dkim: {
domain: "example.com",
selector: "mcdkim",
privateKey: "BASE64_PRIVATE_KEY",
},
});
const logoPngBase64 = Buffer.from(
await fs.readFile("./assets/logo.png")
).toString("base64");
await client.sendEmail({
personalizations: [{ to: [{ email: "recipient@example.com" }] }],
from: { email: "sender@example.com" },
subject: "Email with Attachment",
content: [{ type: "text/plain", value: "Please see the attached image." }],
attachments: [
{
type: "image/png",
filename: "logo.png",
content: logoPngBase64,
},
],
});API failures throw a MailChannelsError with rich diagnostics so you can log or retry intelligently. In addition to the message and status code, the error exposes the HTTP status text, response headers, any structured body, the upstream request identifier, and a parsed retryAfterSeconds hint when the service returns Retry-After.
import { MailChannelsError } from "@jconet-ltd/mailchannels-client";
try {
await client.sendEmail(payload);
} catch (error) {
if (error instanceof MailChannelsError) {
console.error(
"MailChannels request failed",
error.status,
error.statusText,
error.requestId
);
if (error.retryAfterSeconds) {
console.info("Safe to retry in", error.retryAfterSeconds, "seconds");
}
console.debug("Response headers", error.headers);
console.debug("Original payload", error.details);
}
throw error;
}npm install
npm run typecheck
npm run buildnpm run build emits ESM output plus .d.ts bundles into dist/ ready for publishing.
apiKey– MailChannels API credential with theapiscope.2dkim– required defaults{ domain, selector, privateKey }; applied automatically to every request and personalization.3baseUrl– override the API endpoint (defaults tohttps://api.mailchannels.net/tx/v1/).fetchImplementation– provide an alternative Fetch-compatible function if needed.defaultHeaders– headers merged into every outbound request.
- Accepts a strongly typed payload matching the MailChannels
/sendschema.5 - Ensures DKIM, recipients, and content blocks are valid before calling the API.
options.dryRuntoggles thedry-runquery to request synchronous validation.6options.signallets you cancel in-flight requests with anAbortSignal.options.idempotencyKeysets theIdempotency-Keyheader for safe retries.
Non-success responses raise MailChannelsError, exposing the HTTP status, status text, request identifier, retry hints, headers, and the parsed or raw response body. Advanced callers can construct the error manually with MailChannelsErrorOptions when wrapping lower-level utilities.