Skip to main content

altissimo-customerio

CI Python License Code style: ruff

Customer.io transactional email and messaging client for Altissimo Python projects.

Features

  • 📋 Template emails via send_email() — uses Customer.io transactional templates
  • 📧 Plain text emails via send_text() — inline content, no template required
  • 🎨 HTML emails via send_html() — inline content, no template required
  • 🌍 Region support — US and EU data centers
  • 🏗️ Factory method — CustomerIOClient.from_env() reads CUSTOMERIO_API_KEY from the environment
  • 🔌 Optional dependency — customerio SDK is lazily imported with a helpful error message
  • 📐 Typed — full type annotations with py.typed PEP 561 marker
  • ✅ Consistent return type — all methods return a SendResult dataclass
  • 🔄 Retry with backoff — configurable retry for transient failures
  • ✉️ Custom headers — including the RFC 8058 one-click unsubscribe pair
  • 🔔 Fails loudly — send failures raise CustomerIOSendError by default; opt out per client for batch sends

Requirements

  • Python 3.11+

Installation

Published on PyPI as altissimo-customerio; the import name is altissimo.customerio.

pip install "altissimo-customerio[customerio]"   # core + Customer.io SDK

Quick Start

from altissimo.customerio import CustomerIOClient

client = CustomerIOClient.from_env()

# Template email (most common)
result = client.send_email(
    to="user@example.com",
    transactional_message_id="3",
    message_data={"first_name": "Alice", "year": 2026},
)

# Plain text
result = client.send_text(
    to="user@example.com",
    subject="Hello",
    body="Welcome aboard!",
)

# HTML
result = client.send_html(
    to="user@example.com",
    subject="Hello",
    html="<h1>Welcome!</h1>",
    reply_to="support@example.com",
)

print(result.delivery_id)

Error Handling

By default a failed send raises CustomerIOSendError. The originating SDK exception is preserved as __cause__, and the full SendResult is attached as .result:

from altissimo.customerio import CustomerIOClient, CustomerIOSendError

try:
    result = client.send_html(to="user@example.com", subject="Hi", html="<p>Hi</p>")
except CustomerIOSendError as exc:
    logger.error("Reset email failed (status=%s): %s", exc.status_code, exc)
else:
    logger.info("Reset email sent: delivery_id=%s", result.delivery_id)

For batch sends where one bad recipient should not abort the run, opt out and check result.ok yourself:

client = CustomerIOClient.from_env(raise_on_error=False)

for user in users:
    result = client.send_email(to=user.email, transactional_message_id="3")
    if not result.ok:
        failures.append((user.email, result.status_code, result.error))

SendResult.raise_for_status() converts a result to an exception on demand (mirroring requests.Response.raise_for_status), which is handy when you want swallow semantics in one place and raise semantics in another:

client.send_html(to=..., subject=..., html=...).raise_for_status()

Note: the library does not log send failures above DEBUG when raise_on_error=True — it has no recipient or template context worth logging from that frame, and a log there would make the failure look handled. Reporting is the caller's job.

One-Click Unsubscribe (RFC 8058)

Gmail and Yahoo's bulk-sender rules expect notification and marketing mail to carry a one-click unsubscribe. That needs both headers — List-Unsubscribe alone only produces a plain link:

client.send_email(
    to=user.email,
    transactional_message_id="12",
    message_data={"first_name": user.first_name},
    headers={
        "List-Unsubscribe": f"<https://api.example.com/email/unsubscribe?t={token}>",
        "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
    },
    # The opt-out of record is our own database, so Customer.io's global
    # `unsubscribed` flag (which tracks marketing) must not suppress this.
    send_to_unsubscribed=True,
)

Your endpoint must answer an unauthenticated POST with a 2xx and no redirect — RFC 8058 forbids redirecting the POST, because browsers have historically turned redirected POSTs into GETs. Providers will retry, so make it idempotent. Expect GETs on the same URL too: some clients (Apple Mail, Thunderbird) render the header as a link rather than POSTing it, and mail gateways and link scanners fetch URLs found in mail — so a GET should never mutate anything.

Note: send_to_unsubscribed is bool | None. Leave it unset to inherit whatever the transactional message is configured with in the Customer.io dashboard; pass False to explicitly opt out. False is forwarded, not treated as unset.

Tracking and message retention

Customer.io's dashboard defaults a new transactional message to open and click tracking on, and to keeping the rendered body in delivery history. Neither can be changed through Customer.io's API, but both can be overridden on each send:

client.send_email(
    to=user.email,
    transactional_message_id="password_reset",
    message_data={"action_url": reset_link},
    # Click tracking rewrites links through a redirect, which can break one-time codes.
    tracked=False,
    # Keep the reset link out of delivery history.
    disable_message_retention=True,
)

Both are bool | None, like send_to_unsubscribed: unset inherits the message's dashboard setting, and False is forwarded. tracked covers opens and clicks together.

send_text() and send_html() accept both too, but an inline send has no transactional message to inherit from: unset there means Customer.io's own default, which is tracking on and the body retained. Pass tracked=False and disable_message_retention=True explicitly for inline mail that carries one-time links or codes.

Configuration

Environment Variables

Variable Description Default
CUSTOMERIO_API_KEY Customer.io App API key (required)
CUSTOMERIO_REGION Data center region (us or eu) us

Constructor

client = CustomerIOClient(
    app_api_key="your-api-key",
    region="us",  # "us" or "eu"
    default_from="noreply@lived.com",  # default sender
    max_retries=3,  # retry transient failures
    retry_delay=1.0,  # base delay (seconds)
    raise_on_error=True,  # raise CustomerIOSendError on failure (default)
)

API Reference

CustomerIOClient

Method Description
CustomerIOClient(app_api_key, region?, default_from?, raise_on_error?) Create a client with an explicit API key
CustomerIOClient.from_env(env_var?, region?, default_from?, raise_on_error?) Create a client from an environment variable
send_email(to, transactional_message_id, message_data?, ...) Send a template-based transactional email
send_text(to, subject, body, from_email?, reply_to?, ...) Send a plain-text email (inline content)
send_html(to, subject, html, from_email?, reply_to?, ...) Send an HTML email (inline content)

SendResult

Field Type Description
ok bool Whether the request succeeded
delivery_id str Delivery ID from Customer.io
status_code int HTTP status code (0 on exception)
body dict[str, Any] Response body
error str | None Error message on failure
exception Exception | None Originating SDK exception on failure
Method Description
raise_for_status(context?) Raise CustomerIOSendError if ok is False; no-op otherwise

Exceptions

Exception Raised when
CustomerIOError Base class for all library errors
CustomerIOImportError The customerio SDK is not installed
CustomerIOSendError A send failed (carries status_code and result)

Architecture

altissimo.customerio
├── __init__.py       # Public API surface
├── client.py         # CustomerIOClient with lazy SDK initialization
├── exceptions.py     # CustomerIOError, CustomerIOImportError, CustomerIOSendError
├── models.py         # EmailAddress, SendResult dataclasses
└── py.typed          # PEP 561 marker

Development

# Install all dependencies
poetry sync

# Run tests
poetry run pytest

# Run tests with coverage
poetry run pytest --cov=altissimo --cov-report=term-missing

# Run linters
poetry run ruff check .
poetry run ruff format --check .

See CONTRIBUTING.md for detailed development guidelines.

Changelog

See CHANGELOG.md for version history.

Security

For reporting security vulnerabilities, see SECURITY.md.

License

Apache License 2.0 — see LICENSE for details.

Metadata

Release files for altissimo-customerio 0.4.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 altissimo-customerio 0.4.0
File Size Uploaded Provenance
altissimo_customerio-0.4.0.tar.gz 20.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for altissimo-customerio 0.4.0
File Interpreter ABI Platform
altissimo_customerio-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.1 kB

Release files / altissimo_customerio-0.4.0.tar.gz

Download URL altissimo_customerio-0.4.0.tar.gz
Size 20.4 kB
Tags Source
SHA-256 checksum
How to use checksums
1a3a90965e31b9825d56550928f0eccce7c081a5940969d64e05e02bafcaf3d7
BLAKE2b-256 checksum
How to use checksums
dc415529fcedd21c158ee30b154c79f702b90cf722fbef17d64409a856c0a266
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

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 Oct 26, 2026.

Transparency log

Release files / altissimo_customerio-0.4.0-py3-none-any.whl

Download URL altissimo_customerio-0.4.0-py3-none-any.whl
Size 19.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8aeb72215d19c43c3b3099f639cb5530b4a83dc526de29fc59d5559f03637400
BLAKE2b-256 checksum
How to use checksums
e02f29b97df43c8e278af880b1715cb455c3fa41be6f8502571c68f7bd6c69fb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

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 Oct 26, 2026.

Transparency log

Security for altissimo-customerio 0.4.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for altissimo_customerio-0.4.0.tar.gz

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 Oct 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files Full provenance

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