Skip to main content

Centive Python SDK

Production-quality Python SDK for integrating Centive into your backend services. This SDK provides both synchronous and asynchronous clients with built-in retry logic, comprehensive error handling, WebSocket support for real-time FE SDK communication, and automatic PII redaction for secure logging.

Features

  • Sync & Async Support - Use CentiveClient for synchronous code or AsyncCentiveClient for async/await
  • WebSocket Server - Real-time communication with frontend SDKs
  • Message Accumulation - Capture and save Anam AI conversation messages automatically
  • Automatic Retries - Exponential backoff with jitter for transient failures
  • Type Safety - Full Pydantic validation and type hints throughout
  • Error Handling - Comprehensive typed exceptions for all error scenarios
  • Zero API Changes - Integrate without modifying your existing API responses
  • Production Ready - Designed for high-reliability backend services

Installation

pip install centive-sdk

For development with all dev dependencies:

pip install centive-sdk[dev]

Quick Start

Basic Usage (Tool Mapping + Session Trigger)

from centive_sdk import AsyncCentiveClient, ToolMappingRequest, TriggerSessionRequest

async def main():
    async with AsyncCentiveClient(api_key="sk_live_your_api_key") as client:
        # At login: map user to tool
        mapping = await client.sessions.tool_mapping(
            ToolMappingRequest(
                user_id="user_123",
                user_name="John Doe",
                user_email="john.doe@acme.com",
                company_name="Acme Corp",
                role="admin"
            )
        )
        
        # Trigger session when needed
        session = await client.sessions.trigger_session(
            TriggerSessionRequest(
                user_id="user_123",
                user_trigger=True
            )
        )
        
        if session.session_data:
            print(f"Token: {session.session_data['token']}")

asyncio.run(main())

WebSocket Integration (Recommended)

initialize_websocket() returns a connection token bound to the user. Hand it to your frontend in the websocket URL; the Aria FE SDK connects with it and the socket can only act as that user.

Identity always comes from your authentication (session cookie, JWT, ...) — never from a query parameter or request body an anonymous caller controls.

from centive_sdk import AsyncCentiveClient, ToolMappingRequest

client = AsyncCentiveClient()  # api_key from CENTIVE_API_KEY env var

@app.post("/api/login")
async def login(user = Depends(get_current_user)):  # your existing auth
    await client.sessions.tool_mapping(
        ToolMappingRequest(user_id=user.id, user_name=user.name, user_email=user.email,
                           company_name=user.company, role=user.role)
    )
    token = await client.initialize_websocket(user_id=user.id)
    return {
        "status": "success",
        # FE passes this straight to the Aria FE SDK as websocketUrl
        "aria_websocket_url": f"wss://your-host/ws?token={token}" if token else None,
    }

The SDK automatically:

  • Checks if Aria is paused internally
  • If paused: no WebSocket server starts and no token is issued - the avatar stays hidden
  • If active: starts the WebSocket server and returns the user's connection token
  • When the FE connects with the token, the session is triggered and emitted to it

FastAPI Integration

Here's a complete example of integrating the SDK into a FastAPI application:

import os
from fastapi import Depends, FastAPI, HTTPException
from centive_sdk import (
    AsyncCentiveClient,
    ToolMappingRequest,
    AuthError,
    ValidationError,
    RateLimitError,
)

app = FastAPI()
centive = AsyncCentiveClient(
    api_key=os.getenv("CENTIVE_API_KEY"),
    ws_allowed_origins=["https://app.yourcompany.com"],
)
PUBLIC_WS_URL = "wss://app.yourcompany.com/ws"

@app.post("/api/login")
async def login(user = Depends(get_current_user)):
    try:
        await centive.sessions.tool_mapping(
            ToolMappingRequest(user_id=user.id, user_name=user.name, user_email=user.email,
                               company_name=user.company, role=user.role)
        )
    except AuthError:
        raise HTTPException(status_code=401, detail="Centive auth failed")
    except ValidationError as e:
        raise HTTPException(status_code=400, detail=str(e))

    token = await centive.initialize_websocket(user_id=user.id)
    return {
        "status": "success",
        "aria_websocket_url": f"{PUBLIC_WS_URL}?token={token}" if token else None,
    }

@app.on_event("shutdown")
async def shutdown():
    await centive.aclose()

See examples/fastapi_integration.py for the full pattern, including refreshing an expired connection token.

Configuration

Client Options

Parameter Type Default Description
api_key str $CENTIVE_API_KEY your API key (required here or via env var)
environment "prod" | "dev" $CENTIVE_ENVIRONMENT, else "prod" which Centive API to target
base_url str $CENTIVE_BASE_URL, else per environment explicit override of the API base URL
tool_mapping_path str /anam/tool-mapping endpoint path for tool mapping
trigger_session_path str /anam/trigger-session endpoint path for session trigger
save_messages_path str /anam/save-messages endpoint path for saving messages
pause_status_path str /anam/pause-status endpoint path for pause status check
timeout_seconds float 10.0 request timeout in seconds
max_retries int 3 maximum number of retry attempts
initial_retry_delay float 0.5 initial delay for exponential backoff
max_retry_delay float 8.0 maximum delay between retries
ws_host str 0.0.0.0 websocket bind address (127.0.0.1 behind a proxy)
ws_port int 8765 websocket server port
ws_auth_mode "token" | "open" "token" connection identity binding (see Security)
ws_token_ttl_seconds float 3600.0 how long a connection token stays valid
ws_allowed_origins list[str] None allow-list of Origin headers for WS handshakes
ws_handshake_secret str None static shared secret (legacy "open" mode only)
ws_ping_interval float 20.0 websocket ping interval (seconds)
auto_save_interval_seconds float 300.0 periodic auto-save interval
max_sessions_per_user int 10 concurrent sessions kept per user
max_stream_messages_per_session int 10000 stream chunks kept per session
max_session_bytes int 2000000 message bytes retained per session
max_message_content_chars int 32768 longest single message accepted
max_session_id_chars int 200 longest session_id accepted
max_connections_per_user int 5 concurrent websocket connections per user
pause_check_fail_open bool True proceed as active if the pause check fails; False withholds access
circuit_breaker_threshold int 5 failures before circuit opens
circuit_breaker_recovery_seconds float 60.0 time before circuit recovery attempt
logger Optional[Any] None custom logger instance for debugging

Example with Custom Configuration

import logging

logger = logging.getLogger("centive")
logger.setLevel(logging.INFO)

client = AsyncCentiveClient(
    api_key="sk_live_your_api_key",
    base_url="https://custom-centive-api.example.com",
    tool_mapping_path="/api/v1/tool-mapping",
    timeout_seconds=15.0,
    max_retries=5,
    ws_port=8080,
    logger=logger
)

Error Handling

The SDK provides typed exceptions for different error scenarios:

from centive_sdk import (
    CentiveError,      # Base exception
    AuthError,         # 401/403 - Invalid API key
    ValidationError,   # 400/422 - Invalid request data
    RateLimitError,    # 429 - Rate limit exceeded
    ServerError,       # 5xx - Server-side error
    NetworkError,      # Connection/timeout errors
)

try:
    mapping = await client.sessions.tool_mapping(request)
except AuthError as e:
    print(f"Authentication failed: {e}")
    print(f"Request ID: {e.request_id}")
except ValidationError as e:
    print(f"Invalid data: {e}")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after} seconds")
except (ServerError, NetworkError) as e:
    print(f"Service unavailable: {e}")
except CentiveError as e:
    print(f"Unexpected error: {e}")

Security

WebSocket identity binding (default: ws_auth_mode="token")

Every call to initialize_websocket(user_id) issues a cryptographically random connection token bound to that user. The FE must present it as ?token= on the websocket URL:

  • A socket with a valid token is bound to that user for its lifetime. Frames claiming a different user_id are rejected (IDENTITY_MISMATCH), and frames targeting another user's session are rejected (SESSION_OWNERSHIP).
  • Connections without a valid token are closed during the handshake (code 4401).
  • Tokens expire after ws_token_ttl_seconds (default 1 hour) and stay valid within that window so the FE SDK's automatic reconnect keeps working. Fetch a fresh URL from your backend when a token expires (see examples/fastapi_integration.py).

What the SDK guarantees between users

Each of these is verified by the blackbox suite (blackbox/), which attacks the built wheel from a separate process:

  • Sessions are namespaced per user. Session ids arrive from the client, so reusing another user's session id creates a separate session owned by the caller. One user cannot read, corrupt, or destroy another's transcript, and cannot squat a session id to lock someone out of recording their conversation.
  • Identity is server-side. user_id in a frame is only accepted if it matches the connection's bound user; otherwise the frame is rejected.
  • Upstream error detail never reaches a browser. Failures from the Centive API return a generic message on the websocket; full detail goes to your server log only. This includes the API's HTTP-200-with-status="error" responses, whose text can contain SQL, internal hostnames and organization ids.
  • A failed save never destroys a transcript. If the API reports a rollback, messages stay buffered for retry instead of being cleared as if stored.
  • Credentials stay out of logs. The API key, Anam session tokens, connection tokens (including in transport debug logging) and message content are never written to your logger.
  • One user cannot exhaust the process, or affect anyone else's persistence. Sessions, retained bytes, message size and concurrent connections are bounded per user, and malformed payloads are rejected on arrival so they cannot trip the shared circuit breaker.
  • Pause and lifecycle are per-user. Pausing or revoking one user closes only their connections; everyone else keeps their session.
  • Malformed input cannot wedge the server. Invalid JSON, oversized frames, deeply-nested payloads and wrong-typed fields are handled without affecting other users' connections.

Revoking access mid-session

Connection tokens stay valid for their TTL, and a valid token can obtain a fresh avatar session. When a user's access ends (logout, deactivation, ban), cut it off explicitly — pause is handled automatically by initialize_websocket():

server = client.websocket_server
if server:
    await server.disconnect_user(user_id)   # revokes tokens + closes their sockets

Sending data to users yourself

Use send_to_user(), not broadcast(), for anything user-specific:

server = client.websocket_server
await server.send_to_user(user_id, {"type": "custom", "payload": ...})  # this user only
await server.broadcast({"type": "maintenance"})  # EVERY connected user

broadcast() reaches every end-user connected to the process; passing user-specific data to it is a cross-user disclosure.

Recommended hardening

  • Serve the websocket over TLS (wss://) behind your reverse proxy, and set ws_host="127.0.0.1" so only the proxy can reach the SDK directly.
  • Set ws_allowed_origins to your frontend origin(s) so browsers on other sites cannot even complete the handshake.
  • Treat connection tokens like short-lived credentials: don't log them and keep them out of analytics that record URLs.

Legacy mode (ws_auth_mode="open") — deprecated

Pre-2.0 behavior: the first socket to connect is assigned the next registered user and client-supplied user_id is trusted. This is insecure — any client that can reach the port can race for another user's session token — and exists only as a temporary migration escape hatch. The server emits a warning at startup when it is enabled. ws_handshake_secret (a static shared ?token= secret) is honored only in this mode.

Logging

The SDK automatically redacts sensitive information in logs:

  • API keys are completely redacted as [REDACTED]
  • User names are masked (e.g., "Harrison""Ha***")
  • Company names are masked (e.g., "Acme Corp""Ac***")
import logging

logger = logging.getLogger("my_app")
logger.setLevel(logging.INFO)

client = AsyncCentiveClient(
    api_key="sk_live_secret",
    logger=logger
)

# Logs will show masked values:
# INFO: Mapping user to tool {"user_id": "user_123", "user_name": "Jo***", "role": "admin"}

Pause Status Handling

The SDK automatically handles Aria pause status internally:

@app.post("/api/login")
async def login(user = Depends(get_current_user)):
    # SDK internally checks if Aria is paused
    # If paused: no WebSocket starts, no token issued, avatar stays hidden
    # If active: WebSocket starts and the user's connection token is returned
    token = await client.initialize_websocket(user_id=user.id)

    return {"aria_websocket_url": f"wss://your-host/ws?token={token}" if token else None}

Optional: Check Pause Status for UI Display

If you want to show pause status in your UI (e.g., "Aria is unavailable"), use the helper method:

@app.get("/api/aria/status")
async def aria_status(user = Depends(get_current_user)):
    pause_status = await client.get_aria_status(user.id)
    return {
        "is_paused": pause_status.is_paused,
        "pause_source": pause_status.pause_source,  # 'org_paused', 'account_paused', or 'not_paused'
        "message": pause_status.message,
        "paused_until": pause_status.paused_until,
    }

Optional: Check Last Pause Status

After calling initialize_websocket(), you can check the cached pause status:

await client.initialize_websocket(user_id=user_id)

# Check what happened (without another API call)
if client.last_pause_status and client.last_pause_status.is_paused:
    print(f"Aria is paused: {client.last_pause_status.message}")

WebSocket Flow

  1. Backend: Call initialize_websocket(user_id) from an authenticated endpoint
  2. SDK: Checks if Aria is paused for this user
  3. If paused: No WebSocket server starts, no token is issued, avatar stays hidden
  4. If active: WebSocket server starts and a per-user connection token is returned
  5. Backend: Returns wss://your-host/ws?token=<token> to the frontend
  6. FE SDK: Connects with that URL; the socket is bound to that user, the session is triggered, and the avatar renders

Message Accumulation (Anam AI Integration)

The SDK includes built-in support for accumulating conversation messages from Anam AI video avatar sessions. Messages are collected via WebSocket and automatically saved to the Centive API.

How It Works

  1. frontend SDK sends message events to the BE SDK via WebSocket during the Anam AI session
  2. BE SDK accumulates messages in memory with automatic deduplication
  3. messages are saved via periodic auto-save AND on session end/disconnect

Backend Usage

The message accumulation happens automatically once the WebSocket server is initialized:

from centive_sdk import AsyncCentiveClient

client = AsyncCentiveClient(api_key="sk_live_your_api_key")

@app.post("/api/login")
async def login(user = Depends(get_current_user)):
    token = await client.initialize_websocket(user_id=user.id)
    return {"aria_websocket_url": f"wss://your-host/ws?token={token}" if token else None}

The SDK automatically:

  • accumulates messages from incoming WebSocket events
  • deduplicates messages using history as source of truth
  • periodic auto-save every 5 minutes (configurable)
  • saves on session end or early disconnect
  • circuit breaker prevents cascading failures if API is down
  • graceful shutdown saves all pending sessions when server stops

Features

  • no limits: unlimited sessions, lifetime, and messages per session
  • incremental save mode: send messages to API immediately as they arrive (real-time)
  • batch save mode (default): accumulate and send all messages at session end
  • periodic auto-save: saves long-running sessions automatically (default: every 5 min)
  • circuit breaker: opens after 5 failures, recovers after 60s (configurable)
  • automatic deduplication: backend handles duplicate messages automatically
  • early disconnect handling: saves partial messages if connection drops
  • graceful shutdown: saves all sessions when server stops
  • retry logic: failed saves are retried with exponential backoff

Save Modes

The SDK supports two save modes for message persistence:

Batch Mode (Default)

Messages are accumulated in memory and sent all together when:

  • Session ends (via session_end event)
  • Periodic auto-save triggers (every 5 minutes)
  • Early disconnect occurs
  • Server shuts down
client = AsyncCentiveClient(
    api_key="sk_live_your_api_key",
    incremental_save_enabled=False  # default
)

Incremental Mode (Real-time)

Messages are sent to the API immediately as they arrive:

  • After each message_history update
  • After each finalized stream message (is_final=true)
  • Backend handles deduplication automatically
client = AsyncCentiveClient(
    api_key="sk_live_your_api_key",
    incremental_save_enabled=True  # enable real-time saving
)

When to use incremental mode:

  • Critical data that must be saved immediately
  • Real-time analytics or monitoring requirements
  • Lower risk tolerance for data loss
  • Shorter, more frequent API calls preferred

When to use batch mode:

  • Standard use cases where end-of-session save is sufficient
  • Minimize API calls and bandwidth
  • Periodic saves (every 5 min) are acceptable

For details on WebSocket event formats, see websocket_messages.md.

Advanced Usage

Concurrent Tool Mapping (Async)

import asyncio

async def map_multiple_users(users):
    async with AsyncCentiveClient(api_key="sk_live_your_api_key") as client:
        tasks = [
            client.sessions.tool_mapping(ToolMappingRequest(**user))
            for user in users
        ]
        return await asyncio.gather(*tasks)

users = [
    {"user_id": "1", "user_name": "Alice", "user_email": "alice@acme.com", "company_name": "Acme Corp", "role": "admin"},
    {"user_id": "2", "user_name": "Bob", "user_email": "bob@techco.com", "company_name": "TechCo", "role": "viewer"},
]

mappings = asyncio.run(map_multiple_users(users))

WebSocket Server Status

client = AsyncCentiveClient(api_key="sk_live_your_api_key")
await client.initialize_websocket(user_id="user_123")

# Check server status
if client.websocket_server:
    print(f"Server running: {client.websocket_server.is_running}")
    print(f"Connected clients: {client.websocket_server.connected_clients}")

Development

Running Tests

# install dev dependencies
pip install -e ".[dev]"

# run all tests
pytest

# run with coverage
pytest --cov=centive_sdk --cov-report=html

# run specific test file
pytest tests/test_client_sync.py

# run async tests only
pytest tests/test_client_async.py

# run WebSocket tests
pytest tests/test_websocket_server.py

# run the blackbox security suite against the built wheel
python -m build
python -m venv /tmp/customer-venv
/tmp/customer-venv/bin/pip install dist/centive_sdk-*.whl
BLACKBOX_CUSTOMER_PYTHON=/tmp/customer-venv/bin/python pytest blackbox/ -v

See blackbox/README.md for what that suite covers and why it is structured the way it is.

Code Quality

# format code
ruff format .

# lint code
ruff check .

# fix auto-fixable issues
ruff check --fix .

Examples

See the examples/ directory for complete working examples:

  • sync_example.py - Basic synchronous usage
  • async_example.py - Asynchronous usage with WebSocket
  • fastapi_integration.py - Full FastAPI integration
  • websocket_example.py - WebSocket flow demonstration

Run examples:

python examples/sync_example.py
python examples/async_example.py
python examples/fastapi_integration.py
python examples/websocket_example.py

Environments (dev / prod)

There is one packagecentive-sdk on PyPI. Which Centive environment it talks to is selected by configuration, never by installing a different package:

Environment Base URL
prod (default) https://centive-prod-api.theagentic.ai/api
dev https://centive-api.theagentic.ai/api
# production deployment
export CENTIVE_API_KEY="sk_live_..."
# CENTIVE_ENVIRONMENT unset -> prod

# dev deployment
export CENTIVE_API_KEY="sk_test_..."
export CENTIVE_ENVIRONMENT="dev"
client = AsyncCentiveClient()                    # env-var driven (recommended)
client = AsyncCentiveClient(environment="dev")   # or explicit
client = AsyncCentiveClient(base_url="https://...")  # full override, wins over both

Releasing SDK changes: dev branch → pre-release, prod branch → stable

The SDK follows the same branch model as the Centive FE/BE repos:

Branch CI Publishes
dev publish-dev.yaml X.Y.Z.dev<run> to PyPI (unique per push)
prod publish-prod.yaml X.Y.Z to PyPI (only if that version isn't already published)

Both channels use the real index. pip excludes pre-releases unless they are exact-pinned, so pip install centive-sdk and range pins never resolve to a dev build — while dev consumers still get normal dependency resolution. TestPyPI is deliberately not used: installing from it safely requires --index-url plus --no-deps, which means the dev layer would not be running the SDK's real dependency set.

Flow for a change:

  1. PR into devci.yaml runs lint + tests (Python 3.10–3.13).

  2. Merge to dev — tests and the blackbox suite run again, then a uniquely-versioned dev build (e.g. 2.1.0.dev42) is published automatically.

  3. Test on dev — pin that exact version in the dev consumer with CENTIVE_ENVIRONMENT=dev, and run your integration suite against the dev API:

    pip install centive-sdk==2.1.0.dev42
    
  4. Promote — set version in pyproject.toml to the release version, update CHANGELOG.md, and merge devprod. The prod workflow publishes, tags vX.Y.Z, and smoke-tests the published package. Pushes to prod without a version bump publish nothing (idempotent), so doc-only changes are safe.

  5. Open the next cycle — bump version on dev to the next unreleased version, because X.Y.Z.devN sorts below X.Y.Z. The dev workflow fails with a clear message if you forget.

Production installs are exact-pinned (centive-sdk==2.0.0) from pypi.org with no extra index configured — never add --extra-index-url, which would expose every dependency in the build to substitution (security-audit finding A3). Keep dev and prod API keys separate; a dev deployment should hold no prod credentials.

Maintainers: RELEASING.md has the one-time trusted-publishing setup plus the full release procedure.

Migration to v2.0

v2.0 makes WebSocket identity binding secure by default:

  1. initialize_websocket(user_id) now returns a connection token (a str, or None when Aria is paused).
  2. Your API must hand the frontend its websocket URL including the token: wss://your-host/ws?token=<token>, and the FE app passes that URL to the Aria FE SDK as websocketUrl. No FE SDK upgrade is required.
  3. Frames on a connection are bound to its user: forged user_id values and cross-user session access are rejected.
  4. Derive user_id from your authentication, never from an unauthenticated query parameter.

If you cannot plumb the token through yet, ws_auth_mode="open" temporarily restores the old behavior (deprecated, insecure, warns at startup); plan to remove it before exposing the websocket to untrusted networks.

Migration from v1.0.0

If you're upgrading from v1.0.0, note these breaking changes:

  • create_session() → Use tool_mapping() + trigger_session() instead
  • CreateSessionRequest → Use ToolMappingRequest and TriggerSessionRequest
  • SessionCreated → Use ToolMappingResponse and TriggerSessionResponse

See CHANGELOG.md for detailed migration guide.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

centive_sdk-2.0.0.tar.gz (72.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

centive_sdk-2.0.0-py3-none-any.whl (44.9 kB view details)

Uploaded Python 3

File details

Details for the file centive_sdk-2.0.0.tar.gz.

File metadata

  • Download URL: centive_sdk-2.0.0.tar.gz
  • Upload date:
  • Size: 72.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for centive_sdk-2.0.0.tar.gz
Algorithm Hash digest
SHA256 53571fd897cf38c6f668e8bf79c89cda2a5b9650bc033f046da8be14149cd4e2
MD5 4c88d3ef7a6c2f7981eff84977f6cb31
BLAKE2b-256 bc4dcbbb298ab21c9c51581175641c6bfa18d5c711c263757a2e5e5bad4a149b

See more details on using hashes here.

Provenance

The following attestation bundles were made for centive_sdk-2.0.0.tar.gz:

Publisher: publish-prod.yaml on TheAgenticAI/centive-backend-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file centive_sdk-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: centive_sdk-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 44.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for centive_sdk-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 21d3a367395fe3dc6a3cd147492948fd51edc5add68dfed03e76c5f3645b6f8d
MD5 f786d982c668f0d6e6ed5957590e5925
BLAKE2b-256 65bd4914d42ac9f2c615928a983d00287c39c4a79802b559a019a15ce1634dea

See more details on using hashes here.

Provenance

The following attestation bundles were made for centive_sdk-2.0.0-py3-none-any.whl:

Publisher: publish-prod.yaml on TheAgenticAI/centive-backend-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

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