Skip to main content

mailkube

CI PyPI License: Apache 2.0 Python Ruff Code of Conduct

The official Python SDK for Mailkube — send transactional email and verify inbound webhooks. Fully typed, sync and async, Python 3.12+.

Full product and API documentation: mailkube.com/docs.

Install

pip install mailkube
# or
uv add mailkube

Configuration

from mailkube import Mailkube

client = Mailkube(
    api_key="mk_...",                    # or set MAILKUBE_API_KEY
    base_url="https://api.mailkube.com/mta/v1/",  # or set MAILKUBE_BASE_URL; this is the default
    timeout=30.0,                        # per-request timeout in seconds
)

AsyncMailkube takes the same arguments. Both also accept http_client= — your own httpx.Client / httpx.AsyncClient — if you need custom transport, proxies, or mTLS; the SDK will not close a client you pass in.

Send an email

from mailkube import Mailkube

with Mailkube() as client:
    email = client.emails.send(
        from_="Acme <hello@yourdomain.com>",
        to="customer@example.com",
        cc="manager@example.com",
        reply_to="support@yourdomain.com",
        subject="Hello world",
        html="<p>It works!</p>",
    )
    print(email.id, email.message_id)

from_ maps to the wire from field (from is a reserved keyword). Supply html and/or text, or a template_id with template_version and variables. Attachments accept raw bytes or a base64 string. See SendEmailParams for the full field list (cc, bcc, reply_to, headers, attachments, tags, template_id, topic, idempotency_key, …).

Async

import asyncio
from mailkube import AsyncMailkube

async def main():
    async with AsyncMailkube() as client:
        email = await client.emails.send(
            from_="Acme <hello@yourdomain.com>",
            to="customer@example.com",
            subject="Hello world",
            html="<p>It works!</p>",
        )
        print(email.id)

asyncio.run(main())

Idempotency

Pass idempotency_key to safely retry a send without risking a duplicate — sent as the Idempotency-Key header, not in the body:

email = client.emails.send(
    from_="Acme <hello@yourdomain.com>",
    to="customer@example.com",
    subject="Your receipt",
    html="<p>Thanks for your order.</p>",
    idempotency_key="order-4821-receipt",
)
print(email.idempotent_replayed)  # True if this key was already used

Reusing a key with a different payload raises ConflictError instead of silently sending the new content.

Errors

Every failure raises a subclass of MailkubeError (AuthenticationError, InvalidRequestError, RateLimitError — which carries .retry_after — ServerError, MailkubeConnectionError, …). Each API error exposes .error_name, .message, and .status_code.

Threading

Echo a message's message_id in the In-Reply-To / References headers of a later send:

reply = client.emails.send(
    from_="Acme <hello@yourdomain.com>",
    to="customer@example.com",
    subject="Re: Your order",
    html="<p>An update.</p>",
    headers={"In-Reply-To": first.message_id, "References": first.message_id},
)

Tags

Attach free-form {"name": ..., "value": ...} pairs to a send. The server denormalizes them onto the sending-log, so you can filter, export, and dashboard by tag, and they ride along on delivery webhooks:

email = client.emails.send(
    from_="Acme <hello@yourdomain.com>",
    to="customer@example.com",
    subject="Welcome aboard",
    html="<p>Glad you're here.</p>",
    tags=[{"name": "campaign", "value": "spring24"}, {"name": "plan", "value": "pro"}],
)

Validation is server-side: names and values allow the [A-Za-z0-9_-] charset and up to 256 characters each, values may be blank, at most 20 tags per send, and names must be unique. Tag values are not encrypted, so keep personal data out of them.

Verify webhooks

webhooks.verify is a pure, stdlib-only helper — call it in your request handler with the raw body:

from mailkube import verify, SignatureVerificationError, UnknownEvent

try:
    event = verify(raw_body, request.headers, signing_secret)
except SignatureVerificationError:
    ...  # reject with 400

if isinstance(event, UnknownEvent):
    ...  # a newer event type than this SDK version knows about — still usable
else:
    print(event.type)

An unrecognized event type is returned as UnknownEvent instead of raising, so a new server event type never forces an SDK upgrade on receivers.

Logging

Silent by default. Turn on request/response logging with:

import mailkube

mailkube.enable_logging(level="DEBUG")

or set the MAILKUBE_LOG environment variable. Authorization and Idempotency-Key headers are redacted from log output.

Client lifecycle

Create one client and reuse it. Mailkube is thread-safe; AsyncMailkube is bound to its event loop. Use the client as a (async) context manager, or call .close() / .aclose().

More examples

Runnable scripts in examples/:

Contributing

See CONTRIBUTING.md for the development setup and the quality gates every change must pass. Security issues: see SECURITY.md.

License

Apache-2.0 © 2026 Mailtactic, Corp.

Release files for mailkube 1.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 mailkube 1.2.0
File Size Uploaded
mailkube-1.2.0.tar.gz 71.9 kB Details

Built distribution (wheel)

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

Total release size: 98.8 kB

Release files / mailkube-1.2.0.tar.gz

Download URL mailkube-1.2.0.tar.gz
Size 71.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4c5ac90d0d4b6be8ef3b9bc644b85a990265e0743ef0b84bf00f9a4a0be138db
BLAKE2b-256 checksum
How to use checksums
de0f53a6575935b42feefe208d36388ac7d858b1612a545b7b39f10189bb3816
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.

Transparency log

Release files / mailkube-1.2.0-py3-none-any.whl

Download URL mailkube-1.2.0-py3-none-any.whl
Size 26.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
04e34c1b024309542fb6ac79a0d8dcb2430157642b8252aa156c7299d92ff6b3
BLAKE2b-256 checksum
How to use checksums
f70d25e5f29f3f4eb1a40c08b9a33fc1f7553e4496a1ce66169c515c926dac25
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.

Transparency log

Release history Release notifications | RSS feed

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

This release

1.2.0 This release

2 release files

1.1.1

2 release files

1.1.0

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