Send transactional email, manage campaigns and contacts, and track engagement — all from a single, developer-friendly API.
import MisarMail from "@misarmail/sdk";
const client = new MisarMail("msk_your_api_key_here");
const result = await client.send({
from: { email: "hello@yourdomain.com", name: "Your App" },
to: [{ email: "user@example.com" }],
subject: "Welcome to the platform!",
html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
text: "Welcome! Thanks for signing up.",
});
console.log(result.message_id); // msg_abc123import misarmail
client = misarmail.Client("msk_your_api_key_here")
result = client.send(
from_={"email": "hello@yourdomain.com", "name": "Your App"},
to=[{"email": "user@example.com"}],
subject="Welcome to the platform!",
html="<h1>Welcome!</h1><p>Thanks for signing up.</p>",
text="Welcome! Thanks for signing up.",
)
print(result["message_id"]) # msg_abc123curl -X POST https://mail.misar.io/api/v1/send \
-H "Authorization: Bearer msk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"from": { "email": "hello@yourdomain.com", "name": "Your App" },
"to": [{ "email": "user@example.com" }],
"subject": "Welcome to the platform!",
"html": "<h1>Welcome!</h1>",
"text": "Welcome!"
}'All API requests must include your API key in the Authorization header as a Bearer token. API keys start with msk_.
Get your API key from Settings → API Keys. Keys are scoped — assign only the permissions your integration needs.
Authorization: Bearer msk_your_api_key_here| Scope | Access |
|---|---|
send | Send transactional and marketing email |
send:transactional | Send transactional email only |
send:marketing | Send marketing campaigns only |
read | Read campaigns, contacts, templates |
write | Create/update campaigns, contacts, templates |
contacts | Manage contact lists |
analytics | Access analytics data |
sandbox | Sandbox mode — no real email delivery |
/api/v1/sendSend a transactional email. The from.email must match a verified account in your MisarMail workspace.
Parameters
fromobjectrequired{ email: string, name?: string } — sender address (must be verified)toarrayrequiredArray of { email, name? } — up to 100 recipientsccarrayCC recipients — up to 50bccarrayBCC recipients — up to 50subjectstringrequiredEmail subject line — max 998 charactershtmlstringHTML body — max 500KB (provide html and/or text)textstringPlain text body — max 500KBalias_iduuidRoute via a specific SMTP pool aliasidempotency_keystringUnique key to prevent duplicate sends — max 128 charstagsarrayUp to 10 string tags for filtering and reportingmetadataobjectUp to 20 key-value pairs for custom dataRequest
{
"from": { "email": "no-reply@yourdomain.com", "name": "Your App" },
"to": [{ "email": "user@example.com", "name": "Jane" }],
"subject": "Your receipt — Order #12345",
"html": "<p>Thanks for your purchase!</p>",
"text": "Thanks for your purchase!",
"idempotency_key": "order_12345_receipt",
"tags": ["transactional", "receipt"],
"metadata": { "order_id": "12345", "customer_id": "cus_abc" }
}Response
{
"success": true,
"message_id": "msg_01j9xkqp7v8f3...",
"provider": "smtp",
"timestamp": "2026-03-14T10:23:45.000Z"
}X-MisarMail-Sandbox: true to any send request and the email will be intercepted — no real delivery happens. Useful for testing. Open sandbox dashboard →/api/v1/campaignsList all campaigns. Requires read, send, or send:marketing scope.
Parameters
pagenumberPage number (default: 1)limitnumberResults per page — max 50 (default: 20)statusstringFilter by status: draft | scheduled | sending | sent | paused | cancelledResponse
{
"success": true,
"data": [
{
"id": "uuid",
"name": "March Newsletter",
"subject": "What's new this month",
"status": "sent",
"total_sent": 5842,
"total_opened": 2193,
"total_clicked": 847,
"scheduled_at": "2026-03-01T09:00:00Z",
"created_at": "2026-02-28T12:00:00Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 42, "totalPages": 3 }
}/api/v1/campaignsCreate a new campaign draft. Requires send or send:marketing scope.
Parameters
namestringrequiredCampaign name — max 100 charssubjectstringrequiredEmail subject linefromNamestringrequiredSender display namefromEmailstringrequiredSender email (verified account)bodyHtmlstringHTML email body — max 500KBbodyTextstringPlain text fallbacksegmentIduuidContact segment to send toscheduledAtdatetimeISO 8601 — schedule for future sendRequest
{
"name": "April Newsletter",
"subject": "Spring updates from our team",
"fromName": "Your Company",
"fromEmail": "newsletter@yourdomain.com",
"bodyHtml": "<h1>Hello!</h1><p>Here's what's new...</p>",
"bodyText": "Hello! Here's what's new...",
"segmentId": "550e8400-e29b-41d4-a716-446655440000",
"scheduledAt": "2026-04-01T09:00:00Z"
}Response
{
"success": true,
"data": {
"id": "uuid",
"name": "April Newsletter",
"status": "scheduled",
"created_at": "2026-03-14T10:00:00Z"
}
}/api/v1/contactsList contacts. Requires contacts or read scope.
Parameters
pagenumberPage number (default: 1)limitnumberMax 50 (default: 20)searchstringSearch by email or namestatusstringFilter: subscribed | unsubscribed | bouncedResponse
{
"success": true,
"data": [
{
"id": "uuid",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"status": "subscribed",
"tags": ["customer", "premium"],
"created_at": "2026-01-15T08:00:00Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1250, "totalPages": 63 }
}/api/v1/contactsCreate or update a contact. Requires contacts or write scope.
Parameters
emailstringrequiredContact email addressfirstNamestringFirst namelastNamestringLast namephonestringPhone number (E.164 format)tagsarrayString tags — max 50customFieldsobjectKey-value custom fieldsRequest
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"tags": ["customer", "trial"],
"customFields": { "company": "Acme Inc", "plan": "pro" }
}Response
{
"success": true,
"data": {
"id": "uuid",
"email": "jane@example.com",
"status": "subscribed",
"created_at": "2026-03-14T10:00:00Z"
}
}/api/v1/templatesList email templates. Returns id, name, subject, type, and detected variables.
Parameters
pagenumberPage numberlimitnumberMax 50typestringFilter: marketing | transactional | automationResponse
{
"success": true,
"data": [
{
"id": "uuid",
"name": "Welcome Email",
"subject": "Welcome, {{first_name}}!",
"template_type": "transactional",
"variables": ["first_name", "company"],
"created_at": "2026-02-01T09:00:00Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 8, "totalPages": 1 }
}/api/v1/templates/renderRender a template with variables substituted. Returns the final HTML, text, and subject. Useful for previewing before sending.
Parameters
template_iduuidrequiredID of the template to rendervariablesobjectrequiredKey-value pairs to substitute into {{variable}} placeholdersRequest
{
"template_id": "550e8400-e29b-41d4-a716-446655440000",
"variables": {
"first_name": "Jane",
"company": "Acme Inc",
"trial_end": "2026-04-01"
}
}Response
{
"success": true,
"data": {
"subject": "Welcome, Jane!",
"html": "<h1>Welcome, Jane!</h1><p>Your trial at Acme Inc ends April 1.</p>",
"text": "Welcome, Jane! Your trial at Acme Inc ends April 1."
}
}/api/v1/analyticsRetrieve email usage and campaign performance data. Requires analytics scope.
Parameters
startDatestringYYYY-MM-DD — defaults to 30 days agoendDatestringYYYY-MM-DD — defaults to todaycampaignIduuidScope results to a single campaigngroupBystringday | week | monthResponse
{
"success": true,
"data": {
"period": { "start": "2026-02-12", "end": "2026-03-14" },
"campaignTotals": {
"sent": 18420,
"opened": 7368,
"clicked": 2213,
"bounced": 92,
"complained": 4
},
"rates": {
"openRate": 39.99,
"clickRate": 12.02,
"bounceRate": 0.5
},
"emailUsage": [
{ "date": "2026-03-14", "emails_sent_today": 342, "emails_sent_month": 8211 }
]
}
}/api/v1/keysList all API keys in your workspace. Never returns the raw key — only metadata.
Response
{
"success": true,
"keys": [
{
"id": "uuid",
"name": "Production Key",
"key_prefix": "msk_a1b2c3d4",
"scopes": "send,analytics",
"is_active": true,
"last_used_at": "2026-03-14T09:00:00Z",
"created_at": "2026-01-10T12:00:00Z",
"expires_at": null
}
]
}/api/v1/keysCreate a new API key. The raw key is returned only once — store it securely.
Parameters
namestringrequiredHuman-readable key name — max 80 charsscopesarrayrequiredArray of scope stringsexpiresAtdatetimeISO 8601 expiration — omit for no expiryallowedAccountIduuidRestrict key to a single sending accountRequest
{
"name": "Production Transactional",
"scopes": ["send:transactional", "analytics"],
"expiresAt": "2027-01-01T00:00:00Z"
}Response
{
"success": true,
"key": "msk_a1b2c3d4e5f6...",
"keyId": "uuid",
"prefix": "msk_a1b2c3d4",
"name": "Production Transactional",
"scopes": "send:transactional,analytics",
"createdAt": "2026-03-14T10:00:00Z"
}All REST endpoints are also accessible via a single GraphQL endpoint at POST /api/graphql. Use the same Authorization: Bearer msk_ header for authentication.
# Query example
query GetCampaigns($page: Int, $status: String) {
campaigns(page: $page, limit: 10, status: $status) {
items {
id
name
subject
status
totalSent
totalOpened
}
total
page
}
}
# Mutation example
mutation Send($input: SendEmailInput!) {
sendEmail(input: $input) {
success
messageId
provider
timestamp
}
}curl -X POST https://mail.misar.io/api/graphql \
-H "Authorization: Bearer msk_your_key" \
-H "Content-Type: application/json" \
-d '{
"query": "query { me { id email } }",
"variables": {}
}'Sandbox mode intercepts all email sends and stores them for inspection — no real email is ever delivered. Enable it per-request or on a key.
Option 1 — Per-request header
X-MisarMail-Sandbox: trueOption 2 — Dedicated sandbox key (scope: sandbox)
{ "scopes": ["sandbox", "send"] }View sandbox sends in the Sandbox Dashboard or via GET /api/v1/sandbox.
/api/v1/sandboxList your most recent sandbox sends (last 50). Requires API key auth.
Response
{
"success": true,
"sends": [
{
"id": "uuid",
"from_address": "hello@yourdomain.com",
"to_addresses": ["user@example.com"],
"subject": "Welcome!",
"message_id": "sandbox_abc123...",
"created_at": "2026-03-14T10:00:00Z"
}
]
}Configure webhook endpoints in Settings → Webhooks. MisarMail signs every payload with HMAC-SHA256 using your webhook secret. Verify the signature in your handler:
import crypto from "node:crypto";
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature.replace("sha256=", ""))
);
}
// In your handler:
const sig = request.headers["x-misarmail-signature"];
const raw = await request.text();
if (!verifyWebhook(raw, sig, process.env.WEBHOOK_SECRET)) {
return response.status(401).send("Invalid signature");
}
const event = JSON.parse(raw);| Event | Description |
|---|---|
email.delivered | Email was accepted by the recipient mail server |
email.opened | Recipient opened the email (tracking pixel loaded) |
email.clicked | Recipient clicked a tracked link |
email.bounced | Email bounced — permanent or temporary failure |
email.complained | Recipient marked email as spam |
email.unsubscribed | Recipient clicked the unsubscribe link |
campaign.sent | Campaign finished sending to all recipients |
contact.unsubscribed | Contact's global subscription status changed |
{
"event": "email.opened",
"timestamp": "2026-03-14T10:23:45.000Z",
"data": {
"message_id": "msg_01j9xkqp7v8f3...",
"recipient": "user@example.com",
"campaign_id": null,
"metadata": { "order_id": "12345" }
}
}| Endpoint | Limit |
|---|---|
POST /api/v1/send (API key) | 1,000 requests / minute |
All other v1 endpoints | 100 requests / minute |
POST /api/graphql | 100 requests / minute |
Rate limit headers are returned on every response:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1741948800
Retry-After: 12 # only on 429 responsesNode.js / TypeScript
npm package
npm install @misarmail/sdkPython
PyPI package
pip install misarmailNeed help? Contact support · OpenAPI Spec · Sandbox