# Attribution headers

Attribution is the product. A proxied call with no attribution is a recorded
dollar with no owner, which is the problem Spendline exists to solve.

## The contract

| Header | Required | Cardinality | Value |
|---|---|---|---|
| `x-spendline-key` | Always | Per process | The Spendline API key, from `SPENDLINE_API_KEY`. |
| `x-agent-id` | When enforcement is on | **Per agent** | Stable name of the bot/feature: `support-bot`, `doc-summarizer`. |
| `x-customer-id` | When enforcement is on | **Per request** | The end customer this call is billable to. |
| `x-spendline-tags` | When enforcement is on | Per call site | JSON object carrying a cost-centre key. |
| `x-workflow-id` | Optional | Per run | Groups a multi-step run. |
| `x-step-name` | Optional | Per step | Labels a step inside a workflow. |
| `x-source-route` | Optional | Per call site | The app route that triggered the call. |

## x-customer-id must vary per request

This is the mistake that quietly ruins the data. If you set `x-customer-id` once
in `default_headers`, every call is attributed to the same customer and
per-customer cost is meaningless.

Thread the customer from request context:

```python
# WRONG: every call attributed to one customer
client = OpenAI(default_headers={"x-customer-id": "cus_123"})

# RIGHT: per request
client.chat.completions.create(..., extra_headers={"x-customer-id": request.customer_id})
```

If the call site genuinely has no customer concept (an internal batch job, a
cron task), pick a stable synthetic id such as `internal:nightly-reindex` and say
so in your report. Do not omit the header and do not invent a random value per
call, a random id makes every request look like a new customer.

## x-spendline-tags

A JSON object that **must** include one of these keys:

- `cost_center`
- `costCenter`
- `department`
- `team`

```
x-spendline-tags: {"team":"support","env":"prod","tier":"enterprise"}
```

Extra keys are kept and available for reporting. A tags header without a
cost-centre key fails the attribution check.

## x-agent-id

One stable value per agent, bot or feature, not per request, and not per
customer. This is the dimension hierarchical budgets scope to, so it must be
stable enough to budget against.

## Workflow attribution

For a multi-step agent run, set `x-workflow-id` once per run and `x-step-name`
per step:

```python
workflow_id = f"run_{uuid4()}"

for step in ("retrieve", "draft", "review"):
    client.chat.completions.create(..., extra_headers={
        "x-customer-id": customer_id,
        "x-workflow-id": workflow_id,
        "x-step-name": step,
    })
```

This makes the whole run costable as a unit, which is what "what does one
support ticket cost us" actually requires.

## Enforcement

When attribution enforcement is on, a call missing a required field is rejected:

```
HTTP 400
{"error_type":"missing_required_attribution","missing_fields":["customer_id"]}
```

Send the headers from day one rather than turning enforcement on later and
discovering which call sites were never instrumented.

## Body fallback

`customer_id`, `agent_id`, `workflow_id` and `step_name` are also read from a
`metadata` object or the top level of the request body when the header is
absent. Headers are preferred, they survive SDK body validation, which may
strip unknown fields.

## Timestamps are server-controlled

You cannot set the timestamp that decides which month a call lands in. Spendline
stamps it server-side. A client-supplied `created_at` is ignored for spend
attribution by design.
