> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opper.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tags & usage attribution

> Attach key:value tags to calls, then slice usage and cost by customer, team, or environment — via the analytics API, the CLI, or the dashboard.

When one Opper account serves many customers, teams, or features, per-model spend isn't enough — you want to know *whose* usage it was. Tags solve that: attach up to 8 `key:value` pairs to any call (`customer_id:acme`, `team:eu`, `env:prod`), and they're recorded on the call's billing and metrics rows. From there you can group spend and token counts by any tag key.

Every call already carries built-in tags like `model`, so `group_by=model` works with no setup at all. Custom tags add your own dimensions on top.

There are three ways to attach tags. They differ only in *where* the tags travel — pick whichever your client can send:

| Mechanism                                                  | Use it when                                                                              |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| [`X-Opper-Tags` header](#tag-with-the-x-opper-tags-header) | You call the gateway and can set request headers (drop-in SDKs, LiteLLM, curl).          |
| [`tags` on `opper.call`](#tag-with-the-sdk)                | You use Opper's native SDK.                                                              |
| [URL session prefix](#tag-with-the-url)                    | All you control is a base URL — no headers, no body (coding agents, fixed integrations). |

## Tag with the `X-Opper-Tags` header

Works on every gateway (`/v3/compat`) call. The value is comma-separated `key:value` pairs; spaces after commas are fine, and sending the header more than once accumulates pairs.

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.opper.ai/v3/compat/chat/completions \
    -H "Authorization: Bearer $OPPER_API_KEY" \
    -H "X-Opper-Tags: customer_id:acme, team:eu" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "openai/gpt-5-mini",
      "messages": [{"role": "user", "content": "Hello"}]
    }'
  ```

  ```python OpenAI SDK theme={null}
  import os
  from openai import OpenAI

  client = OpenAI(
      base_url="https://api.opper.ai/v3/compat",
      api_key=os.environ["OPPER_API_KEY"],
      default_headers={"X-Opper-Tags": "customer_id:acme, team:eu"},
  )

  response = client.chat.completions.create(
      model="openai/gpt-5-mini",
      messages=[{"role": "user", "content": "Hello"}],
  )
  ```

  ```yaml LiteLLM theme={null}
  model_list:
    - model_name: claude-haiku
      litellm_params:
        model: openrouter/anthropic/claude-haiku-4-5
        api_base: https://api.opper.ai/v3/compat
        api_key: os.environ/OPPER_API_KEY
        extra_headers: {"X-Opper-Tags": "tenant:acme,env:prod"}
  ```
</CodeGroup>

Header values are taken literally — no escaping needed. A value may contain `:` (only the first colon splits key from value) but not `,`.

A malformed header is rejected with a `400` rather than silently dropped, so a templating bug on your side can't quietly produce untagged (unattributable) usage.

## Tag with the SDK

Opper's native `call` takes a `tags` object directly:

```python Python theme={null}
import os
from opperai import Opper

opper = Opper(http_bearer=os.environ["OPPER_API_KEY"])

result = opper.call(
    name="support_reply",
    instructions="Draft a reply to the customer's message",
    input=ticket_text,
    tags={"customer_id": "acme", "env": "prod"},
)
```

## Tag with the URL

Some clients let you configure nothing but a base URL — a coding agent's `ANTHROPIC_BASE_URL`, a vendor integration with a single "endpoint" field. For those, tags can ride in the URL path itself:

```
https://api.opper.ai/v3/session/sess_<uuid>/<key>:<value>/<key>:<value>/...
```

Everything after the tag segments is routed exactly like `/v3/compat/...`, so the client's SDK appends its usual path (`/chat/completions`, `/v1/messages`, …) and every request through that base URL carries the tags:

```python theme={null}
import os, uuid
from openai import OpenAI

session = f"sess_{uuid.uuid4()}"  # lowercase UUIDv4 with a sess_ prefix

client = OpenAI(
    base_url=f"https://api.opper.ai/v3/session/{session}/customer_id:acme/team:eu",
    api_key=os.environ["OPPER_API_KEY"],
)
```

Two things are specific to this mechanism:

* **`session_id` comes free.** The `sess_<uuid>` segment (a lowercase UUIDv4 you generate) is recorded as a `session_id` tag on every call, so you can also pull usage for one session: `GET /v2/analytics/usage?session_id=sess_...`. This is how `opper launch` accounts for each coding-agent session.
* **Percent-encode tag values.** Values are URL path segments and are decoded exactly once — encode any character that would break a path (`/` → `%2F`, and so on).

If a request carries both URL tags and an `X-Opper-Tags` header, the URL's value wins for any key present in both.

## Limits and validation

The same rules apply to all three mechanisms. Invalid tags fail the request with a `400` — they are never silently dropped.

| Rule          | Limit                                                                                                           |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| Tags per call | 8                                                                                                               |
| Key format    | Starts with a letter; then letters, digits, `_`, `.`, `-`; max 64 characters (`^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$`) |
| Value size    | 256 bytes                                                                                                       |
| Reserved keys | Keys starting with `opper.`, and `session_id` (set by the URL session prefix)                                   |

## Slice usage by tag

### Analytics API

`GET /v2/analytics/usage` aggregates cost (and any metadata fields you ask for) over time, and `group_by=<tag key>` splits every time bucket by that tag's values:

```bash theme={null}
curl -s "https://api.opper.ai/v2/analytics/usage?group_by=customer_id&fields=total_tokens&granularity=day" \
  -H "Authorization: Bearer $OPPER_API_KEY"
```

```json theme={null}
[
  {
    "time_bucket": "2026-08-10T00:00:00Z",
    "cost": "1.8421",
    "total_tokens": 261404,
    "customer_id": "acme"
  },
  {
    "time_bucket": "2026-08-10T00:00:00Z",
    "cost": "0.4114",
    "total_tokens": 60912,
    "customer_id": "globex"
  },
  {
    "time_bucket": "2026-08-10T00:00:00Z",
    "cost": "0.0231",
    "total_tokens": 4102,
    "customer_id": null
  }
]
```

Untagged usage shows up with the group key `null`, so the rows always sum to your total spend — nothing hides just because it wasn't tagged.

| Parameter               | What it does                                                                                                                                                             |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `group_by`              | Tag keys to split by. Repeat the param or pass a comma-separated list (`group_by=customer_id,env` returns one row per combination). Built-in tags like `model` work too. |
| `fields`                | Metadata fields to sum per row: `total_tokens`, `prompt_tokens`, `completion_tokens`, …                                                                                  |
| `granularity`           | Bucket size: `minute`, `hour`, `day` (default), `month`, `year`.                                                                                                         |
| `from_date` / `to_date` | Time range (inclusive / exclusive). Defaults to the current month.                                                                                                       |
| `session_id`            | Narrow to one URL-prefix session (`sess_...`).                                                                                                                           |

### CLI

The [Opper CLI](/developer-tools/cli) wraps the same endpoint:

```bash theme={null}
opper usage list --group-by=customer_id
```

### Dashboard

The usage page at [platform.opper.ai](https://platform.opper.ai) has a **Tag** filter that scopes the whole page — spend chart, breakdowns, attribution, CSV export — to one `key:value` tag. The dropdown lists your organization's recently seen tags, so anything you tag shows up there automatically.

<Note>
  The dashboard Tag filter is rolling out and may not be visible on your account yet. The analytics API and CLI above work today.
</Note>

## What's next

<CardGroup cols={2}>
  <Card title="Drop-in SDKs" icon="plug" href="/build/gateway/drop-in-sdks">
    Point your existing OpenAI, Anthropic, or Google SDK at the gateway.
  </Card>

  <Card title="Traces" icon="wave-pulse" href="/control-plane/trace">
    Tags on spans make individual traces filterable, too.
  </Card>

  <Card title="Integrations" icon="puzzle-piece" href="/overview/integrations">
    LiteLLM, Vercel AI SDK, and more — with per-tenant attribution built in.
  </Card>

  <Card title="Models & pricing" icon="server" href="/capabilities/models">
    Per-token pricing for every model the gateway serves.
  </Card>
</CardGroup>
