CDP Python SDK
A Python client library for Codematic's Customer Data Platform (CDP) with optional Customer.io integration.
Installation
pip install cdp-python-sdk
Or via requirements.txt:
cdp-python-sdk
Features
- Async Support: Built on
httpxandasynciofor high-performance non-blocking I/O. - Dual-Write: Optional integration to send data to both CDP and Customer.io simultaneously.
- Type Safety: Uses Pydantic models for request validation.
- Transactional Messaging: Support for Email, Push, and SMS.
- Device Registration: Register devices for push notification targeting.
Examples
A complete runnable example is available in example/:
example/
├── main.py # Full flow: init → identify → track → register_device → email → push → sms
└── README.md # How to run
See example/README.md for run instructions.
Quick Start
import asyncio
from cdp_client import CDPClient, CDPConfig
async def main():
config = CDPConfig(cdp_api_key="your-cdp-api-key")
client = CDPClient(config)
await client.identify("user-123", {"email": "user@example.com", "name": "Jane"})
await client.track("user-123", "signed_up", {"plan": "pro"})
await client.close()
asyncio.run(main())
Usage
Initialization
import asyncio
from cdp_client import CDPClient, CDPConfig, CustomerIoConfig
async def main():
config = CDPConfig(
cdp_api_key="your-cdp-api-key",
debug=True,
# Optional: enable Customer.io dual-write
send_to_customer_io=True,
customer_io=CustomerIoConfig(
site_id="your-cio-site-id",
api_key="your-cio-api-key",
region="us" # "us" or "eu"
)
)
client = CDPClient(config)
# ... use client ...
await client.close()
asyncio.run(main())
Identify a User
Associate a user ID with a set of traits (name, email, plan, etc.). Call this on sign-up, login, or whenever user attributes change.
await client.identify("user-123", {
"email": "user@example.com",
"name": "Jane Doe",
"plan": "premium"
})
Track an Event
Record a user action or behaviour.
await client.track("user-123", "purchase_completed", {
"amount": 99.99,
"currency": "USD",
"item_id": "prod-456"
})
Register a Device
Register a device token for push notification targeting. Call this after you receive a push token from your mobile platform.
await client.register_device(
user_id="user-123",
device_id="device-abc", # Unique device identifier
platform="ios", # "ios", "android", or "web"
token="apns-or-fcm-token",
attributes={ # Optional device metadata
"app_version": "2.1.0",
"os_version": "17.2"
}
)
Send Transactional Email
from cdp_client import EmailPayload, Identifiers
await client.send_email(EmailPayload(
to="user@example.com",
identifiers=Identifiers(id="user-123"),
transactional_message_id="WELCOME_EMAIL",
subject="Welcome!",
body="<h1>Thanks for joining!</h1>",
body_plain="Thanks for joining!"
))
Attachments
Attach up to 5 files (2 MB decoded in total). Bytes are base64-encoded for you:
payload = EmailPayload(
to="user@example.com",
identifiers=Identifiers(id="user-123"),
transactional_message_id="INVOICE_EMAIL",
)
payload.attach_file("./invoice.pdf") # named "invoice.pdf"
payload.attach("notes.txt", "Plain text content") # strings are base64-encoded by default
payload.attach("report.csv", existing_base64, encode=False) # already base64, sent as-is
await client.send_email(payload)
You can also pass attachments={"invoice.pdf": "<base64>"} directly. send_email validates attachments before sending: at most 5 files, at most 2 MB decoded in total, filenames without /, \ or .., and non-empty base64 content. Invalid attachments raise CDPValidationError when fail_on_exception=True; otherwise the error is logged and nothing is sent. The content type is inferred from the file extension.
Send Push Notification
from cdp_client import PushPayload, Identifiers
await client.send_push(PushPayload(
identifiers=Identifiers(id="user-123"),
transactional_message_id="PROMO_PUSH",
title="Flash Sale 🔥",
body="50% off for the next hour."
))
Send SMS
from cdp_client import SmsPayload, Identifiers
await client.send_sms(SmsPayload(
identifiers=Identifiers(id="user-123"),
to="+14155551234", # Optional: raw phone number
transactional_message_id="OTP_MSG",
body="Your one-time code is 881234."
))
Send WhatsApp
WhatsApp sends use a saved WhatsApp transactional, so transactional_message_id is required.
from cdp_client import WhatsAppPayload, Identifiers
result = await client.send_whatsapp(WhatsAppPayload(
identifiers=Identifiers(id="user-123"),
transactional_message_id="ORDER_WHATSAPP",
to="+14155551234", # Optional: overrides the profile phone number
message_data={"order_number": "12345"}, # Available in the template as {{trigger.order_number}}
))
- A successful return means the message was queued, not delivered. Delivery runs asynchronously, so a missing WhatsApp provider, no phone number, or a template rejected by Meta does not fail this call.
resultholds the transactional execution record; keep its id to trace the send. template_variables({"header": {...}, "body": {...}, "button": {...}}) sets the template slots from code. Keys must be slot numbers ("1","2", ...), and values may use Liquid such as{{customer.first_name}}. Passing it replaces all variables saved on the transactional, so include every section the template needs.- Sends are not retried on another gateway host after a read, write, or pool timeout or an HTTP error, because the message may already have been queued. The exceptions are connection failures (including connection timeouts) and the Cloudflare errors 521, 523, 525 and 526, which mean the gateway never received the request.
Clear Identity / Logout
To reset the client's user context (e.g., on logout), close the current client and re-initialize without a user session:
# On user logout: flush pending work and release the HTTP client
await client.close()
# Re-initialize for anonymous or new user session
client = CDPClient(config)
Error Handling
By default (fail_on_exception=False) the SDK logs errors and does not raise, so a failed call never crashes your app. Set fail_on_exception=True to raise instead:
from cdp_client import CDPError, CDPValidationError
config = CDPConfig(cdp_api_key="...", fail_on_exception=True)
client = CDPClient(config)
try:
await client.identify("user-123", {"email": "user@example.com"})
except CDPValidationError as e:
# Invalid input, caught before any request is sent
print(f"Invalid {e.field}: {e}")
except CDPError as e:
# status_code is the HTTP status, or None when the gateway could not be reached
print(f"API error {e.status_code}: {e}")
This applies to all methods (
identify,track,send_email,send_push,send_sms,send_whatsapp,register_device). Dual-write Customer.io errors are non-fatal and only emit a warning log.
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
cdp_api_key |
str |
Required | Your CDP API Key |
cdp_endpoint |
str |
Production URL | Custom CDP Gateway URL |
debug |
bool |
False |
Enable verbose debug logging |
send_to_customer_io |
bool |
False |
Enable dual-write to Customer.io |
customer_io |
CustomerIoConfig |
None |
Customer.io integration config (see below) |
CustomerIoConfig Options
| Option | Type | Description |
|---|---|---|
site_id |
str |
Customer.io Site ID |
api_key |
str |
Customer.io API Key |
region |
str |
"us" (default) or "eu" |
Dual-Write to Customer.io
When send_to_customer_io=True, all identify, track, and register_device calls are mirrored to Customer.io automatically. Customer.io failures are non-blocking — the CDP call succeeds even if the Customer.io call fails.
config = CDPConfig(
cdp_api_key="your-cdp-api-key",
send_to_customer_io=True,
customer_io=CustomerIoConfig(
site_id="cio-site-id",
api_key="cio-api-key",
region="eu"
)
)
Development
Setup
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip setuptools wheel build
python -m pip install -e ".[test]"
Run Tests
pytest -v
Building the Package
python -m build
This creates files in the dist/ directory:
cdp_python_sdk-{version}-py3-none-any.whlcdp_python_sdk-{version}.tar.gz
Versioning
Follow Semantic Versioning:
| Bump | When |
|---|---|
PATCH 1.0.0 → 1.0.1 |
Bug fixes |
MINOR 1.0.0 → 1.1.0 |
New features, backward compatible |
MAJOR 1.0.0 → 2.0.0 |
Breaking API changes |
Metadata
Release files for cdp-python-sdk 0.2.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 | |
|---|---|---|---|---|
| cdp_python_sdk-0.2.0.tar.gz | 15.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cdp_python_sdk-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.5 kB
Release files / cdp_python_sdk-0.2.0.tar.gz
| Download URL | cdp_python_sdk-0.2.0.tar.gz |
|---|---|
| Size | 15.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e96293bdefeb90fc0776f9b03ad3b97f7d39459bb6d18cdde4ca771ce0ba2d37
|
|
BLAKE2b-256 checksum How to use checksums |
6cff269cb123b774ce341fda5d63eeb4268fd3dafbbac7db07821953c10dd750
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 7, 2026.
Transparency logRelease files / cdp_python_sdk-0.2.0-py3-none-any.whl
| Download URL | cdp_python_sdk-0.2.0-py3-none-any.whl |
|---|---|
| Size | 13.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
17ac9e174389bf3a7f6ed977d811d0de4eb4c59dc3c6bdcac0c94900d4015055
|
|
BLAKE2b-256 checksum How to use checksums |
ec578e6e1c461cd42510e9a9551bdc69e49afd3b08be1bfad1855c35b6e37322
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 7, 2026.
Transparency logSecurity for cdp-python-sdk 0.2.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 cdp_python_sdk-0.2.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 7, 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.