# Python SDK

> Send events and confirm contact consent from a Python backend with the whisperr package, including Django middleware and billing events.

The `whisperr` package sends events and identify calls from a Python backend. Use it for facts only your server knows, such as payment failures, cancellations and plan changes, and to confirm contact consent with the server key.

## Install

```bash
pip install whisperr
```

The package is [`whisperr`](https://pypi.org/project/whisperr/) on PyPI. The current version is 0.2.1. It needs Python 3.8 or later and has no runtime dependencies. For Django, `pip install "whisperr[django]"` also installs Django 3.2 or later.

## Quickstart

`whisperr_client.py`

```python
import os
from whisperr import Whisperr


# WHISPERR_API_KEY is the server key (wrk_…).
whisperr = Whisperr(api_key=os.environ["WHISPERR_API_KEY"])


# A server-side event:
whisperr.track("user_8842", "payment_failed", {"amount_cents": 4900, "reason": "card_declined"})


# Traits and a contact the user agreed to:
whisperr.identify(
    "user_8842",
    traits={"first_name": "Ada", "plan": "pro"},
    channels=[{
        "type": "email",
        "address": "ada@example.com",
        "opted_in": True,
        "verified": True,
    }],
)


# Before the process exits (also registered with atexit):
whisperr.shutdown()
```

Create the server key as a **Server** key in **Settings → API keys**. Hold one client for the life of the process.

`track(external_user_id, event_type, properties=None, context=None)` always takes the user id first. Pass the same id your app passes to `identify()`, and backend and frontend events land on [one timeline](/concepts/events/#one-user-one-timeline). Event names must be lowercase `snake_case`; the SDK drops any other name and reports it to `on_error` as `dropped`.

## How do I confirm contact consent with the Python SDK?

The Python SDK runs with the server key, so its `identify()` is how you confirm consent. Send `"opted_in": True` only when the user agreed to messages on that channel, and `"verified": True` only for an email address your app confirmed. Send `"opted_in": False` to record an opt-out.

> The Python SDK sends `opted_in: true` for a channel when you leave `opted_in` out. The `email`, `phone` and `push_token` shortcuts do the same. Send a contact only when you hold consent for it, or pass `"opted_in": False`.

See [Contact consent](/concepts/consent/).

## Django

`settings.py`

```python
WHISPERR_API_KEY = os.environ["WHISPERR_API_KEY"]


MIDDLEWARE = [
    # ... after django.contrib.auth.middleware.AuthenticationMiddleware ...
    "whisperr.django.WhisperrMiddleware",
]
```

`views.py`

```python
def upgrade(request):
    request.whisperr.track("plan_upgraded", {"plan": "pro"})
    ...
```

`request.whisperr.track()` and `request.whisperr.identify()` use the authenticated user, `str(request.user.pk)` by default. When the user is not signed in, the calls do nothing. To read the id from somewhere else, set `WHISPERR_RESOLVE_USER` to the dotted path of a function `resolve(request) -> str | None`.

For work outside a request, such as Celery tasks, webhooks and management commands, use the shared client with the user id from your own data:

`tasks.py`

```python
from whisperr.django import get_client


get_client().track(str(subscription.user_id), "subscription_cancelled", {"reason": reason})
```

Django settings the integration reads:

| Setting                 | Default                    | Notes                              |
| ----------------------- | -------------------------- | ---------------------------------- |
| `WHISPERR_API_KEY`      | `""`                       | Server key (`wrk_…`).              |
| `WHISPERR_BASE_URL`     | `https://api.whisperr.net` | Ingestion base URL.                |
| `WHISPERR_APP_VERSION`  | —                          | Sent as `context.app_version`.     |
| `WHISPERR_DISABLED`     | `False`                    | Captures and sends nothing.        |
| `WHISPERR_DEBUG`        | `False`                    | Logs warnings to stderr.           |
| `WHISPERR_RESOLVE_USER` | —                          | Dotted path to a user-id resolver. |

## How do I set the message language from Python?

Pass the language the user picked in your product as the `app_language` trait. Use a BCP 47 tag such as `de` or `pt-BR`. The Python SDK has no separate method for it and never guesses a language.

```python
whisperr.identify("user_8842", traits={"app_language": "pt-BR"})
```

## What the Python SDK adds to every event

| `context` key | Value                                      |
| ------------- | ------------------------------------------ |
| `sdk_name`    | `whisperr-python`                          |
| `sdk_version` | The SDK version, for example `0.2.1`       |
| `app_version` | The `app_version` option. Not set: no key. |

A key you pass in the `context` argument of `track()` wins over these.

## How do I send billing events from Python?

When Whisperr has no billing source connected for your app (such as [RevenueCat](/integrations/revenuecat/)), send membership changes from your backend with the reserved names in `BillingEvent`. Never send them from the browser or the app.

```python
from whisperr import BillingEvent


whisperr.track(
    "user_8842",
    BillingEvent.MEMBERSHIP_STARTED,
    {
        "plan_id": "pro_monthly",
        "price": 9.99,  # major units, not cents
        "currency": "USD",  # ISO 4217, upper case
        "plan_period": "P1M",  # ISO 8601 duration
        "transaction_id": "txn_123",
        "store": "stripe",
        "is_first_ever": True,
        "access_source": "self_subscription",
        "current_period_ends_at": "2026-11-12T10:00:00Z",
    },
)
```

`BillingEvent` has `TRIAL_STARTED`, `TRIAL_CONVERTED`, `TRIAL_AUTO_RENEW_DISABLED`, `MEMBERSHIP_STARTED`, `RENEWAL_SUCCEEDED`, `AUTO_RENEW_DISABLED`, `AUTO_RENEW_ENABLED`, `MEMBERSHIP_ENDED`, `BILLING_ISSUE_STARTED`, `BILLING_RECOVERED` and `REFUND_GRANTED`. The [Node SDK page](/sdks/node/#how-do-i-send-billing-events-from-node) lists the properties of each event and the closed value lists.

Register these event names for your app; [setup with your AI agent](/agents/) registers the events your app sends. Whisperr stores an event with an unregistered name but does not map it to your app’s events.

## Short-lived processes

In scripts, serverless handlers and management commands, call `flush()` or `shutdown()` before the process ends:

```python
whisperr.track(user_id, "report_generated")
whisperr.flush()
```

`flush()` blocks until the queue is sent or sending cannot continue.

## How does the Python SDK deliver events?

- Events go to `POST /v1/events/batch` and identify calls to `POST /v1/identify`, in order, with the `X-API-Key` header.
- Every event has a `$message_id` that stays the same across retries, so Whisperr removes duplicates.
- `429`, `5xx` and network errors are retried with exponential backoff, up to 30 seconds between tries. After `max_retries`, the batch stays queued for the next flush. The Python SDK does not read `Retry-After`.
- On `401` or `403` the SDK stops sending and keeps the batch. Other `4xx` responses drop the batch and report `dropped`.
- The queue is not written to disk. A crash loses unsent events.

See [Delivery, retries, idempotency](/api/delivery/).

## Options

| Option            | Default                    | Notes                                                                                      |
| ----------------- | -------------------------- | ------------------------------------------------------------------------------------------ |
| `api_key`         | —                          | Server key (`wrk_…`). Required unless `disabled`.                                          |
| `base_url`        | `https://api.whisperr.net` | Ingestion base URL.                                                                        |
| `app_version`     | —                          | Your release version. Sent as `context.app_version`.                                       |
| `flush_at`        | `20`                       | Send when this many events are queued.                                                     |
| `flush_interval`  | `10.0`                     | Background send interval (seconds).                                                        |
| `max_queue_size`  | `10000`                    | Oldest events drop when the queue is full.                                                 |
| `max_batch_size`  | `500`                      | Events per batch request. 500 is the maximum.                                              |
| `max_retries`     | `6`                        | Retries before the batch waits for the next flush.                                         |
| `request_timeout` | `10.0`                     | Per-request timeout (seconds).                                                             |
| `disabled`        | `False`                    | Captures and sends nothing. Useful in tests.                                               |
| `debug`           | `False`                    | Logs warnings to stderr.                                                                   |
| `on_error`        | —                          | `Callable[[WhisperrError], None]`. `error.type` is `auth`, `dropped` or `retry_exhausted`. |

## Common questions

### Which API key does the Python SDK use?

The server key (wrk_…), shown as a Server key in Settings → API keys. Keep it in your backend secrets. It can always opt a contact in.

### Does the Python SDK block my request while it sends?

No. track() and identify() add to an in-memory queue and return at once. A background daemon thread sends the queue.

### Which Python versions does the whisperr package support?

Python 3.8 or later. The package uses only the standard library.

### How do I send events from a Celery task with the Django integration?

Call whisperr.django.get_client() to get the process-wide client, then call track() with the user id from your own data.

Source: https://docs.whisperr.net/sdks/python/
