Costs & billing
Most of what Cormorant does is free to the project: its own search engines, its own fetch tiers and the shared cache. Money is spent only when a paid provider is called — and every response tells you exactly how much.
What costs money#
| Action | Provider | Cost basis | Avoid it with |
|---|---|---|---|
| Search answered by SearXNG | own | free | — |
| Search that falls back to Brave | Brave Search API | per call (operator-configured, about half a cent) | fewer, better queries; the cache |
Fetch with direct, browser or archive |
own / Internet Archive | free | — |
Fetch with proxy |
Massive residential proxy | per GB transferred | allow_paid: false |
Fetch with proxy_render |
Massive Web Render | per successful call × the site's difficulty | allow_paid: false, tiers |
| Any cache hit | — | free, even if the page first came from a paid tier | — |
Paid tiers are used only when the free tiers 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 free tiers is retried once after a cool-down before a paid tier is tried.
Seeing the cost#
- Every search and fetch result has
cost_usd: the provider spend caused by this request (cache hits and free tiers report0) —nullfor keys of a customer account, which see theirchargeper request inGET /v1/requestsinstead.billing_categorysays which category of your plan it counts toward. tierandattemptsshow which tier answered and what was tried before.GET /v1/usagesums it up per project.
Keeping it at zero (or bounded)#
{"url": "https://www.example.com/page", "allow_paid": false}
allow_paid: false removes the paid tiers from the ladder, so the fetch can
never cost anything; a site that blocks every free tier then answers
all_tiers_failed. tiers gives finer control, for example
["direct", "browser", "proxy"] to allow the proxy but not Web Render.
Budgets#
Each project has a monthly budget in USD for paid providers (calendar
month, UTC), set by the operator. Before every paid 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, paid tiers and the Brave fallback are skipped (budget_exhausted
in attempts or in a search's error) while the free tiers 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_paid, fetch_cached, company, company_cached, sec, sec_cached. |
format |
csv (same as Accept: text/csv). |
{
"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,
"paid_requests": 90, "paid_share": 0.0113, "charge": "15.96",
"billable": {"fetch_own": {"quantity": "8710", "included": "1000",
"billable": "7710", "amount": "15.42"},
"fetch_paid": {"quantity": "90", "included": "0",
"billable": "90", "amount": "0.36"},
"paid_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, "paid_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}, ...},
"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,
"paid_bytes": 9123456, "charge": "15.70"}],
"account": [{"period": "2026-10", "from": "2026-10-01", "to": "2026-10-19", "currency": "USD",
"free": true, "usage_charges": "15.96", "account_level_amount": "-15.96",
"account_lines": [{"kind": "free", "description": "Free plan: 100 % discount",
"amount": "-15.96"}],
"list_total": "15.96", "net_total": "0.00"}]
}
- Metrics (in
totalsand every group):requests(cache hits included),succeeded,failed,cached,errors(failures by error code, top 50),latency_ms_p50/latency_ms_p95of 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,paid_requestsandpaid_share(fetches that needed the paid premium tiers, of all uncached fetches). - Charges (projects with a customer):
chargeis what the group costs at your plan's prices in your invoice currency, as a decimal string;billablelists each price-plan meter with the measuredquantity, theincludedunits it used (its pro-rata share of the plan's included units of the month),billableandamount. 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 free-plan credit are account-level lines: account admin keys see them per period inaccount. Free accounts see list-price charges and the 100 %freeline. - Provider cost —
cost_usdand the projectbudget(Cormorant's spend with Brave and Massive) appear only for projects without an external customer (internal use). Customers see what they pay, never what Cormorant pays. - Freshness:
as_ofis the time up to which every request is included;final: true(top level, and per group withgroup_by=day) means the days are closed and will not change (a day becomes final two hours after it ends). searchandfetchkeep the per-kind blocks of earlier versions (now for the caller's scope), withproviders/tiersbreakdowns. Withkeyingroup_by,keyskeeps the earlier per-key list as well.
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.
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
curl -sS "https://api.cormorant.ch/v1/usage?from=2026-10-01&to=2026-10-19" \
-H "Authorization: Bearer $CORMORANT_API_KEY"
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"))
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_usdon results, budgets) — what Cormorant paid Brave and Massive because of your requests.GET /v1/usageshows 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_paid,fetch_cached,sec,sec_cached), is metered hourly and priced monthly by the plan (included units, volume tiers, pass-through of provider cost). Free 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 for pricing rules.
GET /v1/account#
{
"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, "paid_bytes": 0, "paid_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", "free": true},
"customer_usage": {...},
"plan": {"id": 1, "code": "standard-usd", "version": 1, "name": "Standard (USD)",
"currency": "USD", "free": 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": "free", "description": "Free 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", "free": 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 free
credit stay on the account. A row with "prefix": null is usage recorded
before per-key attribution.
"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_paid, fetch_cached, company, company_cached, sec, sec_cached.
GET /v1/account/invoices#
{"invoices": [{"number": "CORM-2026-0001", "status": "issued|paid|void", "period": "2026-09", "period_start", "period_end", "currency", "plan_label", "free", "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.
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
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"])
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#
Spend is charged only to the project whose request made the paid call; the
caches are shared, so a page another project paid for is free 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.
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).