SDKs
JavaScript / TypeScript
Official JavaScript & TypeScript SDK for MisarMail — works in Node.js, Next.js, Bun, and any JS runtime
JavaScript / TypeScript SDK
The misarmail npm package is the official JavaScript SDK for MisarMail. It ships with full TypeScript types, works in Node.js ≥18, Bun, Deno, and any runtime with native fetch.
Installation
npm install misarmail
# or
pnpm add misarmail
# or
yarn add misarmailQuick Start
import MisarMail from "misarmail";
const client = new MisarMail({ apiKey: "msk_your_key_here" });
const result = await client.send({
from: { email: "hello@yourapp.com", name: "Your App" },
to: [{ email: "user@example.com" }],
subject: "Welcome!",
html: "<p>Welcome aboard!</p>",
});
console.log(result.message_id);Configuration
const client = new MisarMail({
/** Required — API key starting with msk_ */
apiKey: process.env.MISARMAIL_API_KEY!,
/** Override base URL (e.g. for local testing). Default: https://mail.misar.io */
baseUrl: "https://mail.misar.io",
/** Additional headers sent with every request */
headers: { "X-App-Version": "2.0" },
/** Fetch timeout in ms. Default: 30000 */
timeout: 15_000,
});Add the key to .env.local:
MISARMAIL_API_KEY=msk_your_key_hereSending Emails
Basic Send
await client.send({
from: { email: "no-reply@yourapp.com", name: "Your App" },
to: [{ email: "user@example.com", name: "Jane" }],
subject: "Your receipt",
html: "<p>Thanks for your order!</p>",
text: "Thanks for your order!",
});With CC, BCC & Reply-To
await client.send({
from: { email: "billing@yourapp.com", name: "Billing" },
to: [{ email: "customer@example.com" }],
cc: [{ email: "manager@yourapp.com" }],
bcc: [{ email: "archive@yourapp.com" }],
reply_to: { email: "support@yourapp.com", name: "Support" },
subject: "Invoice #1042",
html: invoiceHtml,
});Idempotency (Prevent Duplicates)
await client.send({
from: { email: "no-reply@yourapp.com" },
to: [{ email: user.email }],
subject: "Welcome!",
html: "<p>Welcome aboard!</p>",
idempotency_key: `welcome-${user.id}`, // retry-safe
});Tags & Metadata
await client.send({
from: { email: "no-reply@yourapp.com" },
to: [{ email: user.email }],
subject: "Password reset",
html: resetHtml,
tags: ["transactional", "password-reset"],
metadata: { user_id: user.id, request_ip: req.ip },
});Route via Alias SMTP Pool
await client.send({
from: { email: "news@yourapp.com" },
to: [{ email: subscriber.email }],
subject: "This week in news",
html: newsletterHtml,
alias_id: "your-alias-uuid", // routes via that alias's SMTP pool
});Contacts
List Contacts
const { data, pagination } = await client.contacts.list({
page: 1,
limit: 50,
status: "subscribed", // "subscribed" | "unsubscribed" | "bounced" | "complained"
search: "john",
});
console.log(`${pagination.total} contacts`);
data.forEach(c => console.log(c.email, c.status));Create a Contact
const { data: contact } = await client.contacts.create({
email: "jane@example.com",
firstName: "Jane",
lastName: "Doe",
status: "subscribed",
customFields: { plan: "pro", signup_source: "landing-page" },
tags: ["newsletter", "beta"],
});Delete a Contact
await client.contacts.delete(contactId);Bulk Import (Pro+)
const result = await client.contacts.import({
contacts: [
{ email: "alice@example.com", firstName: "Alice", tags: ["vip"] },
{ email: "bob@example.com", firstName: "Bob" },
],
updateExisting: true, // update instead of skip on duplicate
});
console.log(result.summary);
// { imported: 1, updated: 1, skipped: 0, errors: 0 }Campaigns
List Campaigns
const { data: campaigns } = await client.campaigns.list({
status: "sent",
page: 1,
limit: 20,
});Create a Draft Campaign
const { data: campaign } = await client.campaigns.create({
name: "March Newsletter",
subject: "What's new in March 🌱",
fromName: "Your App",
fromEmail: "news@yourapp.com",
bodyHtml: "<h1>Hello!</h1><p>Here's what's new...</p>",
segmentId: "segment-uuid", // optional — send to a specific segment
});Schedule a Campaign
const { data: campaign } = await client.campaigns.create({
name: "Product Launch",
subject: "Something big is coming...",
fromName: "Your App",
fromEmail: "news@yourapp.com",
bodyHtml: launchHtml,
scheduledAt: "2026-03-01T09:00:00Z", // ISO 8601
});Get, Update & Delete
// Get by ID
const { data: campaign } = await client.campaigns.get(campaignId);
// Update (only draft/scheduled campaigns)
await client.campaigns.update(campaignId, {
subject: "Updated subject line",
bodyHtml: updatedHtml,
});
// Delete a draft
await client.campaigns.delete(campaignId);Send Immediately
const result = await client.campaigns.send(campaignId);
console.log(result.status); // "sending"Templates
List Templates
const { data: templates } = await client.templates.list({
type: "transactional",
page: 1,
limit: 20,
});Create a Template
const { data: template } = await client.templates.create({
name: "Welcome Email",
subject: "Welcome, {{firstName}}!",
bodyHtml: `
<h1>Hi {{firstName}},</h1>
<p>Your account is ready. <a href="{{dashboardUrl}}">Get started →</a></p>
`,
templateType: "transactional",
variables: ["firstName", "dashboardUrl"],
});Analytics
Aggregate Analytics
const { data } = await client.analytics.get({
startDate: "2026-02-01",
endDate: "2026-02-28",
groupBy: "day",
});
console.log(data.rates);
// { openRate: "42.5%", clickRate: "8.1%", bounceRate: "0.3%" }Per-Campaign Analytics
const { data } = await client.analytics.campaign(campaignId);
console.log(data.campaign.total_sent);
console.log(data.rates.openRate); // "42.5%"
console.log(data.rates.clickRate); // "8.1%"Error Handling
All errors throw a MisarMailError with structured fields:
import MisarMail, { MisarMailError } from "misarmail";
try {
await client.send({ ... });
} catch (err) {
if (err instanceof MisarMailError) {
console.log(err.status); // HTTP status code (e.g. 429)
console.log(err.code); // Error code string (e.g. "RATE_LIMIT")
console.log(err.message); // Human-readable message
console.log(err.details); // Optional structured details
if (err.isRateLimit) console.log("Slow down — you're being rate limited");
if (err.isPlanLimit) console.log("Upgrade your plan to continue");
if (err.isUnauthorized) console.log("Check your API key");
if (err.isNotFound) console.log("Resource not found");
}
}Error Reference
| Status | err.isX helper | Meaning |
|---|---|---|
401 | isUnauthorized | Invalid or missing API key |
403 | isPlanLimit | Plan limit reached or missing scope |
404 | isNotFound | Resource not found |
408 | — | Request timed out |
409 | — | Duplicate (e.g. email already exists) |
429 | isRateLimit | Rate limit hit — back off and retry |
500 | — | Internal server error |
TypeScript Types
All types are exported directly from misarmail:
import type {
// Config
MisarMailConfig,
// Email
EmailAddress,
SendEmailOptions,
SendEmailResponse,
// Contacts
Contact,
ContactStatus,
CreateContactOptions,
ImportContactRow,
ImportContactsResponse,
// Campaigns
Campaign,
CampaignStatus,
CreateCampaignOptions,
UpdateCampaignOptions,
CampaignSendResponse,
// Templates
EmailTemplate,
TemplateType,
CreateTemplateOptions,
// Analytics
AggregateAnalyticsResponse,
CampaignAnalyticsResponse,
// Pagination
Pagination,
PaginatedResponse,
} from "misarmail";Next.js Integration
Route Handler
// app/api/auth/register/route.ts
import MisarMail from "misarmail";
const mail = new MisarMail({ apiKey: process.env.MISARMAIL_API_KEY! });
export async function POST(req: Request) {
const { email, name } = await req.json();
// ... create user ...
await mail.send({
from: { email: "no-reply@yourapp.com", name: "Your App" },
to: [{ email, name }],
subject: "Verify your email",
html: `<p>Click <a href="${verifyUrl}">here</a> to verify.</p>`,
idempotency_key: `verify-${userId}`,
});
return Response.json({ success: true });
}Server Action
// app/actions/email.ts
"use server";
import MisarMail from "misarmail";
const mail = new MisarMail({ apiKey: process.env.MISARMAIL_API_KEY! });
export async function sendWelcomeEmail(userId: string, email: string) {
return mail.send({
from: { email: "hello@yourapp.com", name: "Your App" },
to: [{ email }],
subject: "Welcome!",
html: "<p>Welcome aboard!</p>",
idempotency_key: `welcome-${userId}`,
});
}When running in a containerised environment (Coolify, Docker) with Next.js, process.env may not receive runtime-injected variables. Use getRuntimeEnv("MISARMAIL_API_KEY") from your project's runtime-env helper instead.
Rate Limits
| Scope | Limit |
|---|---|
Transactional send (/api/v1/send) | 100 req/min per key |
| Contacts & Campaigns | 60 req/min per key |
| Bulk import | 10 req/min per key |
A 429 response means you've hit a rate limit. Back off exponentially and retry:
async function sendWithRetry(options: SendEmailOptions, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await client.send(options);
} catch (err) {
if (err instanceof MisarMailError && err.isRateLimit && attempt < maxRetries) {
await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
continue;
}
throw err;
}
}
}