Skip to main content

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 host URL, for example https://ingest.traceten.com.
  • A visitor_id for 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 an h:<64-hex> identify hash. See supplying identifiers.

Install

Zero runtime dependencies. The SDK uses the standard library only.

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 a visitor_id
  • an event_name that fails its pattern
  • a url that is not a valid URL
  • a value_cents that is not a non-negative integer
The client is thread-safe and can be shared across threads.

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:
  1. It gets you in. The SDK posts to {host}/v1/server/events, {host}/v1/server/conversions and {host}/v1/server/payments, which return 401 without a valid key. The open /v1/events and /v1/conversions endpoints stay open for the browser snippet, which cannot hold a secret.
  2. 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_id is 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.
The constructor validates the key’s shape and raises 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.
Which user agent gets recorded. The 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.
Keep visitor_id opaque: a random first-party id or a salted hash you control. Never a bare sha256(email) or any hash of an email or phone number (that is re-identifiable PII). The SDK enforces the shape (UUID or h:<64-hex>) but cannot see what you hashed. Keep properties to ids and enums such as {"plan": "pro"}. Do not put emails, names, or phone numbers in them.

goal(name, *, visitor_id, properties=None, value_cents=None, currency=None, session_id=None, timestamp=None)

Records a goal completion. Same arguments, same endpoint, and same payload as 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.
Calling it again with the same 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.
Do not send payments here for a processor you have also connected natively. Traceten would record the payment twice and overstate your revenue. See avoiding duplicate payments.

flush() -> None

Sends everything queued now, blocking until both queues drain.

close() -> None

Flushes, stops the background thread, and marks the client closed. Idempotent. It is also registered with 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/events in 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/conversions one 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_retries times.
  • 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

  1. Send one page() call from your backend.
  2. Open Sites → Events in the Traceten dashboard for that site.
  3. The event appears in the live event feed within a few seconds.
If nothing arrives, wire up 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(), not request.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 up on_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