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
asyncioclient, with the same methods. - Typed responses (
TypedDict) and typed errors with a stablecode. - Safe automatic retries:
Retry-Afterrespected, 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.
Links
License
MIT
Metadata
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)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mailcheer-0.2.0.tar.gz | 25.6 kB | Without provenance | Details for mailcheer-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mailcheer-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for mailcheer-0.2.0-py3-none-any.whl |
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
|
Security for mailcheer 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.