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

# Customer attribution

> Tag each request with your own customer (and optionally a feature) to see spend per customer in Analytics — no change to routing, billing, or auth.

If you build a product on top of AI Reserve, your usage rolls up to one organization — but
you usually want to know what each of **your** customers costs you. Customer attribution lets
you tag every request with your own customer id (and, optionally, the feature that made the
call). The gateway records both on the usage record for that request, and the portal's
Analytics page breaks spend down by customer.

Tags are reporting-only. They never change which model serves a request, what it costs, or
whether it is allowed. Requests without a tag behave exactly as before.

There are two ways to get the customer id onto a request: tag each request yourself (the three
options below, best when your own backend calls AI Reserve for many customers through one key),
or hand each customer a [customer key](#option-c-customer-keys-recommended) that carries the id
for them.

## Three ways to tag a request

The gateway reads the customer id from the first of these that is present, in this order:

| Where | Field | Works on |
| - | - | - |
| Request header | `x-aireserve-customer: <id>` | Every endpoint |
| OpenAI-compatible body | `"user": "<id>"` | `/v1/chat/completions`, `/v1/responses`, `/v1/images/generations` |
| Anthropic body | `"metadata": {"user_id": "<id>"}` | `/v1/messages` |

If you already pass `user` to the OpenAI SDK or `metadata.user_id` to the Anthropic SDK, you
are done — the gateway picks those up with no code change and still forwards the fields to
the provider unchanged. The header is for everything else (Bedrock-style `/model/…/converse`,
text-to-speech, GPU endpoints) and for when you want one mechanism across every SDK.

```bash curl theme={"dark"}
curl https://api.aireserve.com/v1/chat/completions \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-aireserve-customer: cust_8f3a2b" \
  -H "x-aireserve-feature: draft-email" \
  -d '{
    "model": "gpt-5.5",
    "messages": [{"role": "user", "content": "Draft a follow-up to the Acme call."}]
  }'
```

```python Python theme={"dark"}
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.aireserve.com/v1",
    api_key=os.environ["AIRESERVE_API_KEY"],
)

# Option A — the OpenAI `user` field; no header needed.
res = client.chat.completions.create(
    model="gpt-5.5",
    messages=[{"role": "user", "content": "Draft a follow-up to the Acme call."}],
    user="cust_8f3a2b",
)

# Option B — headers; add the optional feature tag too.
res = client.chat.completions.create(
    model="gpt-5.5",
    messages=[{"role": "user", "content": "Draft a follow-up to the Acme call."}],
    extra_headers={
        "x-aireserve-customer": "cust_8f3a2b",
        "x-aireserve-feature": "draft-email",
    },
)
```

```typescript TypeScript theme={"dark"}
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.aireserve.com/v1",
  apiKey: process.env.AIRESERVE_API_KEY,
});

// Option A — the OpenAI `user` field; no header needed.
await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [{ role: "user", content: "Draft a follow-up to the Acme call." }],
  user: "cust_8f3a2b",
});

// Option B — headers; add the optional feature tag too.
await client.chat.completions.create(
  {
    model: "gpt-5.5",
    messages: [{ role: "user", content: "Draft a follow-up to the Acme call." }],
  },
  {
    headers: {
      "x-aireserve-customer": "cust_8f3a2b",
      "x-aireserve-feature": "draft-email",
    },
  },
);
```

On the Anthropic surface the SDK's own `metadata.user_id` is enough:

```python theme={"dark"}
message = anthropic_client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Draft a follow-up to the Acme call."}],
    metadata={"user_id": "cust_8f3a2b"},
)
```

## Option C: customer keys (recommended)

When the requests come from the customer's own systems — a reseller integration, a tenant you
onboard with their own credentials — give that customer a **customer key** instead of asking
them to tag anything. A customer key is one of your organization's API keys bound to one
customer id: every request made with it is billed to you and reported under that customer,
with nothing to add to the request. Add the customer's **organization / sub-account** and the
key becomes a **sub key** that also splits the customer's spend by their organizations
(`cust_8f3a2b › emea`).

Create them in the portal — **Keys → Create key → One of your customers** (enterprise admins)
— or over the API with one of your organization-level keys:

```bash curl theme={"dark"}
# A key for the customer
curl -X POST "https://api.aireserve.com/v1/customers/cust_8f3a2b/keys" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme production"}'

# A sub key for one of the customer's organizations
curl -X POST "https://api.aireserve.com/v1/customers/cust_8f3a2b/keys" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"org_id": "emea"}'
```

```python Python theme={"dark"}
import os, requests
from urllib.parse import quote

headers = {"Authorization": f"Bearer {os.environ['AIRESERVE_API_KEY']}"}
customer_id = "cust_8f3a2b"

created = requests.post(
    f"https://api.aireserve.com/v1/customers/{quote(customer_id, safe='')}/keys",
    json={"org_id": "emea"},
    headers=headers,
).json()
secret = created["key"]  # shown once — hand it to the customer and store it
```

```typescript TypeScript theme={"dark"}
const customerId = "cust_8f3a2b";
const created = await fetch(
  `https://api.aireserve.com/v1/customers/${encodeURIComponent(customerId)}/keys`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.AIRESERVE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ org_id: "emea" }),
  },
).then((r) => r.json());
const secret = created.key; // shown once — hand it to the customer and store it
```

The response carries the plaintext key exactly once. The customer then calls AI Reserve with it
like any other key — same base URL, endpoints, and limits as your organization. A customer
key's binding always wins: if the customer's code also sends `x-aireserve-customer` or `user`,
the key's customer is what gets reported. The `x-aireserve-feature` tag below still works on a
customer key.

Each customer key on the Keys page carries a *Customer* badge and a **Spend for this customer**
link straight into Analytics, where a customer with sub keys expands into its organizations in
the *Spend by Customer* card. Listing and revoking keys, the error codes, and the full schemas
are on the [Customer keys](/api-reference/customer-keys) reference page.

## The optional feature tag

`x-aireserve-feature` names the feature or action inside your product that made the call —
`draft-email`, `summarize-thread`, `nightly-sync`. It is recorded next to the customer id on
every usage record and comes along in exports, so you can later split one customer's spend by
what they used. It is header-only and entirely optional.

## Limits and validation

| Tag | Header | Max length | Allowed characters |
| - | - | - | - |
| Customer id | `x-aireserve-customer` | 128 | Printable characters (no control characters) |
| Feature | `x-aireserve-feature` | 64 | Printable characters (no control characters) |

Values are trimmed of surrounding whitespace. A tag that is empty, too long, or contains
control characters is **ignored** — the request is served and billed normally, it just lands
as untagged. Tagging never produces an error response. When the header is invalid but the body
carries a valid `user` / `metadata.user_id`, the body value is used.

> [!WARNING]
>
> **Use opaque ids, never emails or names.** Send the identifier your own system uses for the
> customer (`cust_8f3a2b`, a UUID, a tenant slug). Tags are stored on usage records and appear
> in Analytics and in exports; personal data does not belong there.

## Where it shows up

* **Analytics → Spend by Customer.** The portal's Analytics page gains a *Spend by Customer*
  card listing your top 25 customers in the selected date range by spend, with requests,
  tokens, and each customer's share of spend. An *(untagged)* row holds every request that
  carried no customer id, so the table always reconciles with the period total. The card
  appears only once at least one request in the range carried a customer id; it follows the
  same date range and team/member filters as every other card.
* **CSV / JSON download.** Each card downloads its own rows (`endCustomerId`, `requests`,
  `tokens`, `spendUsd`, `spendShare`), and the page-level *Download all data (JSON)* includes
  the same breakdown.
* **The API.** `GET /v1/customers/usage` returns the same figures per customer — plus, once
  your organization has [pricing rules](#pricing-you-charge), what each customer owes and your
  margin — and `GET /v1/customers/{customerId}/usage` drills into one customer. See
  [Check the amount](#check-the-amount-api).

> [!NOTE]
>
> Requests whose results are produced by a background job after the call returns — image
> edits and video generation — are not attributed to a customer yet. Everything served inline
> (chat, messages, responses, image generation, speech, Bedrock-style converse, GPU chat
> completions) is.

## Pricing you charge

Spend by Customer tells you what each customer **cost you**. Pricing rules add what each
customer **owes you**: a rule is the price your organization charges for a slice of its tagged
usage, and every matched request is priced with it, so revenue and margin sit next to cost in
the [usage API](#check-the-amount-api). Each rule has a *scope* (which customers and features
it covers), a *kind* (how the amount is computed), and an *effective window* (the period of
usage it prices).

| Kind | What the customer is charged |
| - | - |
| Markup | Your cost plus a percentage — a 20% markup bills a request that cost you $0.010 at $0.012 |
| Per request | A fixed amount for every successful request |
| Per token | Your own rate card: a price per 1M prompt tokens and a price per 1M completion tokens |
| Monthly flat | A fixed fee for every UTC calendar month in which the customer made at least one matched request |

| Scope | Applies to |
| - | - |
| All customers | Every tagged request — your default price |
| One customer | Every request tagged with that customer id |
| One feature | Every tagged request carrying that `x-aireserve-feature` tag, whoever the customer |
| One customer and one feature | Exactly that pair |

When several rules cover a request, the **most specific scope wins**: customer and feature,
then customer only, then feature only, then all customers. Within one scope, the rule whose
window covers the request's UTC day and has the latest *effective from* wins, so a price change
is a new rule with a later start — the old one keeps pricing the usage it covered. A window may
start in the future (a scheduled price) and may be closed with an end date; closing it never
rewrites history. Usage that no rule covers is reported **at cost** and flagged `priced: false`,
so a gap in your price list shows up as a visible zero margin rather than a quietly wrong total.

How the amounts come together:

* Markup, per-request, and per-token rules are applied to each successful request and summed.
  A monthly flat fee is booked once per customer per UTC month, on the first day in that month
  with matched usage, and is never pro-rated.
* **Failed requests are never billed to a customer.** They are left out of `requests` and the
  token counts. Their cost still counts toward `cost_usd`, so your cost reconciles with the
  rest of Analytics.
* Amounts are USD with six decimals, rounded half away from zero once per request. Margin is
  `billable − cost`; margin percent is `margin ÷ billable × 100`, two decimals, and `null` when
  nothing was billable.
* As soon as one rule applies to the period, every report row carries `billable_usd`,
  `margin_usd`, and `margin_pct` next to `cost_usd` (`has_pricing_rules: true`). The `untagged`
  bucket stays cost-only — nothing can be billed to a customer you did not name.

> [!NOTE]
>
> Pricing rules are reporting-only, like the tags themselves. They never change what AI
> Reserve charges your organization, and AI Reserve does not invoice your customers for you —
> the figures are there for you to bill from.

### In the portal

Organization admins manage rules under **Analytics → Customer pricing** (also reachable from
*Set prices* on the Spend by Customer card). The list shows every rule's scope, price, window
and status — *Active*, *Scheduled*, or *Ended*. *Add rule* asks who the price is for, then how
you charge, and reads the rule back in one sentence before saving. *Change price* starts a new
version and ends the current one where it begins; *End* stops a rule from now, or removes one
that has not started yet.

Once one rule covers the selected range, the **Spend by Customer** card adds **Revenue** and
**Margin** (amount and percent) to every customer row, to each organization under it, and in a
totals line; a *By feature* view breaks the same figures down by `x-aireserve-feature` tag. A
negative margin is labelled *Loss*, a gross margin under 20% *Low margin*, usage that no rule
covers *No rule* (shown at cost, with a link to add one), and a group where only some usage was
priced *Partly priced*. The card's CSV gains `costUsd`, `revenueUsd`, `marginUsd`, `marginPct`
and `priced` columns. The figures are the ones `GET /v1/customers/usage` returns for the same
range: the same engine prices both, so the portal and the API never disagree. Margin is an
organization-wide figure — it is computed for the whole organization, not for a team or member
filter — and, like the API, for ranges of up to 92 days.

## Check the amount (API)

Two read endpoints return usage, cost, and — with pricing rules — billable amount and margin per
customer, so your own billing or support tooling can answer "what does this customer owe?".
Both take your AI Reserve API key and are scoped to the organization that owns it.

| Endpoint | Returns |
| - | - |
| `GET /v1/customers/usage` | Every customer in the period, plus an `untagged` bucket and totals |
| `GET /v1/customers/{customerId}/usage` | One customer, broken down by feature (`group_by=feature`, the default) or by UTC day (`group_by=day`), plus any monthly flat fees booked |

`start` is inclusive and `end` exclusive; both accept a UTC date (`2026-10-01`) or an RFC 3339
timestamp. Leave them out for the last 30 days. A period may span at most 92 days — longer
ranges and malformed dates return `400`. The customer id in the path is URL-encoded, exactly as
you tagged it; an id you never tagged returns zeros, not `404`.

```bash curl theme={"dark"}
# Every customer, last 30 days
curl "https://api.aireserve.com/v1/customers/usage" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY"

# One customer, September, by feature
curl "https://api.aireserve.com/v1/customers/cust_8f3a2b/usage?start=2026-09-01&end=2026-10-01" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY"
```

```python Python theme={"dark"}
import os, requests
from urllib.parse import quote

headers = {"Authorization": f"Bearer {os.environ['AIRESERVE_API_KEY']}"}

# Every customer, last 30 days
report = requests.get("https://api.aireserve.com/v1/customers/usage", headers=headers).json()
for row in report["customers"]:
    print(row["customer_id"], "cost", row["cost_usd"], "owes", row.get("billable_usd"))

# One customer, September, by feature
customer_id = "cust_8f3a2b"
statement = requests.get(
    f"https://api.aireserve.com/v1/customers/{quote(customer_id, safe='')}/usage",
    params={"start": "2026-09-01", "end": "2026-10-01", "group_by": "feature"},
    headers=headers,
).json()
print(statement["totals"]["billable_usd"])
```

```typescript TypeScript theme={"dark"}
const headers = { Authorization: `Bearer ${process.env.AIRESERVE_API_KEY}` };

// Every customer, last 30 days
const report = await fetch("https://api.aireserve.com/v1/customers/usage", { headers }).then((r) => r.json());
for (const row of report.customers) {
  console.log(row.customer_id, "cost", row.cost_usd, "owes", row.billable_usd);
}

// One customer, September, by feature
const customerId = "cust_8f3a2b";
const params = new URLSearchParams({ start: "2026-09-01", end: "2026-10-01", group_by: "feature" });
const statement = await fetch(
  `https://api.aireserve.com/v1/customers/${encodeURIComponent(customerId)}/usage?${params}`,
  { headers },
).then((r) => r.json());
console.log(statement.totals.billable_usd);
```

A per-customer response for an organization with two rules — a 20% markup on this customer's
`draft-email` feature, and a \$99 monthly flat fee for everything else the customer does:

```json theme={"dark"}
{
  "customer_id": "cust_8f3a2b",
  "period": { "start": "2026-09-01T00:00:00.000Z", "end": "2026-10-01T00:00:00.000Z" },
  "currency": "USD",
  "has_pricing_rules": true,
  "truncated": false,
  "group_by": "feature",
  "groups": [
    {
      "feature": "draft-email",
      "requests": 1240,
      "prompt_tokens": 2810400,
      "completion_tokens": 611200,
      "cost_usd": "18.421300",
      "billable_usd": "22.105560",
      "margin_usd": "3.684260",
      "margin_pct": 16.67,
      "priced": true
    },
    {
      "feature": null,
      "requests": 35,
      "prompt_tokens": 40100,
      "completion_tokens": 9800,
      "cost_usd": "0.312400",
      "billable_usd": "99.000000",
      "margin_usd": "98.687600",
      "margin_pct": 99.68,
      "priced": true
    }
  ],
  "totals": {
    "requests": 1275,
    "prompt_tokens": 2850500,
    "completion_tokens": 621000,
    "cost_usd": "18.733700",
    "billable_usd": "121.105560",
    "margin_usd": "102.371860",
    "margin_pct": 84.53,
    "priced": true
  },
  "monthly_flat_charges": [{ "month": "2026-09", "day": "2026-09-01", "amount_usd": "99.000000" }]
}
```

The flat fee is booked on the customer's first matched usage of the month and included in that
row's `billable_usd`; requests under a monthly flat rule add nothing per request. Had a feature
matched no rule at all, its row would show `billable_usd` equal to `cost_usd` and
`priced: false`.

| Field | Meaning |
| - | - |
| `cost_usd` | What the usage cost your organization — your billed AI Reserve cost. A six-decimal USD string, never a float. |
| `billable_usd` | What the customer owes under your price list. Present only when `has_pricing_rules` is `true`. |
| `margin_usd`, `margin_pct` | `billable − cost`, and that as a percentage of `billable` (two decimals, `null` when nothing was billable). Present only with pricing rules. |
| `priced` | `true` when every request in the row matched a rule. `false` when some usage was counted at cost. |
| `has_pricing_rules` | Whether any rule of yours applies to the period. When `false`, the billable and margin fields are omitted everywhere — there is no price list to apply. |
| `untagged` | Org-wide endpoint only: requests that carried no customer id, cost only, so the report reconciles with your period total. |
| `monthly_flat_charges` | Per-customer endpoint only: each monthly fee booked in the period. Already included in `billable_usd`. |
| `truncated` | `true` when the period held more usage rows than one response carries. Narrow the period or query one customer. |

Both endpoints share your organization's API rate limit and return the standard
[error shape](/api-reference/errors) — `401` for a missing key, `403` when a screen rejects
it, `429` with `Retry-After`. Full request and response schemas are under *Customers* in the
[API reference](/api-reference).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.