Skip to main content

Mailcheer for Python

The official Python SDK for Mailcheer: transactional emails, subscribers, campaigns and signed webhooks — blocking or asyncio, fully typed.

  • A blocking client and an asyncio client, with the same methods.
  • Typed responses (TypedDict) and typed errors with a stable code.
  • Safe automatic retries: Retry-After respected, and a send is only retried with an idempotency key.
  • Your monthly quota read from every response.
  • Webhook signatures verified in one line.
pip install mailcheer

Send an email

Create a key under Settings → API in your workspace, then:

from mailcheer import Mailcheer

mailcheer = Mailcheer("mch_live_…")  # or set MAILCHEER_API_KEY and call Mailcheer()

email = mailcheer.emails.send({
    "from": "Acme <billing@acme.com>",
    "to": "jane@example.com",
    "subject": "Your September invoice",
    "html": "<p>Here it is.</p>",
    "text": "Here it is.",
})
print(email["id"])  # accepted (202), on its way

from must be on a domain verified in your workspace. mailcheer.me() lists your verified domains and senders.

Responses are plain dictionaries, shaped exactly like the API's JSON and typed for your editor.

Errors

A refusal raises a MailcheerError. Read error.code: it is stable and never translated.

from mailcheer import MailcheerError, QuotaExceededError

try:
    mailcheer.emails.send(email)
except QuotaExceededError as error:
    print("Quota reached; resets", error.details["resets_at"])
except MailcheerError as error:
    print(error.code, error.message, error.status_code, error.details)
Class When
AuthenticationError 401 — missing, unknown or revoked key
QuotaExceededError 402 — quota_exceeded, free_plan_domain_used
PermissionDeniedError 403 — missing permission, sending blocked
NotFoundError 404
ConflictError 409 — idempotency key reused with another body…
ValidationError 422 — faulty field, unverified domain, suppressed recipient
SendingPausedError 423 — workspace under review: send the same call later
RateLimitError 429 — retry_after tells how long to wait
ServerError 5xx
MailcheerConnectionError no answer: network_error or timeout

Messages are in English by default; Mailcheer(language="fr") asks for French.

Retries and idempotency

Pass an idempotency_key — an invoice number, an order id — and a send can be retried as often as needed: it goes out once. Mailcheer replays the first response for 24 hours.

mailcheer.emails.send(email, idempotency_key=f"invoice-{invoice.id}")

The SDK retries on its own (2 retries by default) after a network failure, a timeout, a 429 or a 5xx, waiting for Retry-After when the API gives it. It only retries a call that changes something when that call carries an idempotency key — or when the API refused it before reading it (rate_limit_exceeded). A retry never sends an email twice.

Your quota, on every response

mailcheer.emails.send(email)
mailcheer.last_quota     # {"limit": 3000, "used": 2531, "remaining": 469, "reset_at": "2026-10-01T00:00:00.000Z"}
mailcheer.last_response  # status, quota, rate_limit, idempotent_replay, retries
mailcheer.quota()        # read it now, from GET /api/v1/me

limit and remaining are None on an unlimited plan.

A key with its own monthly limit (Settings → API) also reports it in mailcheer.last_key_quota (limit, used, remaining); beyond it a send raises QuotaExceededError with code == "key_quota_exceeded".

With a test key (mch_test_…), every call is checked as in production but nothing is sent and the quota is not touched: mailcheer.last_response.mode is "test", and mailcheer.me()["key"]["mode"] says so.

Subscribers and campaigns

mailcheer.subscribers.upsert({"email": "jane@example.com", "firstName": "Jane", "tags": ["customer"]})

for subscriber in mailcheer.subscribers.list_all(status="subscribed"):
    print(subscriber["email"])

batch = mailcheer.subscribers.batch(contacts, idempotency_key="import-2026-09")  # up to 500
changed = mailcheer.subscribers.list(updated_since=last_sync)
mailcheer.subscribers.update_tags("jane@example.com", add=["vip"], remove=["lead"])
mailcheer.tags.update_subscribers("vip", add=["joe@example.com"])
mailcheer.subscribers.erase("jane@example.com")  # GDPR erasure, irreversible

draft = mailcheer.campaigns.create({"name": "October newsletter", "subject": "What's new", "text": "# Hello\n\nThree new things…"})
preview = mailcheer.campaigns.send(draft["id"], {"dry_run": True})  # checks everything, sends nothing
mailcheer.campaigns.send(draft["id"])                                # irreversible
stats = mailcheer.campaigns.stats(draft["id"])

Also: subscribers.get(), subscribers.unsubscribe(), suppression.list(), suppression.add(), campaigns.update() (a draft), campaigns.list_recipients(), campaigns.preview_audience(to=[…]), tags.list(), tags.rename(), tags.delete(), and the webhooks resource.

asyncio

from mailcheer import AsyncMailcheer

async with AsyncMailcheer() as mailcheer:
    email = await mailcheer.emails.send({...}, idempotency_key="invoice-42")
    async for subscriber in mailcheer.subscribers.list_all():
        ...

Webhooks

Verify the signature on the raw body, before parsing:

# Flask
from mailcheer import WebhookSignatureError, construct_webhook_event, is_test_event

@app.post("/mailcheer")
def mailcheer_webhook():
    try:
        event = construct_webhook_event(
            os.environ["MAILCHEER_WEBHOOK_SECRET"],
            request.headers.get("Mailcheer-Signature"),
            request.get_data(),
        )
    except WebhookSignatureError:
        return "Invalid signature", 400
    if is_test_event(event):
        return "", 200
    if event["type"] == "email.bounced":
        mark_bounced(event["data"]["email"])
    return "", 200

With Django, pass request.body; with FastAPI, await request.body(). A call signed more than five minutes ago is refused. event["id"] is the same on every attempt: use it to ignore duplicates. An event simulated for a test key carries event.get("test") is True, with the fields of a real event — is_test_event() only recognises the ping of the “Send a test” button.

Options

Mailcheer(
    api_key,
    base_url="https://mailcheer.com",  # or MAILCHEER_BASE_URL
    max_retries=2,
    timeout=60.0,          # per attempt, in seconds
    max_retry_after=60.0,  # the longest Retry-After waited for on its own
    language="en",         # "fr" for French messages
    headers={},            # added to every request
    http_client=None,      # your own httpx.Client (proxy, transport…)
)

For a route not wrapped here, mailcheer.request("GET", "/api/v1/…", query=…, body=…) gives the same retries and errors.

Python 3.9 or later.

License

MIT

Release files for mailcheer 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mailcheer 0.2.0
File Size Uploaded
mailcheer-0.2.0.tar.gz 25.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mailcheer 0.2.0
File Interpreter ABI Platform
mailcheer-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 49.6 kB

Release files / mailcheer-0.2.0.tar.gz

Download URL mailcheer-0.2.0.tar.gz
Size 25.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8e1909ff7f8cf58618bdeab0c81bc3191218853003c42d390685bf4ba322f416
BLAKE2b-256 checksum
How to use checksums
ab49cfd4f7b6d2d9dc5e4494b40e9785d547b324bee3bf9038317e51b028e451
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / mailcheer-0.2.0-py3-none-any.whl

Download URL mailcheer-0.2.0-py3-none-any.whl
Size 24.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5df056332162cda66eb8d16f77e28946c0838cf34fe5533d86605c51f2dd4589
BLAKE2b-256 checksum
How to use checksums
ad1d8c225657af1a9310fbf9d68659f2c523ecec7d2eea7cf444efe8ba52e37e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release 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