# Costs & billing

Every call is billed at your plan's price, and every response tells you
exactly what it is charged. What a call costs depends on the route that
answered it: **standard routes** (the shared cache, Cormorant's own search
engines, a direct request, a headless browser, archives) or **premium routes**
(the search fallback service, the residential network and the rendering
service), which are priced higher.

## What a call costs

| Action | Route | Billing category | Control it with |
|---|---|---|---|
| Search answered by SearXNG | standard | `search` | — |
| Search that falls back to Brave | premium (fallback search service, billed on its cost) | `search_fallback` | fewer, better queries; the cache |
| Search answered from the cache | standard | `search_cached` | — |
| Fetch with `direct`, `browser` or `archive` | standard | `fetch_own` | — |
| Fetch with `proxy` | premium (residential network, also per GB transferred) | `fetch_premium` | `allow_premium: false` |
| Fetch with `proxy_render` | premium (rendering service, per successful call × the site's difficulty) | `fetch_premium` | `allow_premium: false`, `tiers` |
| Fetch answered from the cache | standard, even if the page first came from a premium route | `fetch_cached` | — |

The premium routes are used **only when the standard routes were blocked** —
never for a page that is merely short, never for a definitive answer such as
a 404. In batches, an item blocked on the standard routes is retried once
after a cool-down before a premium route is tried. Your plan sets the price
of each category (and any included units).

## Seeing the cost

- Every billable answer — search, fetch, batch items, `/v1/sec/…`,
  `/v1/companies/…` — has a `billing` block: what *this*
  request is charged under your plan (see below). `billing_category` says which
  category of your plan it counts toward.
- `cost_usd` is the provider spend caused by the request (cache hits and
  standard routes report `0`) — `null` for keys of a customer account: customers see
  what they are charged, never what Cormorant pays.
- `tier` and `attempts` show which tier answered and what was tried before.
- `GET /v1/usage` sums it up per project.

### The `billing` block

```json
"billing": {
  "category": "fetch_premium",
  "billable": true,
  "currency": "CHF",
  "charge": "0.014000",
  "within_allowance": false,
  "units": [
    {"meter": "fetch_premium", "unit": "fetch", "quantity": "1", "included": "0", "charge": "0.004000"},
    {"meter": "premium_gb", "unit": null, "quantity": null, "included": null, "charge": "0.010000"}
  ]
}
```

- `charge` — this request at your plan price (decimal string, 6 places, in
  your invoice currency): the plan's volume tiers at your account's position
  in the month, its included units, contract rates and discount. `"0.000000"`
  inside the allowance.
- `within_allowance` — `true` when every unit fell inside your plan's
  included units; `null` when nothing was priced (a failed request your plan
  does not bill, `billable: false`, or a category your plan does not charge).
- `units` — per plan meter: units added, how many were included, their charge.
  Meters billed on provider cost show only their charge.
- Not included: the monthly base fee, the minimum top-up and VAT (account
  lines of the invoice). The invoice rates the whole month at once and rounds
  each line to the cent, so it is authoritative; summing `charge` over a month
  matches it up to rounding.
- Projects without a customer plan (internal use) get `currency` and `charge`
  `null`, and see `cost_usd`.

## Keeping it bounded

```json
{"url": "https://www.example.com/page", "allow_premium": false}
```

`allow_premium: false` removes the premium routes from the ladder, so the fetch is
always billed at a standard-route price; a site that blocks every standard
route then answers
`all_tiers_failed`. `tiers` gives finer control, for example
`["direct", "browser", "proxy"]` to allow the proxy but not the rendering tier (`proxy_render`).

### Capping the cost of a run

A run (a research job, an agent session) can enforce its own cost cap from
the answers alone:

1. Before the run, read `GET /v1/quota` → `spend_budget`: your key's `cap`,
   `spent` and `remaining` this month (`cap: null` means no cap is set on the
   key — ask the operator for one, see below).
2. Keep a running total of `billing.charge` over the run's answers (batch
   items carry it too). Stop, or switch to `allow_premium: false`, when the
   total reaches the run's cap.
3. Budget for the worst case before expensive steps: a premium-route fetch is at least
   the plan's `fetch_premium` price, a search fallback is billed on provider
   cost. Answers with `within_allowance: true` are inside your plan's included units.

```python
RUN_CAP = Decimal("5.00")  # in your invoice currency
spent = Decimal("0")
for url in urls:
    page = cormorant.post("/v1/fetch", json={"url": url, "allow_premium": spent < RUN_CAP}).json()
    spent += Decimal(page["billing"]["charge"] or "0")
```

A client-side cap is a convenience, not a guarantee (several runs share the
key, a crash loses the total). **Ask the operator to set a monthly spend cap
on the key** (Mission Control → Customers → Keys): Cormorant enforces it on
every request and batch item (`429 key_budget_exhausted`), shows it in
`X-Cormorant-Key-Budget-Remaining` and `spend_budget`, and can alert you at
50 / 80 / 100 % (`POST /v1/alerts`). Use one key per workload so each gets
its own cap.

## Budgets

Each project has a **monthly budget** in USD for third-party providers
(calendar month, UTC), set by the operator. Before every provider call
Cormorant reserves the
estimated cost atomically, so concurrent requests can never overspend; after
the call the reservation is settled to the actual cost. When the budget is
exhausted, premium routes and the Brave fallback are skipped (`budget_exhausted`
in `attempts` or in a search's `error`) while the standard routes keep working.

## Usage: GET /v1/usage

Usage of the caller's scope and what it costs. Inclusive dates (UTC), default:
current month to date, at most 400 days. Read from aggregates refreshed every
5 minutes, never from the request log, so it answers in the same time at any
volume.

**Who sees what.** A standard key (`cmr_…`) sees only its own requests. An
account admin key (`cma_…`) sees every key and project of its customer —
revoked keys included, and usage recorded before per-key attribution as
`"key": null`. A project without a customer is its own account. Nothing ever
crosses a customer boundary; a key or project you cannot see is `404`, like
one that does not exist.

**Parameters**

| Parameter | Meaning |
|---|---|
| `from`, `to` | Range of UTC days (inclusive). |
| `group_by` | Comma-separated, combinable: `key`, `project`, `category`, `day`, `hour` (hourly data: last 35 days). |
| `key` | Only these keys (comma-separated prefixes, e.g. `cmr_AbCdEf`). |
| `project` | Only these projects (comma-separated names). |
| `category` | Only these billing categories: `search`, `search_fallback`, `search_cached`, `fetch_own`, `fetch_premium`, `fetch_cached`, `company`, `company_cached`, `sec`, `sec_cached`, `company_profile`, `company_profile_cached`. |
| `format` | `csv` (same as `Accept: text/csv`). |

```json
{
  "project": "acme-crawler", "key": "cma_XyZaBc", "role": "account_admin",
  "scope": {"customer": "Acme", "projects": ["acme-crawler", "acme-search"], "keys": "all"},
  "from": "2026-10-01", "to": "2026-10-19",
  "as_of": "2026-10-19T11:05:02+00:00", "final": false, "currency": "USD",
  "totals": {"requests": 9120, "succeeded": 8800, "failed": 320, "success_rate": 0.9649,
             "cached": 1200, "cache_hit_rate": 0.1316,
             "errors": {"http_404": 210, "all_tiers_failed": 110},
             "latency_ms_p50": 900, "latency_ms_p95": 7400, "bytes": 812345678,
             "premium_requests": 90, "premium_share": 0.0113, "charge": "15.96",
             "billable": {"fetch_own": {"quantity": "8710", "included": "1000",
                                        "billable": "7710", "amount": "15.42"},
                          "fetch_premium": {"quantity": "90", "included": "0",
                                         "billable": "90", "amount": "0.36"},
                          "premium_gb": {"quantity": null, "included": null,
                                      "billable": null, "amount": "0.18"}}},
  "search": {"requests": 1200, "cached": 300, "cache_hit_rate": 0.25, "succeeded": 1198,
             "success_rate": 0.998, "bytes": 0, "premium_bytes": 0,
             "latency_ms_p50": 1100, "latency_ms_p95": 3900, "fallbacks": 60,
             "fallback_rate": 0.0667, "providers": {"searxng": {"requests": 840, ...}, "brave": {...}}},
  "fetch": {"requests": 7920, "cached": 900, "tiers": {"direct": {...}, "proxy": {...}},
            "errors": {"http_404": 210},
            "by_outcome": {"succeeded": {"requests": 7600, "bytes": 800000000,
                                         "premium_bytes": 8800000, "premium_requests": 80},
                           "failed": {"requests": 320, "bytes": 0,
                                      "premium_bytes": 323456, "premium_requests": 10}}, ...},
  "company_profile": {},  // extended company profiles (kind company_profile), same fields; {} when none
  "group_by": ["key"],
  "groups": [{"key": "cmr_AbCdEf", "key_label": "crawler", "key_role": "standard",
              "key_status": "active", "project": "acme-crawler", "requests": 9000, ...,
              "charge": "15.70", "billable": {...}}],
  "keys": [{"prefix": "cmr_AbCdEf", "label": "crawler", "role": "standard",
            "project": "acme-crawler", "active": true, "requests": 9000, "cached": 1150,
            "succeeded": 8690, "search_requests": 1100, "fetch_requests": 7900, "company_requests": 0,
            "company_profile_requests": 0,
            "premium_bytes": 9123456, "charge": "15.70"}],
  "account": [{"period": "2026-10", "from": "2026-10-01", "to": "2026-10-19", "currency": "USD",
               "complimentary": true, "usage_charges": "15.96", "account_level_amount": "-15.96",
               "account_lines": [{"kind": "complimentary", "description": "Complimentary plan: 100 % discount",
                                  "amount": "-15.96"}],
               "list_total": "15.96", "net_total": "0.00"}]
}
```

- **Metrics** (in `totals` and every group): `requests` (cache hits
  included), `succeeded`, `failed`, `cached`, `errors` (failures by error
  code, top 50), `latency_ms_p50`/`latency_ms_p95` of uncached requests
  (estimated from a latency histogram with bounds 100, 250, 500 ms, 1, 2, 5,
  10, 20, 30, 60 and 120 s — exact to the bucket), `bytes`, `premium_requests`
  and `premium_share` (fetches that needed the premium routes, of all
  uncached fetches).
- **Charges** (projects with a customer): `charge` is what the group costs at
  your plan's prices in your invoice currency, as a decimal string; `billable`
  lists each price-plan meter with the measured `quantity`, the `included`
  units it used (its pro-rata share of the plan's included units of the
  month), `billable` and `amount`. Meters billed on provider cost show only the
  amount. The period is rated exactly like the invoice and each usage line is
  split to the cent, first per key, then per day and category: **any grouping
  adds up to the same total**, and the keys add up to the usage lines of the
  estimate (`GET /v1/account`) or invoice. Base fee, minimum, contract
  discount and the complimentary credit are account-level lines: account admin
  keys see them per period in `account`. Complimentary accounts see list-price
  charges and the 100 % `complimentary` line.
- **Provider cost** — `cost_usd` and the project `budget` (Cormorant's spend
  with its search and proxy providers) appear only for projects without an external
  customer (internal use). Customers see what they pay, never what Cormorant
  pays.
- **Freshness**: `as_of` is the time up to which every request is included;
  `final: true` (top level, and per group with `group_by=day`) means the days
  are closed and will not change (a day becomes final two hours after it
  ends).
- `search` and `fetch` keep the per-kind blocks of earlier versions (now for
  the caller's scope), with `providers`/`tiers` breakdowns. With `key` in
  `group_by`, `keys` keeps the earlier per-key list as well.
- **Latency components**: every per-kind block has `latency_components`
  (`queue_ms`, `politeness_wait_ms`, `upstream_ms`, each `{p50, p95}` of
  uncached requests), and each entry of `tiers`/`providers` adds
  `latency_ms_p50/p95` and `<component>_p50/_p95`, so a slow tier can be told
  apart from a busy domain or a queue backlog. Estimated from histograms like
  the other percentiles; requests from before these were recorded are not
  included.
- `by_outcome` (in every per-kind block) splits the same requests into
  `succeeded` and `failed`, each with `requests`, `bytes`, `premium_bytes`,
  `premium_requests` (fetch) and `cost_usd` (provider cost, internal projects
  only): `fetch.by_outcome.failed.premium_bytes` is what blocked pages cost
  before the fetch gave up. Whether failed requests are billed depends on
  your plan (`billable`).

**CSV.** `Accept: text/csv` (or `format=csv`) returns one row per group — or
one row of totals without `group_by` — with the dimensions, the metrics,
`charge`, the meters as `<meter>_quantity`, `<meter>_included`,
`<meter>_billable`, `<meter>_amount` and `errors` as `code=n;code=n`.

```sh
curl -sS "$CORMORANT_URL/v1/usage?from=2026-10-01&to=2026-10-31&group_by=key,day" \
  -H "Authorization: Bearer $CORMORANT_API_KEY" -H "Accept: text/csv" -o usage.csv
```
```sh
curl -sS "https://api.cormorant.ch/v1/usage?from=2026-10-01&to=2026-10-19" \
  -H "Authorization: Bearer $CORMORANT_API_KEY"
```

```python
usage = client.get("/v1/usage", params={"from": "2026-10-01", "to": "2026-10-19"}).json()
if usage["currency"]:  # a customer account: charges at your plan price
    print(f"charges {usage['totals']['charge']} {usage['currency']} (as of {usage['as_of']})")
else:  # a project without a customer: provider spend and budget
    print(f"spent ${usage['cost_usd']:.2f}, budget left ${usage['budget']['remaining_usd']:.2f}")
print("fetch cache hit rate:", usage["fetch"].get("cache_hit_rate"))
```

```ts
const usage = await (
  await fetch(`${BASE}/v1/usage?from=2026-10-01&to=2026-10-19`, { headers })
).json();
console.log(usage.currency ? `${usage.totals.charge} ${usage.currency}` : `$${usage.cost_usd}`);
```

## Your account, plan and invoices

Two different things are measured:

- **Provider spend** (`cost_usd` on results, budgets) — what Cormorant spent
  on its search and proxy providers because of your requests. `GET /v1/usage` shows it only
  for projects without a customer account.
- **What you are billed** — a project can belong to a **customer** account
  with a price plan. Every served request falls into a billing category
  (`search`, `search_fallback`, `search_cached`, `fetch_own`, `fetch_premium`,
  `fetch_cached`, `company`, `company_cached`, `sec`, `sec_cached`,
  `company_profile`, `company_profile_cached`), is metered hourly and priced
  monthly by the plan (included
  units, volume tiers, pass-through of provider cost). Complimentary accounts
  get the same invoice at list price with a 100 % discount line, so you can
  see what your usage is worth.

Keys belong to a project; a project may belong to a **customer** (the billed
account). These endpoints show the key's own project usage plus the plan,
current-period estimate and issued invoices of that customer — never another
customer's data, never drafts. Money is a decimal **string** in the invoice
currency (USD, CHF or EUR). Usage is aggregated every 5 minutes (`usage_refreshed_at`).
See [billing.md](/docs/billing.md) for pricing rules.

### GET /v1/account

```json
{
  "project": {"id": 7, "name": "acme-crawler"},
  "period": {"period": "2026-10", "from": "2026-10-01", "to": "2026-10-31"},
  "usage_refreshed_at": "2026-10-19T11:00:04+00:00",
  "project_usage": {"fetch_own": {"requests": 9120, "succeeded": 8800, "bytes": 812345678,
                    "bytes_ok": 800000000, "premium_bytes": 0, "premium_bytes_ok": 0,
                    "cost_usd": "0", "cost_ok_usd": "0"}, "search": {...}},
  // cost_usd / cost_ok_usd: null for projects of an external customer
  "customer": {"name": "Acme", "company_name": "Acme Research AG", "currency": "USD",
               "status": "active", "complimentary": true},
  "customer_usage": {...},
  "plan": {"id": 1, "code": "standard-usd", "version": 1, "name": "Standard (USD)",
           "currency": "USD", "complimentary": false},
  "estimate": {"period": "2026-10", "currency": "USD",
               "lines": [{"kind": "usage", "component": "fetch_own",
                          "description": "Page fetches (1,000 included)", "quantity": "8800",
                          "unit": "fetch", "unit_price": null, "amount": "15.60",
                          "detail": {"included": "1000", "billable": "7800",
                                     "tiers": [{"from": "0", "to": "100000", "quantity": "7800",
                                                "unit_price": "0.002", "amount": "15.60"}]}},
                         {"kind": "complimentary", "description": "Complimentary plan: 100 % discount", "amount": "-15.60", ...}],
               "subtotal": "15.60", "discount_total": "15.60", "net_total": "0.00",
               "vat_rate": "8.1", "vat_amount": "0.00", "total": "0.00", "complimentary": true,
               "list_total": "15.60"}
}
```

`keys` breaks the period down per API key. An account admin key (`cma_…`)
sees every key of the customer's projects (or of this project without a
customer); an additional key (`cmr_…`) sees only itself. Keys appear by
`prefix`, `label` and `role`, never ids or secrets, with its usage by category and `amount`, its share of
the usage charges at list price. Each usage line of the estimate is split
between keys in proportion to their share of that meter (to the cent), so the
amounts add up to the usage lines; base fee, minimum, discounts and the
complimentary credit stay on the account. A row with `"prefix": null` is usage recorded
before per-key attribution.

```json
"keys": [{"prefix": "cma_XyZaBc", "label": "billing", "role": "account_admin", "project": "acme-crawler",
          "active": true, "requests": 9120, "succeeded": 8800,
          "usage": {"fetch_own": {"requests": 9120, ...}}, "amount": "15.60"}]
```

Projects without a customer get `"customer": null, "plan": null, "estimate": null`
(and `amount: null` per key).
Categories: `search`, `search_fallback`, `search_cached`, `fetch_own`,
`fetch_premium`, `fetch_cached`, `company`, `company_cached`, `sec`, `sec_cached`,
`company_profile`, `company_profile_cached`.

### GET /v1/account/invoices

`{"invoices": [{"number": "CORM-2026-0001", "status": "issued|paid|void", "period": "2026-09",
"period_start", "period_end", "currency", "plan_label", "complimentary", "subtotal", "discount_total",
"net_total", "vat_rate", "vat_amount", "total", "vat_note", "issued_at", "due_at", "paid_at",
"voided_at", "pdf_url": "/v1/account/invoices/CORM-2026-0001/pdf"}]}`

### GET /v1/account/invoices/{number} and /pdf

The invoice with its lines (and tier breakdown), or the PDF. Another
customer's number (or a draft) is `404`.
```sh
curl -sS https://api.cormorant.ch/v1/account \
  -H "Authorization: Bearer $CORMORANT_API_KEY"
curl -sS https://api.cormorant.ch/v1/account/invoices/CORM-2026-0001/pdf \
  -H "Authorization: Bearer $CORMORANT_API_KEY" -o CORM-2026-0001.pdf
```

```python
account = client.get("/v1/account").json()
estimate = account["estimate"]
if estimate:
    print(f"{estimate['period']}: {estimate['total']} {estimate['currency']}")
for invoice in client.get("/v1/account/invoices").json()["invoices"]:
    print(invoice["number"], invoice["status"], invoice["total"], invoice["currency"])
```

```ts
const account = await (await fetch(`${BASE}/v1/account`, { headers })).json();
if (account.estimate) console.log(account.estimate.period, account.estimate.total);
const { invoices } = await (await fetch(`${BASE}/v1/account/invoices`, { headers })).json();
for (const invoice of invoices) console.log(invoice.number, invoice.status, invoice.total);
```

Amounts are decimal **strings** (`"15.60"`): parse them with a decimal type,
not a float. If the account is suspended, every `/v1` request answers
`403 customer_suspended`.

## Attributing spend

Provider spend is attributed only to the project whose request made the
provider call; the caches are shared, so a page another project fetched
through a premium route is a cache hit for you. Within
an account, every request is also attributed to the **API key** that made it
(batch items to the key that created the job): `GET /v1/usage?group_by=key`
splits usage and charges per key, and `keys` in `GET /v1/account` shows
each key's usage and share of the month's charges. With the account admin key
(`cma_…`) you see every key of the account; an additional key (`cmr_…`) sees
only itself. Give each client or
environment its own key (ask the operator for a labelled key) to see who uses
what.

```python
for group in client.get("/v1/usage", params={"group_by": "key"}).json()["groups"]:
    print(group["key"], group["key_role"], group["requests"], group.get("charge"))
``` Set
`project_ref` on requests to tag usage with your own identifiers (a customer,
a company id, a run).
