Skip to main content

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 httpx and asyncio for 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. result holds 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.whl
  • cdp_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)

Source distribution for cdp-python-sdk 0.2.0
File Size Uploaded Provenance
cdp_python_sdk-0.2.0.tar.gz 15.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cdp-python-sdk 0.2.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 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.

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