Skip to content

Getting Started

Notificator requires Python 3.13+.

Installation

uv add hexed-notificator
pip install hexed-notificator

Send an email

Import MailgunClient from notificator.mail and NotificationContent from notificator:

import asyncio

from notificator import EmailAddress, NotificationContent
from notificator.mail import MailgunClient


async def main() -> None:
    client = MailgunClient(
        domain="mg.example.com",
        api_key="key-...",
        sender_email="no-reply@example.com",
        sender_display_name="My Service",
        default_subject="Service Notification",
    )

    await client.notify(
        NotificationContent(body="Hello from Notificator!"),
        recipient=EmailAddress("user@example.com"),
    )
    await client.aclose()


asyncio.run(main())

Subject resolution

notify() requires a subject. Provide it via NotificationContent(subject=...), or set a default_subject on the client. If neither is supplied, EmailNotificationMissingSubjectError is raised before any network call is made.

Send an SMS

Import TwilioSmsClient from notificator.sms:

import asyncio

from notificator import NotificationContent, PhoneNumber
from notificator.sms import TwilioSmsClient


async def main() -> None:
    client = TwilioSmsClient(
        account_sid="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        token="your_auth_token",
        sender_phone_number=PhoneNumber("+15551234567"),
    )

    await client.notify(
        NotificationContent(body="Your verification code is 123456."),
        recipient=PhoneNumber("+15557654321"),
    )
    await client.aclose()


asyncio.run(main())

Phone number format

Phone numbers must be in E.164 format (e.g. +15551234567). InvalidPhoneNumberFormatError is raised for any number that fails validation.

Resource cleanup

Both clients hold open connections. Always call aclose() when done — use a try/finally block to ensure it runs even if an error occurs:

async def send(body: str) -> None:
    client = MailgunClient(...)
    try:
        await client.notify(NotificationContent(body=body), recipient=EmailAddress("user@example.com"))
    finally:
        await client.aclose()

Error handling

All exceptions inherit from NotificationError, so you can catch at whatever level of specificity you need:

from notificator import NotificationError
from notificator.mail import MailAPIError, MalformedRecipientEmailError

try:
    await client.notify(content, recipient=EmailAddress("not-an-email"))
except MalformedRecipientEmailError as e:
    # Validation failed locally — no HTTP call was made
    print(f"Invalid address: {e.recipient}")
except MailAPIError:
    # The Mailgun API returned an error response
    print("Delivery failed — check Mailgun logs.")
except NotificationError:
    # Catch-all for any other notificator error
    raise

For a full breakdown of errors per provider, see Provider Configuration.

Next steps