Metadata-Version: 2.4
Name: mailblastr
Version: 3.0.0
Summary: Official MailBlastr Python SDK — send transactional and marketing email from your own verified domain.
Author: MailBlastr
License: MIT
Project-URL: Homepage, https://www.mailblastr.com
Project-URL: Documentation, https://www.mailblastr.com/docs
Project-URL: Repository, https://github.com/shekhu10/mailblastr-sdks
Keywords: mailblastr,email,transactional,marketing,smtp
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# mailblastr

Official Python SDK for the [MailBlastr](https://www.mailblastr.com) email API — send transactional and marketing email from your own verified domain.

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

## Install

```bash
pip install mailblastr
```

## Setup

Grab your API key from the [MailBlastr dashboard](https://www.mailblastr.com).

```python
import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"
```

## Send your first email

```python
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:

```python
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`):

```python
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

```python
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

```python
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:

```python
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`.

```python
# 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](https://www.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.

```python
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:

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

### Webhooks

```python
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:

```python
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"]`:

```python
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:

```python
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
