# Data & privacy (/docs/concepts/data-privacy)



TokenPolice works on **token metadata**. The text of your prompts and the model's completions
never reaches TokenPolice — not on the check before a call, not on the log after it. On Python
there is one path by which that text can reach a **third party of your own choosing**; it is
the [callout under *Where it goes*](#where-it-goes), and it is worth reading before you deploy.

Instrumentation runs inside your own process. It reads the shape of each call, not its
content, and sends a small record to the collector.

## What leaves your process [#what-leaves-your-process]

| What                             | Example                                                                                                                                                               |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Model and provider               | `gpt-4o-mini`, `openai`                                                                                                                                               |
| Token counts                     | 812 in, 240 out, cached counts where the provider reports them — TokenPolice computes the USD cost from these on its side                                             |
| The identity tags **you** chose  | `user_id`, `paid_plan`, `session_id`                                                                                                                                  |
| Names you gave the work          | the workflow/session name, and any span name you set                                                                                                                  |
| The kind of call                 | chat, embedding, image, speech, transcription, video, OCR                                                                                                             |
| Tool call shape                  | the tool's name, plus **hashes and lengths** of its arguments and result — never the values                                                                           |
| A content fingerprint            | a hash, used for loop detection (below)                                                                                                                               |
| The provider endpoint you called | `https://api.groq.com/openai/v1` — scheme, host and path only. Credentials, query string and fragment are stripped before it is sent. Sent as `model_extras.api_base` |
| Audio file names, **verbatim**   | on speech and transcription calls the file's own name is sent as-is — `intake-call.mp3`. It is not hashed. See the warning below                                      |
| Error class on a failed call     | how much depends on `error_detail` (below)                                                                                                                            |
| Any `metadata` you attach        | whatever you put there — see the note below                                                                                                                           |

## What never leaves your process [#what-never-leaves-your-process]

* Your prompts, system prompts, messages, and retrieved context.
* The model's completions, in whole or in part.
* Tool arguments and tool results as values.
* Your provider API keys.

## The content fingerprint [#the-content-fingerprint]

Loop detection needs to know when an agent is asking the model the *same thing* over and
over. To do that without seeing the question, the SDK sends a **hash** of the call's content —
a fixed-length fingerprint. Two identical prompts produce the same fingerprint.

The hash is a truncated SHA-1: 16 hex characters, with no salt. It cannot be *reversed* — there
is no procedure that reads the text back out of it. Be precise about what that buys you, though.
Agent prompts are templated and low-entropy by construction, so someone who already holds a
candidate prompt can hash it themselves and see whether it matches. The fingerprint hides your
content from a party starting with nothing; it does not stop a party starting with a good guess
from confirming the guess. That is the whole caveat on "text never leaves": a one-way hash of it
does, and only so a runaway loop can be spotted.

## Error detail [#error-detail]

When a *provider* call fails, the SDK records why. The `error_detail` option in
[`init()`](/docs/sdk/init) decides how much of that leaves your process:

| Mode                               | What is sent                                            |
| ---------------------------------- | ------------------------------------------------------- |
| `"none"`                           | A classification only — no message text, no hash.       |
| `"redacted"&#x60; &#x2A;(default)* | The exception's class name, plus a hash of the message. |
| `"raw"`                            | The verbatim error string, truncated.                   |

`"raw"` is opt-in for a reason: a provider's 400 response sometimes quotes the offending part
of your prompt back at you. Turn it on only when you're debugging, and turn it off after.

<Callout type="warn">
  **`metadata` is the one field you can over-share with.** Everything else is metadata by
  construction; `metadata` is free-form, so whatever you put in it is what gets sent. Use it
  for tags you'd be comfortable seeing in a dashboard — `tenant_id`, `feature`, `region` — and
  keep user content and secrets out of it.
</Callout>

<Callout type="warn">
  **Audio file names are sent unhashed.** On a speech or transcription call the SDK reads the
  file's name and sends it as the entry name. File names routinely carry more than you think —
  `jane-doe-intake-call.mp3`. If your pipeline names files after people, cases or accounts,
  pass the audio under a neutral name (or a stream/buffer with no name) before it reaches the
  provider client.
</Callout>

## Where it goes [#where-it-goes]

To the collector at `https://collect.tokenpolice.ai`, over HTTPS, authenticated with your
`tp_sk_…` key — or to your own address if you [point the SDK
elsewhere](/docs/get-started/api-key). That record — the table above — is everything
TokenPolice receives, and TokenPolice sends it nowhere else.

<Callout type="warn">
  **Python: check your own OpenTelemetry setup before you turn this on.**

  The Node SDK builds a private tracer provider with **no exporters** and binds its
  instrumentors to that, so its spans have nowhere to go but TokenPolice's own record.

  The Python SDK behaves differently when your app has *already* configured a
  `TracerProvider` — Datadog, Honeycomb, Langfuse, Logfire, Sentry tracing, or a plain OTLP
  exporter. In that case TokenPolice attaches to **your** provider instead of building its
  own, and the OpenLLMetry instrumentors underneath it bind to the global provider too. Those
  instrumentors emit `gen_ai.prompt.*` and `gen_ai.completion.*` attributes carrying full
  prompt and completion text, and that behaviour is **on by default**.

  None of that text goes to TokenPolice. But it will start flowing into your own tracing
  backend, which may not have been receiving it before — and if that backend is a third-party
  SaaS, the text has left your servers.

  To prevent it, set `TRACELOOP_TRACE_CONTENT=false` in the environment before `tp.init()`.
  The SDK does not set it for you. With it set, the spans still carry model, token counts and
  timing; only the content attributes are dropped.
</Callout>

If your security review needs more than this page covers, write to
[hello@tokenpolice.ai](mailto:hello@tokenpolice.ai) and ask.

## Next [#next]

<Cards>
  <Card title="Why TokenPolice" href="/docs/why" description="The problem this data is collected to solve." />

  <Card title="Identity" href="/docs/concepts/identity" description="The tags you choose to attach, and what they unlock." />

  <Card title="init() parameters" href="/docs/sdk/init" description="error_detail and everything else you can configure." />
</Cards>
