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": ["user@example.com"],
"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. 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": ["user@example.com"],
"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": ["a@example.com"], "subject": "Hi A", "html": "<p>A</p>"},
{"from": "hello@yourdomain.com", "to": ["b@example.com"], "subject": "Hi B", "html": "<p>B</p>"},
]) # up to 100 emails per request
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:
Contactstake adomain(required to create/list on the flat/contactsAPI; disambiguates an email id on get/update/remove).SegmentsandTopicsbelong to a domain (domainrequired on create and list).Campaigns.createREQUIRESdomain— it picks the contact pool the campaign targets (thefromaddress may be a different verified domain).Automations.createREQUIRESdomain, andEvents.sendREQUIRESdomain— 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.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": "team@you.com"})
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"}]})
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": "user@example.com",
"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 disabled —
add_step / update_step / delete_step (and changing domain, trigger or
connections) all 422 on an enabled automation. Automations.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 (Campaigns, Contacts, Segments, ContactProperties,
Domains, ApiKeys, Topics, Polls, and the nested contact/segment/topic
lists). 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 aMailblastrErrorwithname == "invalid_idempotency_key"(400). - Only
Emails.sendandBatch.sendhonour it. Every other endpoint — includingEvents.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 raisesconcurrent_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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mailblastr-3.0.0.tar.gz.
File metadata
- Download URL: mailblastr-3.0.0.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
289eb7090d093860aa1ee09e613c0605a250db248bf1a936501f49bd57e04ae5
|
|
| MD5 |
0fb6d30a28229d9dcef676fbbd80da70
|
|
| BLAKE2b-256 |
22dcb043ad967bf1609f34f835a2e17978ce9660d7b72e97faa5e615dcae3b3f
|
Provenance
The following attestation bundles were made for mailblastr-3.0.0.tar.gz:
Publisher:
release.yml on shekhu10/mailblastr-sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mailblastr-3.0.0.tar.gz -
Subject digest:
289eb7090d093860aa1ee09e613c0605a250db248bf1a936501f49bd57e04ae5 - Sigstore transparency entry: 2387839766
- Sigstore integration time:
-
Permalink:
shekhu10/mailblastr-sdks@ed53b8dd219e04706c09bea17f5b1782c35b862a -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/shekhu10
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ed53b8dd219e04706c09bea17f5b1782c35b862a -
Trigger Event:
push
-
Statement type:
File details
Details for the file mailblastr-3.0.0-py3-none-any.whl.
File metadata
- Download URL: mailblastr-3.0.0-py3-none-any.whl
- Upload date:
- Size: 35.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a59613e2b83a60a2b5d7d659d785f67b7015a380cd6cae8a007bad5591cdf655
|
|
| MD5 |
e3a3742af571e04205f31f898bf2c4e8
|
|
| BLAKE2b-256 |
f8165797124f2a61b1625ee2f87408695d6b5646b277e119ad012dcf34f971ef
|
Provenance
The following attestation bundles were made for mailblastr-3.0.0-py3-none-any.whl:
Publisher:
release.yml on shekhu10/mailblastr-sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mailblastr-3.0.0-py3-none-any.whl -
Subject digest:
a59613e2b83a60a2b5d7d659d785f67b7015a380cd6cae8a007bad5591cdf655 - Sigstore transparency entry: 2387839775
- Sigstore integration time:
-
Permalink:
shekhu10/mailblastr-sdks@ed53b8dd219e04706c09bea17f5b1782c35b862a -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/shekhu10
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ed53b8dd219e04706c09bea17f5b1782c35b862a -
Trigger Event:
push
-
Statement type: