Official Python SDK for the Maildesk API
Project description
maildeskio
Official Python SDK for the Maildesk API. Typed, mypy --strict Python 3.9+.
Covers:
- Contacts — list, get, create, bulk create (with partial-failure reporting), update, delete, auto-paginate
- Tags — list, get, create, update, delete, auto-paginate
- Webhooks — HMAC-SHA256 signature verification + typed event payloads
Built on httpx and Pydantic v2.
Install
pip install maildeskio
Quick start
from maildeskio import Maildesk
client = Maildesk(api_key="sk_live_...")
contact = client.contacts.create(
email="jane@example.com",
first_name="Jane",
last_name="Doe",
is_confirmed=True,
tags=["01HQZTAG00000000000000"],
)
print(contact.id, contact.email)
Use it as a context manager to close the underlying connection pool:
with Maildesk(api_key="sk_live_...") as client:
for c in client.contacts.list_all():
print(c.email)
Authentication
Generate an API secret key from the Maildesk dashboard under Developer → API keys and pass it as api_key. The SDK sends it as a Bearer token on every request.
Configuration
| Parameter | Type | Default | Notes |
|---|---|---|---|
api_key |
str | — | Required. Your secret key. |
base_url |
str | http://localhost:3000 |
Point at your Maildesk deployment. |
timeout |
float | 30.0 |
Per-request timeout in seconds. |
Contacts
# List (paginated)
result = client.contacts.list(page=1, limit=50)
print(result.total, [c.email for c in result.subscribers])
# Get one
contact = client.contacts.get("01ARZ3NDEKTSV4RRFFQ69G5FAV")
# Create
created = client.contacts.create(
email="new@example.com",
first_name="New",
last_name="Person",
is_confirmed=True,
tags=["01HQZTAG00000000000000"],
)
# Update (partial)
updated = client.contacts.update(created.id, first_name="Renamed")
# Delete
client.contacts.delete(created.id)
# Auto-paginate every contact
for c in client.contacts.list_all(limit=100):
print(c.email)
Bulk create — partial failures
bulk_create accepts up to 100 contacts per call and returns a result with a failed list giving per-item reasons. A fully successful batch has an empty failed list.
from maildeskio import BulkContactFailureReason
result = client.contacts.bulk_create([
{"email": "a@example.com", "first_name": "A", "last_name": "Z"},
{"email": "dup@example.com", "first_name": "D", "last_name": "U"},
])
for failure in result.failed:
if failure.reason is BulkContactFailureReason.EMAIL_ALREADY_EXISTS:
pass # already in the database
elif failure.reason is BulkContactFailureReason.DUPLICATE_IN_REQUEST:
pass # two rows in the same batch had the same email
else: # INTERNAL_ERROR
pass # retry
Tags
result = client.tags.list()
tag = client.tags.create(name="Newsletter", description="Weekly list")
client.tags.update(tag.id, description="Updated")
client.tags.delete(tag.id)
for t in client.tags.list_all():
print(t.name)
Tag names are unique per business — creating a duplicate raises ConflictError.
Webhooks
Maildesk signs every outbound webhook with HMAC-SHA256 using your API secret. Use verify_webhook to validate signatures and get a typed event.
Event types:
subscriber.createdsubscriber.confirmedsubscriber.unsubscribedsubscriber.updated
FastAPI example
import os
from fastapi import FastAPI, Request, HTTPException
from maildeskio import verify_webhook, InvalidSignatureError, StaleWebhookError, WebhookEventType
app = FastAPI()
SECRET = os.environ["MAILDESK_WEBHOOK_SECRET"]
@app.post("/webhooks/maildesk")
async def maildesk_webhook(request: Request):
raw_body = await request.body() # bytes — do NOT parse JSON first
try:
event = verify_webhook(
raw_body=raw_body,
signature_header=request.headers.get("X-Maildesk-Signature"),
secret=SECRET,
)
except (InvalidSignatureError, StaleWebhookError) as exc:
raise HTTPException(status_code=400, detail=str(exc))
if event.type is WebhookEventType.SUBSCRIBER_UNSUBSCRIBED:
# ...
pass
return {"ok": True}
Flask example
from flask import Flask, request, abort
from maildeskio import verify_webhook, InvalidSignatureError, StaleWebhookError
app = Flask(__name__)
@app.post("/webhooks/maildesk")
def maildesk_webhook():
try:
event = verify_webhook(
raw_body=request.get_data(), # raw bytes
signature_header=request.headers.get("X-Maildesk-Signature"),
secret=app.config["MAILDESK_WEBHOOK_SECRET"],
)
except (InvalidSignatureError, StaleWebhookError) as exc:
abort(400, str(exc))
# ... process event.type, event.email, event.event_id
return "ok"
Options
verify_webhook(
raw_body=..., # bytes or str — raw request body
signature_header=..., # value of X-Maildesk-Signature
secret=..., # your API secret key
tolerance_seconds=300, # default — reject timestamps older than 5 minutes. 0 disables.
)
Idempotency
Every event has a unique event_id. Retries (e.g. after your endpoint returns 5xx) reuse the same event_id, so persist it to skip duplicates:
event = verify_webhook(...)
if seen_event_ids.contains(event.event_id):
return
seen_event_ids.add(event.event_id)
# process event
Error handling
All SDK errors inherit from MaildeskError and carry status_code, body, and an optional request_id.
| Exception | Trigger |
|---|---|
AuthenticationError |
401 — missing/invalid API key |
NotFoundError |
404 — resource not found |
ConflictError |
409 — unique-constraint violation |
ValidationError |
400 / 422 — request invalid |
RateLimitError |
429 — includes retry_after seconds |
ServerError |
5xx |
NetworkError |
transport-level failure |
InvalidSignatureError |
webhook signature mismatch |
StaleWebhookError |
webhook timestamp outside tolerance window |
from maildeskio import RateLimitError
import time
try:
client.contacts.create(email=..., first_name=..., last_name=...)
except RateLimitError as e:
if e.retry_after:
time.sleep(e.retry_after)
# retry
raise
The server enforces 120 requests per 60 seconds per API key — back off on 429.
Typing
The SDK ships a py.typed marker; all public methods are annotated. You can run mypy --strict against your code and get full type-checking through the SDK boundary. Responses are Pydantic v2 models, so you can use .model_dump() / .model_dump_json() on any returned object.
Development
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # runs the test suite with pytest-httpx
mypy src/ # strict type-checking
ruff check . # lint
License
MIT
Project details
Release history Release notifications | RSS feed
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 maildeskio-0.1.0.tar.gz.
File metadata
- Download URL: maildeskio-0.1.0.tar.gz
- Upload date:
- Size: 13.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12c51d19c2877aaf917bfd7513efe4b71551a54d1b495e4f18c2d8b0b3e23b2f
|
|
| MD5 |
7515b5da83129cd61df27aacfc3c4b8b
|
|
| BLAKE2b-256 |
690034805027e156d123c4ea726c44a2895a8004c2323534741f6ba0e6dea99a
|
Provenance
The following attestation bundles were made for maildeskio-0.1.0.tar.gz:
Publisher:
publish.yml on KadenLLC/python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maildeskio-0.1.0.tar.gz -
Subject digest:
12c51d19c2877aaf917bfd7513efe4b71551a54d1b495e4f18c2d8b0b3e23b2f - Sigstore transparency entry: 1356224401
- Sigstore integration time:
-
Permalink:
KadenLLC/python-sdk@e2ec9c12385be1d81b4d6ceb906ba23bc037be93 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/KadenLLC
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e2ec9c12385be1d81b4d6ceb906ba23bc037be93 -
Trigger Event:
push
-
Statement type:
File details
Details for the file maildeskio-0.1.0-py3-none-any.whl.
File metadata
- Download URL: maildeskio-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9606b09484ef560c411ee49478e9e5ed6b73958f89f89cbb9bbd949cd2b9c3a9
|
|
| MD5 |
68f33bfbc49ca4dee7508ba1255452db
|
|
| BLAKE2b-256 |
c1de71866be57dfb303cdd295cf1485a4fdc0b0dacb2c09f3dfff2ddd5bf8d2b
|
Provenance
The following attestation bundles were made for maildeskio-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on KadenLLC/python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maildeskio-0.1.0-py3-none-any.whl -
Subject digest:
9606b09484ef560c411ee49478e9e5ed6b73958f89f89cbb9bbd949cd2b9c3a9 - Sigstore transparency entry: 1356224407
- Sigstore integration time:
-
Permalink:
KadenLLC/python-sdk@e2ec9c12385be1d81b4d6ceb906ba23bc037be93 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/KadenLLC
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e2ec9c12385be1d81b4d6ceb906ba23bc037be93 -
Trigger Event:
push
-
Statement type: