altissimo-customerio
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()readsCUSTOMERIO_API_KEYfrom the environment - 🔌 Optional dependency —
customerioSDK is lazily imported with a helpful error message - 📐 Typed — full type annotations with
py.typedPEP 561 marker - ✅ Consistent return type — all methods return a
SendResultdataclass - 🔄 Retry with backoff — configurable retry for transient failures
- ✉️ Custom headers — including the RFC 8058 one-click unsubscribe pair
- 🔔 Fails loudly — send failures raise
CustomerIOSendErrorby 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
DEBUGwhenraise_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_unsubscribedisbool | None. Leave it unset to inherit whatever the transactional message is configured with in the Customer.io dashboard; passFalseto explicitly opt out.Falseis 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| altissimo_customerio-0.4.0.tar.gz | 20.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.