-
Notifications
You must be signed in to change notification settings - Fork 0
Initialization
The SDK entry point is Core. You pass an SDKConfig at construction time; the
config is loaded synchronously during __init__. Once core.is_ready is True,
you can create visitor contexts.
Public symbols referenced here are importable from convert_sdk —
Core, SDKConfig, TransportConfig, TrackingConfig, RefreshConfig.
Use sdk_key when you want the SDK to fetch the project config from the Convert
CDN at startup. The fetch is synchronous and blocking; network failures raise
ConfigLoadError.
import os
from convert_sdk import Core, SDKConfig, TransportConfig
core = Core(
SDKConfig(
sdk_key=os.environ["CONVERT_SDK_KEY"],
sdk_key_secret=os.getenv("CONVERT_SDK_KEY_SECRET"),
environment="production",
transport=TransportConfig(
config_endpoint="https://cdn-4.convertexperiments.com/api/v1",
timeout_seconds=5.0,
),
)
)
assert core.is_readyThe sdk_key_secret, if present, is sent as a Bearer token in the Authorization
header when fetching config and delivering tracking events.
Use config_data when you want to supply the project config yourself — from a
file, a cache, or an inline dict. No network call is made. This mode is ideal for
tests, CI, and local development.
from convert_sdk import Core, SDKConfig
project_config = {
"account_id": "1001",
"project": {"id": "2002", "name": "Demo"},
"features": [],
"experiences": [],
"goals": [],
}
core = Core(SDKConfig(config_data=project_config))
assert core.is_ready| Field | Type | Default | Purpose |
|---|---|---|---|
sdk_key |
str | None |
None |
Convert project SDK key |
sdk_key_secret |
str | None |
None |
Optional HMAC secret for authorization |
config_data |
Mapping | None |
None |
Inline config payload (overrides network fetch) |
environment |
str | None |
None |
Environment filter applied to experience eligibility |
transport |
TransportConfig |
see below | Network settings for config-fetch and tracking |
tracking |
TrackingConfig |
see below | Queue settings for conversion delivery |
refresh |
RefreshConfig | None |
None |
Opt-in background config refresh |
Exactly one of sdk_key or config_data must be provided; passing neither raises
ConfigValidationError.
| Field | Default | Purpose |
|---|---|---|
config_endpoint |
https://cdn-4.convertexperiments.com/api/v1 |
Base URL for config-fetch |
tracking_endpoint |
https://metrics.convertexperiments.com/v1 |
Base URL for tracking delivery |
headers |
{} |
Extra HTTP headers appended to every request |
timeout_seconds |
5.0 |
Per-request timeout |
verify_tls |
True |
Whether to verify TLS certificates |
| Field | Default | Purpose |
|---|---|---|
batch_size |
10 |
Maximum events per tracking POST |
source |
"python-sdk" |
source field in tracking payload |
enrich_data |
True |
Whether to set enrichData: true in tracking payload |
When environment is set, the SDK only activates experiences that list that
environment string in their environments array in the config. Pass the same
environment string you set in the Convert dashboard.
| Error | When raised |
|---|---|
ConfigValidationError |
Neither sdk_key nor config_data was provided, or the payload shape is invalid |
ConfigLoadError |
Network or HTTP error while fetching config with sdk_key
|
InitializationError |
Base class; also raised if visitor_id is empty in create_context()
|
All three are importable from convert_sdk. They share a common base
ConvertSDKError with .code and .context attributes for structured error
handling:
from convert_sdk import ConfigLoadError
try:
core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"]))
except ConfigLoadError as exc:
print(exc.code, exc.context)Core is designed to be a long-lived singleton. Create one instance at
application startup and reuse it. The TrackingQueue owned by Core is
protected by a threading.Lock, so concurrent track_conversion() and
release_queues() calls from different threads will not corrupt the queue.
Caveat: Core.create_context() performs an unlocked
load_context_state → save_context_state round-trip against the configured
DataStore. With the default InMemoryDataStore, two threads calling
create_context() for the same visitor_id at the same time can race and
overwrite each other's stored state. If you create contexts concurrently for
the same visitor (uncommon — most apps create one context per request), wrap
create_context() in your own lock or supply a DataStore that serialises
read-modify-write internally.
# application startup
core = Core(SDKConfig(config_data=project_config))
# per-request handler
def handle_request(visitor_id: str) -> None:
context = core.create_context(visitor_id)
result = context.run_experience("checkout-flow")
...Long-running services can opt into background config refresh by passing a
RefreshConfig to SDKConfig.refresh. Without a RefreshConfig, no
background activity runs and behaviour is identical to the MVP — the
default is refresh=None.
from convert_sdk import Core, SDKConfig
from convert_sdk.config import RefreshConfig
core = Core(
SDKConfig(
sdk_key=os.environ["CONVERT_SDK_KEY"],
sdk_key_secret=os.getenv("CONVERT_SDK_KEY_SECRET"),
environment="production",
refresh=RefreshConfig(
interval_seconds=300.0, # refresh every 5 minutes
jitter_seconds=30.0, # +/- 30s to avoid herding instances
backoff_initial_seconds=30.0, # first failure waits 30s before retry
backoff_factor=2.0, # exponential backoff
backoff_max_seconds=600.0, # cap retries at 10 minutes apart
),
)
)-
Refresh runs on a daemon thread inside
Core. The thread starts atCore(...)time and stops atcore.close()(or when the host process exits). -
Each successful refresh that produces a different snapshot replaces
core.snapshotthrough a single attribute swap. In-flight evaluations see either the old or new snapshot, never a partial state. -
Refresh attempts produce diagnostic events on the
convert_sdk.diagnosticslogger:Event Meaning refresh.startA refresh attempt is beginning. refresh.successFetch succeeded and the snapshot changed. refresh.skippedFetch succeeded but the snapshot is unchanged (or skipped for another stated reason). refresh.failTransient failure. Includes consecutive_failuresandat_terminal_backoff.refresh.terminal_failureTerminal condition (bad upstream payload or apply-callback bug). Worker stops. refresh.fork_detectedThe refresher detected an os.fork()in a child process; the worker is dead there.refresh.worker_crashedAn unexpected exception escaped the worker loop's own handler. Worker exits. -
Each successful refresh that produces a different snapshot also fires the
LifecycleEvent.CONFIG_UPDATEDlifecycle event withaccount_id,project_id, andentity_countsdetails. Subscribe throughcore.on(LifecycleEvent.CONFIG_UPDATED, handler)— this is the Python analog of the JavaScript SDK'sSystemEvents.CONFIG_UPDATED. TheTrackingQueue'saccount_idandproject_idare also refreshed so conversions queued after a refresh attribute to the new project.
-
Transient transport failures back off exponentially up to
backoff_max_secondsand retry; the worker keeps retrying because silently freezing on stale config is worse than periodic retries.on_terminal_failurefires once per failure once the consecutive- failure count reaches the backoff cap. -
Terminal failures stop the worker. Two conditions count as terminal:
(a) the upstream returned a structurally invalid payload
(
ConfigValidationError) — retrying the same broken response is futile; (b) the SDK's internal apply step raised, which indicates a programmer bug rather than a transient condition. In both caseson_terminal_failurefires once and the daemon thread exits;core.refresher_status.is_runningflips toFalseandterminal_failuretoTrue. Recovery requires recreatingCore. -
RefreshConfig.on_terminal_failureis optional. Use it to surface a typed alert through your application's logger or metrics pipeline. Exceptions raised inside the callback are caught and logged onconvert_sdk.refresh; they never crash the worker. - Background failures never raise into the host process. The worker thread is daemon-mode, so it does not block process exit.
RefreshConfig.__post_init__ rejects misconfigurations at construction
time so the worker is never started in a degenerate state:
interval_seconds > 00 ≤ jitter_seconds ≤ interval_secondsbackoff_initial_seconds > 0-
backoff_max_seconds > backoff_initial_seconds(strict — equality would fire the terminal callback on the very first failure) -
backoff_factor > 1.0(strict — factor=1.0 would mean the backoff cap is unreachable, so the terminal callback could never fire)
Invalid policies raise ConfigValidationError with a code of
refresh.invalid_interval, refresh.invalid_jitter, or
refresh.invalid_backoff.
Refreshes update core.snapshot for new contexts. Existing Context
objects retain whatever snapshot was current when they were created;
this is intentional — a Context represents a coherent view of the
project for the duration of a request or unit of work.
If you need a long-lived Context to pick up refreshed config, recreate
it through core.create_context(...) after the refresh has happened.
- One refresher per
Coreinstance. Spawning multipleCoreobjects spawns multiple refresher threads. - Auto-refresh under
os.fork()withoutexecis not supported. The forked child inherits a stopped daemon thread; recreateCorein the child process. This matches the broader expectation that SDK state stays process-local. -
Core.close()and the context-manager form (with Core(...) as core:) stop the refresher cleanly. Use one of these for graceful shutdown. Process exit alone is fine — daemon threads do not block exit.
Call core.refresh_now() to wake the worker and trigger an attempt
immediately, regardless of the configured interval:
core.refresh_now() # fire-and-forget
ok = core.refresh_now(wait=True) # block until the next attempt finishes
ok = core.refresh_now(wait=True, timeout=10.0) # custom timeout in secondswait=True returns True if the next refresh attempt completed
(success, snapshot-unchanged skip, transient failure, or terminal
failure) within timeout seconds, and False if it timed out. With
refresh disabled (SDKConfig.refresh=None) refresh_now() is a no-op
and returns True.
core.refresher_status returns a RefresherStatus snapshot for use in
operational metrics, health checks, or readiness probes:
status = core.refresher_status
status.enabled # False when SDKConfig.refresh is None
status.is_running # False after stop, fork, terminal failure, or crash
status.consecutive_failures # 0 on success/skip; resets on success
status.last_refresh_at # POSIX time of the last attempt (or None)
status.last_success_at # POSIX time of the last refresh that produced or matched a snapshot
status.last_error_type # exception class name of the last failure (or None)
status.last_error_at # POSIX time of the last failure (or None)
status.forked_in_child # True after the SDK detects an os.fork() in this process
status.terminal_failure # True after a terminal-failure shutdownIf SDKConfig.config_data is set (direct-config mode), there is no
remote endpoint to refresh from. Setting refresh=RefreshConfig(...)
in this mode logs a refresh.skipped diagnostic with
reason=direct_config_no_remote_endpoint and the worker is not
started. To pick up new config in direct-config mode, recreate Core.
-
Configuration — full
SDKConfig/TransportConfig/TrackingConfigreference -
Code Examples —
run_experience,run_feature,track_conversion,release_queues - Extending — swap the transport, data store, or event bus
Copyrights © 2025 All Rights Reserved by Convert Insights, Inc.
Getting Started
Python SDK
- Quickstart
- Installation
- Initialization
- Configuration
- Code Examples
- Type Hints
- Diagnostics
- Extending
- Testing
- Async & Frameworks
Migration
Core Concepts
- Experiences & Variations
- Feature Flags
- Bucketing Algorithm
- Rule Evaluation
- Segments
- Data Management
- Event System
- API Communication
How-To Guides
- Running Experiences
- Running Features
- Tracking Conversions
- Visitor Context
- Persistent DataStore
- Troubleshooting
Edge & Integrations
Maintainers