SDKs
Python
Official Python SDK for MisarMail — sync and async clients for Django, FastAPI, Flask, and scripts
Python SDK
The misarmail PyPI package is the official Python SDK for MisarMail. It ships dual sync and async clients built on httpx, with full type hints via TypedDict.
Installation
pip install misarmail
# or
uv add misarmail
# or
poetry add misarmailRequirements: Python ≥ 3.9, httpx ≥ 0.27
Quick Start
from misarmail import MisarMail
client = MisarMail(api_key="msk_your_key_here")
result = client.send(
from_={"email": "hello@yourapp.com", "name": "Your App"},
to=[{"email": "user@example.com"}],
subject="Welcome!",
html="<p>Welcome aboard!</p>",
)
print(result["message_id"])Configuration
from misarmail import MisarMail
client = MisarMail(
api_key="msk_your_key_here", # Required
base_url="https://mail.misar.io", # Default
timeout=15.0, # Seconds. Default: 30
headers={"X-App-Version": "2.0"}, # Extra headers
)Load the key from the environment:
import os
from misarmail import MisarMail
client = MisarMail(api_key=os.environ["MISARMAIL_API_KEY"])Sending Emails
Basic Send
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
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=invoice_html,
)Idempotency (Prevent Duplicates)
client.send(
from_={"email": "no-reply@yourapp.com"},
to=[{"email": user.email}],
subject="Welcome!",
html="<p>Welcome aboard!</p>",
idempotency_key=f"welcome-{user.id}", # retry-safe
)Tags & Metadata
client.send(
from_={"email": "no-reply@yourapp.com"},
to=[{"email": user.email}],
subject="Password reset",
html=reset_html,
tags=["transactional", "password-reset"],
metadata={"user_id": str(user.id), "request_ip": request.remote_addr},
)Contacts
List Contacts
response = client.contacts.list(
page=1,
limit=50,
status="subscribed", # subscribed | unsubscribed | bounced | complained
search="jane",
)
print(f"{response['pagination']['total']} contacts")
for contact in response["data"]:
print(contact["email"], contact["status"])Create a Contact
response = client.contacts.create(
email="jane@example.com",
first_name="Jane",
last_name="Doe",
status="subscribed",
custom_fields={"plan": "pro", "signup_source": "landing-page"},
tags=["newsletter", "beta"],
)
contact = response["data"]Delete a Contact
client.contacts.delete(contact_id)Bulk Import (Pro+)
result = client.contacts.import_contacts(
contacts=[
{"email": "alice@example.com", "first_name": "Alice", "tags": ["vip"]},
{"email": "bob@example.com", "first_name": "Bob"},
],
update_existing=True, # update instead of skip on duplicate
)
print(result["summary"])
# {"imported": 1, "updated": 1, "skipped": 0, "errors": 0}Campaigns
List Campaigns
response = client.campaigns.list(status="sent", page=1, limit=20)
campaigns = response["data"]Create a Draft Campaign
response = client.campaigns.create(
name="March Newsletter",
subject="What's new in March 🌱",
from_name="Your App",
from_email="news@yourapp.com",
body_html="<h1>Hello!</h1><p>Here's what's new...</p>",
segment_id="segment-uuid", # optional
)
campaign = response["data"]Schedule a Campaign
response = client.campaigns.create(
name="Product Launch",
subject="Something big is coming...",
from_name="Your App",
from_email="news@yourapp.com",
body_html=launch_html,
scheduled_at="2026-03-01T09:00:00Z", # ISO 8601
)Get, Update & Delete
# Get by ID
response = client.campaigns.get(campaign_id)
# Update (only draft/scheduled campaigns)
client.campaigns.update(campaign_id, subject="Updated subject line", body_html=updated_html)
# Delete a draft
client.campaigns.delete(campaign_id)Send Immediately
result = client.campaigns.send(campaign_id)
print(result["status"]) # "sending"Templates
List Templates
response = client.templates.list(type="transactional", page=1, limit=20)Create a Template
response = client.templates.create(
name="Welcome Email",
subject="Welcome, {{firstName}}!",
body_html="""
<h1>Hi {{firstName}},</h1>
<p>Your account is ready. <a href="{{dashboardUrl}}">Get started →</a></p>
""",
template_type="transactional",
variables=["firstName", "dashboardUrl"],
)Analytics
Aggregate Analytics
response = client.analytics.get(
start_date="2026-02-01",
end_date="2026-02-28",
group_by="day",
)
data = response["data"]
print(data["rates"])
# {"openRate": "42.5%", "clickRate": "8.1%", "bounceRate": "0.3%"}Per-Campaign Analytics
response = client.analytics.campaign(campaign_id)
data = response["data"]
print(data["campaign"]["total_sent"])
print(data["rates"]["openRate"]) # "42.5%"
print(data["rates"]["clickRate"]) # "8.1%"Error Handling
All API errors raise MisarMailError:
from misarmail import MisarMail, MisarMailError
try:
client.send(...)
except MisarMailError as e:
print(e.status) # HTTP status code (e.g. 429)
print(e.code) # Error code string (e.g. "RATE_LIMIT")
print(str(e)) # Human-readable message
print(e.details) # Optional structured details
if e.is_rate_limit: print("Slow down — you're being rate limited")
if e.is_plan_limit: print("Upgrade your plan to continue")
if e.is_unauthorized: print("Check your API key")
if e.is_not_found: print("Resource not found")Retry with Backoff
import time
from misarmail import MisarMailError
def send_with_retry(client, max_retries=3, **kwargs):
for attempt in range(max_retries + 1):
try:
return client.send(**kwargs)
except MisarMailError as e:
if e.is_rate_limit and attempt < max_retries:
time.sleep(2 ** attempt)
continue
raiseAsync Client
Use AsyncMisarMail for async frameworks (FastAPI, asyncio scripts):
from misarmail import AsyncMisarMail
# As an async context manager (recommended — reuses httpx.AsyncClient)
async with AsyncMisarMail(api_key="msk_...") as client:
result = await client.send(
from_={"email": "no-reply@yourapp.com"},
to=[{"email": "user@example.com"}],
subject="Hello!",
html="<p>Hello!</p>",
)
# One-shot (creates + closes httpx.AsyncClient per call)
client = AsyncMisarMail(api_key="msk_...")
result = await client.send(...)Async Contacts, Campaigns, Templates, Analytics
async with AsyncMisarMail(api_key=api_key) as client:
# Contacts
contacts = await client.contacts.list(status="subscribed")
contact = await client.contacts.create(email="jane@example.com")
await client.contacts.delete(contact_id)
# Campaigns
campaigns = await client.campaigns.list()
campaign = await client.campaigns.create(
name="Newsletter", subject="Hello", from_name="App", from_email="news@app.com",
body_html="<p>Hi!</p>",
)
await client.campaigns.send(campaign["data"]["id"])
# Analytics
stats = await client.analytics.get(start_date="2026-02-01", end_date="2026-02-28")Django Integration
# myapp/services/email.py
import os
from misarmail import MisarMail, MisarMailError
_client = MisarMail(api_key=os.environ["MISARMAIL_API_KEY"])
def send_welcome_email(user) -> None:
try:
_client.send(
from_={"email": "no-reply@yourapp.com", "name": "Your App"},
to=[{"email": user.email, "name": user.get_full_name()}],
subject="Welcome!",
html=f"<h1>Hi {user.first_name}!</h1><p>Your account is ready.</p>",
idempotency_key=f"welcome-{user.pk}",
)
except MisarMailError as e:
import logging
logging.error("Failed to send welcome email to %s: %s", user.email, e)
raiseFastAPI Integration
# routers/auth.py
import os
from fastapi import APIRouter, BackgroundTasks
from misarmail import AsyncMisarMail
router = APIRouter()
mail = AsyncMisarMail(api_key=os.environ["MISARMAIL_API_KEY"])
async def _send_verification(email: str, name: str, verify_url: str) -> None:
async with mail:
await mail.send(
from_={"email": "no-reply@yourapp.com", "name": "Your App"},
to=[{"email": email, "name": name}],
subject="Verify your email",
html=f'<p>Click <a href="{verify_url}">here</a> to verify your email.</p>',
text=f"Verify your email: {verify_url}",
idempotency_key=f"verify-{email}",
)
@router.post("/register")
async def register(user_data: UserCreate, background_tasks: BackgroundTasks):
user = await create_user(user_data)
background_tasks.add_task(
_send_verification, user.email, user.name, user.verify_url
)
return {"message": "Registration successful. Check your email."}Type Hints with mypy
The SDK ships TypedDict types. Import them for full static typing:
from misarmail.types import (
EmailAddress,
SendEmailOptions,
SendEmailResponse,
Contact,
ContactStatus,
CreateContactOptions,
ImportContactRow,
ImportContactsResponse,
Campaign,
CampaignStatus,
CreateCampaignOptions,
EmailTemplate,
TemplateType,
CreateTemplateOptions,
)Run mypy:
mypy --strict your_module.pyRate Limits
| Scope | Limit |
|---|---|
| Transactional send | 100 req/min per key |
| Contacts & Campaigns | 60 req/min per key |
| Bulk import | 10 req/min per key |
A MisarMailError with status=429 or err.is_rate_limit == True means you've been rate limited. Use the retry helper above or implement your own exponential backoff.