Metadata-Version: 2.4
Name: mailkube
Version: 1.2.0
Summary: Mailkube Python SDK
Project-URL: Homepage, https://github.com/mailkube/mailkube
Project-URL: Repository, https://github.com/mailkube/mailkube
Project-URL: Issues, https://github.com/mailkube/mailkube/issues
Author: Mailtactic, Corp.
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: mailkube,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: <4.0,>=3.12
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic<3,>=2.7
Description-Content-Type: text/markdown

# mailkube

[![CI](https://github.com/mailkube/mailkube/actions/workflows/ci.yml/badge.svg)](https://github.com/mailkube/mailkube/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/mailkube)](https://pypi.org/project/mailkube/)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.12%2B-blue)](pyproject.toml)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-2.1-purple.svg)](CODE_OF_CONDUCT.md)

The official Python SDK for [Mailkube](https://mailkube.com) — send transactional email and verify
inbound webhooks. Fully typed, sync **and** async, Python 3.12+.

Full product and API documentation: [mailkube.com/docs](https://mailkube.com/docs).

## Install

```bash
pip install mailkube
# or
uv add mailkube
```

## Configuration

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

```python
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`](src/mailkube/types/params.py) for the full field list (`cc`, `bcc`,
`reply_to`, `headers`, `attachments`, `tags`, `template_id`, `topic`, `idempotency_key`, …).

### Async

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

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

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

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

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

```python
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/`](examples):

- [`simple_send.py`](examples/simple_send.py) — basic sync send
- [`async_send.py`](examples/async_send.py) — async send, then thread a reply onto it
- [`send_with_attachments.py`](examples/send_with_attachments.py) — attach a file from raw bytes
- [`send_with_tags.py`](examples/send_with_tags.py) — tag a send for filtering and reporting
- [`send_with_template.py`](examples/send_with_template.py) — send from a saved template
- [`webhook_receiver_flask.py`](examples/webhook_receiver_flask.py) — verify and dispatch webhooks
  in a Flask app

## Contributing

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

## License

[Apache-2.0](LICENSE) © 2026 Mailtactic, Corp.
