What this lets you do
Send pageviews and revenue conversions to Traceten from your Python backend, over server-to-server HTTP that survives ad-blockers and privacy browsers. Use it where the browser snippet cannot run: server-rendered flows, background jobs, webhook handlers, and mobile or API backends.Before you start
- Python 3.9 or newer.
- Your Traceten
site_id, from Sites → Install in the dashboard. - Your ingest
hostURL, for examplehttps://ingest.traceten.com. - A
visitor_idfor each event. The SDK runs on your server, so there is no Traceten cookie to read. You supply the identifier from your own request context. It is either a UUID or anh:<64-hex>identify hash. See supplying identifiers.
Install
Minimal example
How it behaves
page() and track() buffer the event and return immediately, never blocking on the network. The SDK sends events in the background: when the events queue reaches flush_at, every flush_interval seconds, on an explicit flush(), and on close() or interpreter exit.
Neither method raises because of a network problem. A failed delivery surfaces through your on_error hook. Both raise TracetenError synchronously on a programmer error, so you catch a mis-shaped call in development instead of a silent server-side rejection:
track()called without avisitor_id- an
event_namethat fails its pattern - a
urlthat is not a valid URL - a
value_centsthat is not a non-negative integer
Configuration
host must be an absolute http(s) URL. A trailing slash is stripped. There is no default host: each deployment has its own ingest URL.
host must use https unless it is a loopback address. The API key is sent as a Bearer token on every request, so a plaintext http host would transmit a live credential in cleartext; the SDK refuses one at construction.
api_key is required. See The API key below.
The API key
api_key is required. Create a key in the dashboard under Settings, API keys, or use the key shown once when you created the site.
The key must carry the Send events permission (ingest:write). New keys are created read-only by default, so tick that box when you create one, or the API will reject your calls. The key created automatically with a new site already has it. See permissions.
Keep it on your server. It is a secret: never put it in client-side code, a mobile app, or a public repository. It is not the same value as site_id, which is public and already embedded in your pages.
The key does two things:
- It gets you in. The SDK posts to
{host}/v1/server/events,{host}/v1/server/conversionsand{host}/v1/server/payments, which return401without a valid key. The open/v1/eventsand/v1/conversionsendpoints stay open for the browser snippet, which cannot hold a secret. - It gets you your own quota. Authenticated traffic is rate-limited on a bucket tied to the key, separate from the shared per-site bucket.
site_idis public, so anyone who can read your page source can send events under it. With a key, that traffic cannot exhaust your allowance and 429 your conversion calls.
TracetenError if it is malformed. Omitting it entirely is a TypeError: api_key is a required keyword argument, so Python refuses the call. An integration that silently drops every event is worse than one that fails on its first line.
To rotate a key: create the new one, deploy it, then revoke the old one. Revocation normally takes effect at the edge within about a minute. If our
database is unreachable at that moment, an edge location that was already using
the key may keep honouring it for up to about fifteen minutes more, so that a
database blip cannot silently drop your events.
API reference
Client(site_id, host, *, api_key, flush_at=50, flush_interval=5.0, max_retries=3, timeout=10.0, on_error=None)
page(*, url, referrer="", visitor_id=None, session_id=None, user_agent=None, event_name="pageview", timestamp=None)
Enqueues a pageview to POST {host}/v1/server/events.
user_agent you pass is what Traceten
stores on the event and classifies on. Omit it and the event falls back to the
User-Agent header the SDK’s own HTTP client sent, which describes your server
rather than the visitor. That substitution applies only to requests
authenticated with an API key. The browser snippet cannot override its own
User-Agent this way.
track(name, *, visitor_id, properties=None, value_cents=None, currency=None, session_id=None, timestamp=None)
Enqueues a conversion to POST {host}/v1/server/conversions. name must match ^[a-z][a-z0-9_]*$ (no hyphen). visitor_id is required: a conversion cannot be attributed without it, and omitting it raises TracetenError with nothing enqueued.
value_cents is in minor units. For $49.00, pass 4900. The field is spelled out as value_cents (not value) to prevent the dollars-versus-cents mistake.
goal(name, *, visitor_id, properties=None, value_cents=None, currency=None, session_id=None, timestamp=None)
track(), with one difference: it raises TracetenError on the reserved names, which belong to the Stripe and Shopify integrations.
Reserved: payment, free_trial, trial_started, trial_converted, subscription_started, subscription_upgraded, subscription_downgraded, subscription_renewed, subscription_cancel_scheduled, subscription_reactivated, subscription_ended. track() still accepts them, because that is how those events are legitimately sent.
Goal property keys and values are both stored, and both are read back: GET /v1/goals/{name}/properties returns each key and its most common values, and the dashboard renders the same breakdown. Keep them to ids and enums. See what happens to property keys and values.
payment(*, transaction_id, amount, currency, provider=None, visitor_id=None, email=None, customer_id=None, renewal=None, refunded=None, is_free_trial=None, timestamp=None)
Records a payment from any payment processor via POST {host}/v1/server/payments. See the Payment API reference for the full contract.
Unlike page() and track(), this one blocks until the server answers and returns what it did with the payment.
amount is the major unit, the opposite of track()’s value_cents. For $49.99 pass 49.99, and for ¥5000 pass 5000. It is the number you read off your processor.
Partial refunds do not go through this method.
refunded=True takes back everything still
outstanding on the transaction, and this SDK has no field for a smaller figure. To refund part of
a payment, POST to /v1/server/payments with refunded_amount, which is
the total refunded against the transaction so far.transaction_id returns "duplicate" and creates nothing. That is why the SDK retries a 5xx here.
It raises TracetenError for an invalid field and DeliveryError when the payment could not be delivered after every retry. It is the only method in this SDK that raises on a delivery failure, because a dropped payment is missing revenue rather than a missing pageview.
flush() -> None
close() -> None
atexit, so a graceful interpreter exit flushes automatically. Call it explicitly before a hard shutdown to be safe.
Errors
The SDK is otherwise silent (no
print, no logging side effects). Wire up on_error for observability.
Batching and retries
- Pageviews are sent to
/v1/server/eventsin batch envelopes of up to 50 events. If you enqueue 120 pageviews, the SDK sends them as batches of 50, 50, and 20. - Conversions are sent to
/v1/server/conversionsone body per event. - On a 5xx, a 429, or a network or timeout error, the batch is retried with exponential backoff and full jitter, up to
max_retriestimes. - On any other 4xx the payload is malformed. The SDK drops it and calls
on_error. It does not retry, because retrying a rejected payload forever is a self-inflicted denial of service.
Verify it worked
- Send one
page()call from your backend. - Open Sites → Events in the Traceten dashboard for that site.
- The event appears in the live event feed within a few seconds.
on_error to see why.
Verify webhook signatures
If you subscribe to Traceten’s outbound webhooks (AI-session and conversion events), verify every delivery before you act on it.verify_webhook() recomputes the X-Traceten-Signature HMAC in constant time (hmac.compare_digest), enforces the replay window, and returns the parsed event, or raises SignatureVerificationError.
Verify the raw body, not a re-serialized one
The single most common verification bug: a framework parses the JSON, your code re-serializes it, and the bytes no longer match what Traceten signed (key order, whitespace, and unicode escaping all differ). Always pass the raw bytes as received, and only act on the event after verification succeeds.- Flask: use
request.get_data(), notrequest.get_json(). - FastAPI / Starlette: use
await request.body(), not a Pydantic body parameter.
tolerance_seconds (default 300) sets the replay window. Pass float("inf") only when testing against the fixed conformance vector.
Troubleshooting
Events never appear. Wire upon_error. The most common causes are a wrong host, a site_id typo, or a firewall blocking outbound HTTPS.
TracetenError on track(). You omitted visitor_id, or the event name has a hyphen. Conversion names allow only a-z, 0-9, and _.
TracetenError on page(). The url is not a valid absolute URL, or the event_name does not match ^[a-z][a-z0-9_-]*$.
A short-lived script exits before sending. Call client.close() before the process ends. The atexit hook covers a graceful interpreter exit, but not a hard kill.
Next
- Node.js SDK
- Go SDK
- Browser API reference for client-side tracking
- Stripe revenue attribution

