Skip to main content

mailblastr

Official Python SDK for the MailBlastr email API — send transactional and marketing email from your own verified domain.

Zero dependencies (Python standard library only). Python 3.8+.

Install

pip install mailblastr

Setup

Grab your API key from the MailBlastr dashboard.

import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"

Send your first email

import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"

params: mailblastr.Emails.SendParams = {
    "from": "Acme <hello@yourdomain.com>",
    "to": ["delivered@mailblastr.dev"],
    "subject": "Hello from MailBlastr",
    "html": "<p>Your first email 🎉</p>",
}

email = mailblastr.Emails.send(params)
print(email["id"])

Every method returns the parsed JSON response — except the three binary download helpers (Emails.Receiving.get_attachment, Emails.Receiving.get_raw and Domains.records_csv), which return bytes. On any non-2xx status the SDK raises mailblastr.MailblastrError carrying the API error body:

try:
    mailblastr.Emails.send(params)
except mailblastr.MailblastrError as e:
    print(e.status_code, e.name, e.message)   # e.g. 422 validation_error "..."

Attachments

Attach files by hosted URL (path, fetched at send time) or inline base64 (content):

mailblastr.Emails.send({
    "from": "Acme <hello@yourdomain.com>",
    "to": ["delivered@mailblastr.dev"],
    "subject": "Your invoice",
    "html": "<p>Invoice attached.</p>",
    "attachments": [
        {"filename": "invoice.pdf", "path": "https://yourdomain.com/invoices/invoice.pdf"},
        {"filename": "report.csv", "content": base64_content, "content_type": "text/csv"},
    ],
})

Batch send

mailblastr.Batch.send([
    {"from": "hello@yourdomain.com", "to": ["delivered@mailblastr.dev"], "subject": "Hi A", "html": "<p>A</p>"},
    {"from": "hello@yourdomain.com", "to": ["delivered@mailblastr.dev"], "subject": "Hi B", "html": "<p>B</p>"},
])  # up to 100 emails per request

A batch succeeds in one of two ways, chosen by its size alone — branch on queued, not on the absence of an error:

Batch size Status Response What happened
1–40 200 no queued key at all — never queued: False every id in data is already handed to the mail service
41–100 202 queued: True, queued_count == len(result["data"]) ids are real, but the emails are still schedulednothing has been transmitted yet
result = mailblastr.Batch.send(payloads)
if result.get("queued"):
    # Accepted, not sent. Poll mailblastr.Emails.get(id) for the outcome.
    ...

Queuing is the only way the documented 100-email maximum can be accepted at all: 100 inline sends run past the platform's request ceiling. A batch carrying an @mailblastr.dev simulator recipient in to, cc or bcc stays inline at any size. An inline batch near the 40-email boundary can take ~100s server-side, far past the 30s default mailblastr.timeout — raise it for batches that large, and always pass an idempotency_key, since a client that gives up mid-request cannot tell what was already sent.

Options

mailblastr.base_url = "https://www.mailblastr.com/api"   # override your API host
mailblastr.timeout = 30            # per-request timeout in seconds (default 30)
mailblastr.max_retries = 2         # auto-retry 429/503 responses (default 2; 0 disables)

Requests time out after 30 seconds by default. A 429 (rate limited) or 503 (service unavailable) response is retried up to max_retries times, honoring the Retry-After header (otherwise exponential backoff). Only those two statuses are retried — never other errors, network failures, or timeouts — so a non-idempotent request (like sending an email) is never duplicated by a retry. A batch send that failed part way through is never retried either: that response names the emails that already went out, and re-sending them would duplicate them.

Errors

MailblastrError carries the {statusCode, name, message} envelope. Match on name and read status_code — messages are scrubbed server-side and a few handlers override the status a name usually maps to, so neither is safe to hard-code. Extra fields ride along on the exception:

try:
    mailblastr.Emails.send(params)
except mailblastr.MailblastrError as e:
    if e.name == "daily_quota_exceeded":
        print(e.limit["used"], e.limit["limit"], e.limit["next_plan"])
    if e.retry_after:
        time.sleep(e.retry_after)
    print(e.body)          # the full parsed error body

try:
    mailblastr.Batch.send(payloads, options={"idempotency_key": "batch-1"})
except mailblastr.MailblastrError as e:
    already_sent = e.sent          # [{"id": ...}, ...] — do NOT resend these
    print(e.sent_count)

The domain-first model

MailBlastr is DOMAIN-FIRST: each of your verified sending domains has its own contact pool — the same address on two domains is two records with separate consent. That means:

  • Contacts take a domain (required to create/list on the flat /contacts API; disambiguates an email id on get/update/remove).
  • Segments and Topics belong to a domain (domain required on create and list).
  • Campaigns.create REQUIRES domain — it picks the contact pool the campaign targets (the from address may be a different verified domain).
  • Automations.create REQUIRES domain, and Events.send REQUIRES domain — only automations belonging to that domain are triggered, so the same event name (e.g. user.created) across several products can never double-fire.

Resources

Each resource is a class with methods following a consistent create / get / list / update / remove shape (plus resource-specific verbs): Emails (with nested Emails.Receiving), Batch, Domains, Audiences, Contacts, ContactProperties, Campaigns, Segments, Topics, Templates, Automations, Webhooks, Events, ApiKeys (list only — see below), Logs, Polls.

# Emails
mailblastr.Emails.send(params)
mailblastr.Emails.list({"limit": 20, "after": cursor})   # cursor pagination
mailblastr.Emails.list({"status": "bounced", "search": "acme.com"})  # filters
mailblastr.Emails.list({"folder": "sent"})  # outbox / sent / scheduled / failed — any other value 422s
mailblastr.Emails.get(email_id)
mailblastr.Emails.update(email_id, {"scheduled_at": "2026-08-01T09:00:00Z"})  # reschedule
mailblastr.Emails.cancel(email_id)
mailblastr.Emails.sources()                # per-campaign/automation send metrics
mailblastr.Emails.list_attachments(email_id)
mailblastr.Emails.get_attachment(email_id, attachment_id)

# Inbound email
mailblastr.Emails.Receiving.list()
mailblastr.Emails.Receiving.list_addresses()    # per-address inbound stats
mailblastr.Emails.Receiving.get(email_id)
mailblastr.Emails.Receiving.list_attachments(email_id)
mailblastr.Emails.Receiving.get_attachment(email_id, attachment_id)  # -> bytes
mailblastr.Emails.Receiving.get_raw(email_id)                            # -> bytes (RFC822)
mailblastr.Emails.Receiving.forward(email_id, {"from": "me@yourdomain.com", "to": "delivered@mailblastr.dev"})
mailblastr.Emails.Receiving.reply(email_id, {"from": "me@yourdomain.com", "html": "<p>Thanks!</p>"})
mailblastr.Emails.Receiving.remove(email_id)

# Domains (incl. claiming a domain verified elsewhere + one-click DNS)
mailblastr.Domains.create({"name": "yourdomain.com"})
mailblastr.Domains.verify(domain_id)
mailblastr.Domains.claim({"name": "yourdomain.com"})
mailblastr.Domains.verify_claim(domain_id)
mailblastr.Domains.detect_dns(domain_id)
mailblastr.Domains.apply_cloudflare_dns(domain_id, {"token": cf_token})
mailblastr.Domains.mx_check("yourdomain.com")   # live MX lookup
mailblastr.Domains.records_csv(domain_id)       # -> bytes (text/csv)

# Contacts (domain-first)
mailblastr.Contacts.create({"domain": "yourdomain.com", "email": "user@example.com", "first_name": "Ada"})
mailblastr.Contacts.list({"domain": "yourdomain.com"})
mailblastr.Contacts.get({"id": contact_id})                                # by id (exact) …
mailblastr.Contacts.get({"id": "user@example.com", "domain": "yourdomain.com"})  # … or by email + domain
mailblastr.Contacts.update({"id": contact_id, "unsubscribed": True})
mailblastr.Contacts.remove({"id": contact_id})
mailblastr.Contacts.batch({"audience_id": aud_id, "contacts": [{"email": "a@b.com"}]})
# Domain-first: import straight into a domain's pool, no audience id needed.
mailblastr.Contacts.batch({"domain": "yourdomain.com", "contacts": [{"email": "a@b.com"}]})
mailblastr.Contacts.import_csv({"audience_id": aud_id, "csv": "email,company\na@b.com,Acme"})
mailblastr.Contacts.create_import_upload({"audience_id": aud_id, "filename": "big.csv", "size": 90_000_000})
mailblastr.Contacts.add_to_segment(contact_id, segment_id)
mailblastr.Contacts.list_segments(contact_id)
mailblastr.Contacts.update_topics(contact_id, {"topics": [{"id": topic_id, "subscription": "opt_in"}]})

# Contact properties (custom fields / merge tags)
mailblastr.ContactProperties.create({"key": "plan", "type": "string"})

# Campaigns & Segments (domain-first)
mailblastr.Campaigns.create({"domain": "yourdomain.com", "from": sender, "subject": subject, "html": html})
mailblastr.Campaigns.send(campaign_id, {"scheduled_at": "tomorrow at 9am"})
mailblastr.Campaigns.stats(campaign_id)
mailblastr.Campaigns.engagement(campaign_id)   # who opened / clicked / replied
mailblastr.Campaigns.ab(campaign_id)
mailblastr.Segments.create({"domain": "yourdomain.com", "name": "VIP", "filter": {"status": "subscribed"}})
mailblastr.Segments.list({"domain": "yourdomain.com"})
mailblastr.Segments.contacts(segment_id)   # preview who matches

# Topics (domain-first)
mailblastr.Topics.create({"domain": "yourdomain.com", "name": "Product updates", "default_subscription": "opt_in"})
mailblastr.Topics.list({"domain": "yourdomain.com"})

# Templates
mailblastr.Templates.create({"name": "Welcome", "subject": "Hi {{first_name}}", "html": html})
mailblastr.Templates.duplicate(template_id)
mailblastr.Templates.publish(template_id)
mailblastr.Emails.send({"from": sender, "to": to, "template_id": tmpl_id, "variables": {"first_name": "Ada"}})

# Audiences
mailblastr.Audiences.list()
mailblastr.Audiences.import_sheet(audience_id, {"url": sheet_url})

# API keys (listing only — creating, re-scoping and revoking is dashboard-only)
mailblastr.ApiKeys.list()

# Logs & Polls
mailblastr.Logs.list({"limit": 100, "method": "POST", "status": 429})
mailblastr.Logs.get(log_id)
mailblastr.Polls.list()
mailblastr.Polls.get(email_id)

API keys are managed in the dashboard

ApiKeys.list() is the whole surface: the SDK deliberately exposes no method to create, re-scope or revoke a key. Key lifecycle belongs to a signed-in dashboard session, and the API enforces it — POST /api-keys, PATCH /api-keys/:id and DELETE /api-keys/:id answer 403 dashboard_only to any API-key caller, whatever its permission. That is the point: a key that leaks cannot mint itself a replacement, widen its own access, or revoke the keys you would use to shut it off. Create and revoke keys at mailblastr.com instead.

Automations & Events

Every automation belongs to one of your sending domains — domain is required on create, and Events.send names the domain it targets.

automation = mailblastr.Automations.create({
    "name": "Welcome series",
    "domain": "yourdomain.com",
    "trigger": "contact.created",
})

mailblastr.Automations.add_step(automation["id"], {
    "type": "send_email",
    "config": {"template_id": "tmpl_welcome"},
})
mailblastr.Automations.update(automation["id"], {"status": "enabled"})

# Fire a custom event — only yourdomain.com's automations are triggered
mailblastr.Events.send({
    "event": "signup.completed",
    "domain": "yourdomain.com",
    "email": "delivered@mailblastr.dev",
    "payload": {"plan": "pro"},
})
mailblastr.Events.create({"name": "signup.completed", "schema": {"plan": "string"}})
mailblastr.Events.update(event_id, {"schema": {"plan": "string", "seats": "number"}})

# Inspect execution
runs = mailblastr.Automations.runs(automation["id"], {"limit": 25, "status": "failed"})
mailblastr.Automations.get_run(automation["id"], runs["data"][0]["id"])
mailblastr.Automations.stop(automation["id"])

The step graph is edited while the automation is disabledadd_step / update_step / delete_step (and changing domain, trigger or connections) all 422 on an enabled automation. Automations.create_with_ai builds or extends the graph from a prompt:

mailblastr.Automations.create_with_ai(automation["id"], {"prompt": "Wait 2 days, then send the welcome email"})

Webhooks

hook = mailblastr.Webhooks.create({
    "endpoint": "https://yourapp.com/hooks/mailblastr",   # must be https://
    "events": ["email.delivered", "email.bounced", "email.unsubscribed"],
})
signing_secret = hook["signing_secret"]   # shown ONCE, only here

mailblastr.Webhooks.list()
mailblastr.Webhooks.update(hook["id"], {"status": "disabled"})
mailblastr.Webhooks.rotate(hook["id"])    # new secret, returned once
mailblastr.Webhooks.test(hook["id"])

Verify incoming deliveries locally (no HTTP request) — pass the EXACT raw request body string, the svix-* headers, and your signing secret:

result = mailblastr.Webhooks.verify(raw_body, request.headers, signing_secret)
if not result["valid"]:
    abort(401)   # result["reason"] says why, e.g. 'no_match'

Pagination

list() methods accept optional cursor pagination — {"limit", "after", "before"}. limit is an integer 1–100 (default 20); after and before are item ids and cannot be combined. Responses are {"object": "list", "has_more": bool, "data": [...]} — there is no total and no next_cursor, so page forward with the last data[-1]["id"]:

page = mailblastr.Campaigns.list({"limit": 25})
while page["has_more"]:
    page = mailblastr.Campaigns.list({"limit": 25, "after": page["data"][-1]["id"]})

Called with no pagination params, most list endpoints return the whole collection up to a 1,000-item ceiling (Campaigns, Contacts, Segments, ContactProperties, Domains, ApiKeys, Topics, Polls, and the nested contact/segment/topic lists) — past that the response is truncated and has_more is True, so keep paging rather than trusting one call to be complete. Audiences, Automations, Automations.runs, Templates, Webhooks and Events cap at 20 instead — pass limit explicitly when it matters. An unknown cursor is not an error: it returns an empty page with has_more: False.

Idempotency

Pass an idempotency key to safely retry a send — replaying the same key returns the original response instead of sending twice:

mailblastr.Emails.send(params, options={"idempotency_key": "order-123"})
mailblastr.Batch.send(payloads, options={"idempotency_key": "orders-2026-08-08"})
  • The key must be 1 to 255 characters — measured after the server trims it, so 255, not 256 (mailblastr.IDEMPOTENCY_KEY_MAX_LENGTH). The SDK sends the key verbatim and lets the server be the authority: an out-of-range key comes back as a MailblastrError with name == "invalid_idempotency_key" (400).
  • Only Emails.send and Batch.send honour it. Every other endpoint — including Events.send — accepts and forwards the header but the API ignores it, so a retry there creates a second resource. De-duplicate on your side instead.
  • Reusing a key with a different body raises invalid_idempotent_request (409); reusing it while the first request is still running raises concurrent_idempotent_requests (409).

Documentation

Full docs: https://www.mailblastr.com/docs

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mailblastr-5.1.1.tar.gz (55.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mailblastr-5.1.1-py3-none-any.whl (42.4 kB view details)

Uploaded Python 3

File details

Details for the file mailblastr-5.1.1.tar.gz.

File metadata

  • Download URL: mailblastr-5.1.1.tar.gz
  • Upload date:
  • Size: 55.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mailblastr-5.1.1.tar.gz
Algorithm Hash digest
SHA256 b18926ba738e88e14907aaad109b883e8bb512136aad088b2c20f3ba55af9b4f
MD5 09f3f93e6d7798cbbd3624870e2c0a72
BLAKE2b-256 fcca9a6c75a4e1d8d35c4583ff1a8408e9e45e77703ba8e0147e6925302d4a1e

See more details on using hashes here.

Provenance

The following attestation bundles were made for mailblastr-5.1.1.tar.gz:

Publisher: release.yml on shekhu10/mailblastr-sdks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mailblastr-5.1.1-py3-none-any.whl.

File metadata

  • Download URL: mailblastr-5.1.1-py3-none-any.whl
  • Upload date:
  • Size: 42.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mailblastr-5.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4c7ffcdd92a1d8d1ed95837848eb5c26fa4dd366120adb07bbed833d69d833b0
MD5 b0fcf4ce4e50b1db5e27b08c555d80dd
BLAKE2b-256 b298f9d377458c4a4fce2963add8bbfea793763f6a1703bb8231cc55909042de

See more details on using hashes here.

Provenance

The following attestation bundles were made for mailblastr-5.1.1-py3-none-any.whl:

Publisher: release.yml on shekhu10/mailblastr-sdks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

5.1.1 This release

2 files

5.1.0

2 files

5.0.1

2 files

5.0.0

2 files

4.0.0

2 files

3.0.1

2 files

3.0.0

2 files

2.0.0

2 files

1.3.0

2 files

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page