# Python server SDK

URL: https://traqio.app/docs/sdks/python
Last updated: 2026-10-16

Report sign-ups and purchases from a Python backend with the traqio package, and tie each one to the ad click it came from. Django, Flask, FastAPI.

`traqio` is the Traqio SDK for Python backends — the server half of Traqio's web SDK. The conversion that counts on a website — a sign-up, a subscription, a purchase — happens on your backend, where the user id is known. This package reports it with the **trackID**, the ad click id that [@traqio/web](https://traqio.app/docs/sdks/web) kept in first-party storage since the landing page, and Traqio credits the campaign that brought the user — [deterministically](https://traqio.app/docs/concepts/web-attribution), by that click's id. Standard library only; Django, Flask, FastAPI or any other framework.

## Requirements

- Python 3.9 or later. No dependency; typed (`py.typed`).
- A **server key** for the product (`tq_sk_…`). Neither the app's SDK key nor a web key works here.
- [@traqio/web](https://traqio.app/docs/sdks/web) on the site, and ads pointing at [tracking links](https://traqio.app/docs/concepts/tracking-links) with a website destination — that is how the trackID reaches your sign-up form.

## Set up

### Create a server key

In the console, open **Integrations → SDK** and create a **server key**. It is shown once: put it in the backend's environment, never in code or in a page. Traqio keeps only a fingerprint of it; a lost key is replaced by a new one, and the old one revoked.

.env:
```bash
TRAQIO_SECRET_KEY=tq_sk_...
```

### Install the package

```bash
pip install traqio
```

Until the package is on PyPI, install the wheel we send you:

```bash
# the file you received, committed inside the backend's repository
pip install ./vendor/traqio-0.1.0-py3-none-any.whl
```

### Create the client

```python
import os
from traqio import Traqio

# One client per process. The key comes from the server's environment, never from code.
traqio = Traqio(os.environ["TRAQIO_SECRET_KEY"])
```

The client holds no connection: one per process is enough. It refuses a web key (`tq_web_…`).

### Report the sign-up with its trackID

Call `signup()` once, where the account is created. @traqio/web writes the trackID in the `traq_click` cookie on your own domain, so the browser sends it with the sign-up request:

Django:
```python
from traqio import TraqioError, track_id_from_request

def signup(request):
    user = User.objects.create_user(...)
    try:
        traqio.signup(str(user.pk), track_id_from_request(request))
    except TraqioError as error:
        logger.warning("traqio: %s", error)  # never fail the sign-up itself
    return redirect("/welcome")
```

Flask:
```python
from flask import request
from traqio import track_id_from_request

@app.post("/signup")
def signup():
    user = create_user(request.form)
    traqio.signup(str(user.id), track_id_from_request(request))
    return redirect("/welcome")
```

FastAPI:
```python
from fastapi import BackgroundTasks, Request
from traqio import track_id_from_request

@app.post("/signup")
async def signup(request: Request, background: BackgroundTasks):
    user = await create_user(await request.json())
    # The client is synchronous: a background task runs it in a thread, after the response.
    background.add_task(traqio.signup, str(user.id), track_id_from_request(request))
    return {"id": user.id}
```

If the backend is on another domain than the site, the cookie does not reach it: send `Traqio.getTrackId()` from the browser with the form instead. The sign-up is the moment the user is **acquired**; the last click before it, inside your [attribution window](https://traqio.app/docs/concepts/attribution-windows), gets the credit.

### Track the conversions

```python
traqio.track(
    str(user.pk),
    "purchase",
    value=49,
    currency="EUR",
    transaction_id=payment_intent.id,  # one payment, one row — whoever reports it
    store="stripe",
)
```

Conversions follow the user: they are credited to the campaign that acquired them.

## What the response tells you

```python
result = traqio.signup(str(user.pk), track_id)

result.track_id                 # "matched" | "unknown" | "expired" | "claimed" | "refused" | None
result.acquired                 # True: the sign-up is recorded
result.attribution.network      # "meta", "tiktok"… or "organic"
result.attribution.campaign_id  # the campaign that brought the user
```

| track_id | Meaning |
| --- | --- |
| matched | The click is now this user's. A retry answers matched again. |
| unknown | No click of this product has that id — a typo, another product's link, or a value @traqio/web did not write. |
| expired | The click is older than the product's attribution window. |
| claimed | Another user, or an app install, already owns that click. A click credits one identity. |
| refused | The click failed an anti-fraud rule (it came through an anonymising network). It is counted, never credited. |

## What it does today, and what it does not

|  | Today |
| --- | --- |
| Tie a sign-up to its click, and say who was credited | Yes — in the response |
| Record purchases with value, currency and payment id, deduplicated | Yes |
| Leave your apps' installs, revenue and ROAS untouched | Yes — never attached to an app install |
| Show web conversions in the console's reports | Yes — per campaign, beside installs |
| Send web conversions to Meta or TikTok | Not yet |

Conversions sent here appear in **Live events**, marked as not matched to an install — expected for a website. A sign-up credited to a campaign is counted in the ROAS report as a web conversion of that campaign, beside its installs, and the response of each call gives you the attribution at once.

## Errors and retries

A network error, a timeout, a `429` or a `5xx` is retried — twice by default, with the same body, so nothing is counted twice. A `4xx` (wrong or revoked key, invalid payload) is not retried. Both end as a `TraqioError`:

```python
from traqio import TraqioError

try:
    traqio.signup(str(user.pk), track_id)
except TraqioError as error:
    # error.status: 401 = wrong or revoked key, 400 = invalid payload, None = network
    logger.warning("traqio: %s (status %s)", error, error.status)
```

> **Report after your own write** Call Traqio once the account or the payment is saved, and never let a Traqio error fail it.

## API reference

Traqio(…):
```python
Traqio(
    secret_key,            # the server key, tq_sk_… — required
    endpoint="https://api.traqio.app",
    timeout=10.0,          # seconds per attempt
    max_retries=2,         # after a network error, a 429 or a 5xx
    transport=None,        # a custom transport (proxies, tests)
)
```

| Method | What it does |
| --- | --- |
| signup(user_id, track_id=None, events=()) | Reports the sign-up — the acquisition — with its trackID. The first one is kept. |
| track(user_id, name, *, value, currency, transaction_id, store, event_id, track_id) | Records one conversion of a known user. |
| send(user_id, *, track_id, signup, events) | The raw call behind both, for several events at once. |
| track_id_from_request(request) | The trackID from a Django, Flask or Starlette request, or None. |
| track_id_from_cookie_header(header) | The same, from a raw Cookie header. |

> **Proof so far** Tested on Python 3.9 and 3.11, including the real HTTP transport, and run end to end against a running Traqio API: a sign-up carrying a trackID came back matched with its campaign, an unknown click was told unknown, a wrong key was refused. Not yet run in a customer's production backend.

## Frequently asked questions

**Is there an async client?**
Not today: the client is synchronous and uses the standard library only. In FastAPI or Starlette, hand the call to a background task, which runs it in a thread after the response — the page shows how.

**Which frameworks can read the trackID?**
track_id_from_request reads Django requests (the Cookie header), Flask and Starlette / FastAPI requests (their cookies), and any object with a headers mapping. It returns None when the visitor did not come from an ad.

**What if the visitor did not come from an ad?**
Pass None. The sign-up is still recorded, and the user is organic: no campaign is credited.

**Will a retry count a purchase twice?**
No. Each call mints its event ids once and retries the identical body, and a purchase is also keyed on its transaction_id, so the same payment reported twice is stored once.
