The maildeno PyPI package is the official Python SDK. It ships two clients:

  • MaildenoClient — synchronous (WSGI, scripts, Celery)

  • AsyncMaildenoClient — asynchronous (asyncio, FastAPI, Starlette)

pip install maildeno
# or
poetry add maildeno
# or
uv add maildeno

Requires Python 3.9+ and httpx >= 0.28.1.

30-second example

  • Sync

  • Async

import os
from maildeno import MaildenoClient

client = MaildenoClient(api_key=os.environ["MAILDENO_API_KEY"])

html = client.render_html("550e8400-e29b-41d4-a716-446655440000", {
    "merge_tags": {
        "text": {"name": "Noruwa", "company": "Maildeno"},
        "url":  {"reset_url": "https://app.example.com/reset/abc123"},
    },
    "context": {"plan": "pro"},
})

print(html)  # <!DOCTYPE html>...
import asyncio
import os
from maildeno import AsyncMaildenoClient

async def main():
    async with AsyncMaildenoClient(api_key=os.environ["MAILDENO_API_KEY"]) as client:
        html = await client.render_html(
            "550e8400-e29b-41d4-a716-446655440000",
            {
                "merge_tags": {"text": {"name": "Noruwa"}},
                "context":    {"plan": "pro"},
            },
        )
        print(html)

asyncio.run(main())

Exported types

from maildeno import (
    MaildenoClient,
    AsyncMaildenoClient,
    MaildenoError,
)
from maildeno import (
    RenderTarget,     # Literal["html", "react-email", "mjml"]
    RenderResult,     # frozen dataclass
    DynamicData,      # TypedDict({merge_tags?, context?})
    MergeTagGroup,    # TypedDict({text?, url?, attr?})
    ContextValue,     # str | int | float | bool
    SdkErrorCode,     # Literal[...] of error code strings
    ValidationIssue,  # TypedDict for entries on err.issues
)