Cost Tracking
What the balance is, what one request cost, and what the same call costs elsewhere.
Four places report what you spent, in the order they become available: usage.cost on the response body, the X-Routehook-Cost header beside it, GET /v1/generation for one request after the fact, and GET /v1/credits for the account. They agree, because they are one ledger read from four ends.
usage.cost, on the response you already have
Chat responses carry usage.cost as an OpenRouter-compatible JSON number and usage.cost_decimal as the exact decimal string. A streamed response puts the same fields in the final usage frame only when stream_options.include_usage is true; otherwise look the request up afterwards.
Headers on every gateway response
| HEADER | VALUE |
|---|---|
| X-Routehook-Request-Id | req_…. The id to look the request up by, and the id to quote. |
| X-Routehook-Cost | USD charged, as a decimal string. |
| X-Routehook-Attempts | How many targets were tried. |
| X-Routehook-Fallback | true when the target that answered was not the first one. |
| X-Routehook-Upstream-Latency-Ms | How long the upstream leg took, excluding our own overhead. |
A streamed response has no cost header. Headers go out before the first token and the charge is not known until after the last one, so a stream's total arrives in the final usage frame or from GET /v1/generation. The request id is set before the request is routed, which is why it is on every response including the failures.
GET/v1/generationAvailable
Read what one request cost and how long it took.
GET /v1/generation?id=req_… is the record of one request: usage is what you were charged, attempts and fallback_used say whether the first attempt worked, generation_time and latency are milliseconds, and status with error_code covers the requests that failed. It is scoped to the calling account, another account's id is a 404, not a 403, because a 403 would confirm the id exists.
reference_value is the savings number
reference_value is what the same call would have cost at the vendor's own list price. The charge and the reference sitting on one row is the saving, measured per request rather than asserted per month: subtract usage from reference_value and the difference is the number, for that call, with the tokens it was computed from beside it. It reads 0 where no list price has been recorded for that model, so treat 0 as unknown rather than as no saving.
One call, then its cost
# 1: make the call, keeping the response headers.
curl -sS https://api.routehook.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
-H "Content-Type: application/json" \
-D headers.txt \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "Hello" }]
}' | jq '.usage'
# 2: the id it was recorded under.
REQUEST_ID=$(awk 'tolower($1) == "x-routehook-request-id:" { print $2 }' \
headers.txt | tr -d '\r')
# 3: the same request, read back.
curl -sS "https://api.routehook.ai/v1/generation?id=$REQUEST_ID" \
-H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
| jq '.data | {usage, reference_value, attempts, fallback_used}'
GET/v1/creditsAvailable
Read the account balance, the amount held by requests in flight and what is left to spend.
GET /v1/credits describes the account, not one key. total_credits is lifetime deposits and total_usage lifetime spend, both OpenRouter's names; balance is what is settled, held is what requests in flight have reserved, and available is balance minus held plus any credit limit. available is the figure to check before starting a batch. Balance on its own will happily tell you that you can afford work you have already committed to. These are numbers rounded to two places, not strings: per-token rates are strings, balances are not.
Why held exists
Money is reserved before it is spent and settled at the real cost afterwards. A request holds the most it could possibly cost, the actual charge replaces that hold when the response is delivered, and the hold is released if the request fails. So held is non-zero while work is in flight, a 5xx costs nothing, and a queue job's money is committed at submit rather than at completion, which is the answer to why the balance moved before a video existed.
GET/v1/keyAvailable
Describe the key making the call: its label, spend cap, lifetime usage and rate limit.
GET /v1/key describes the key that made the call: usage is lifetime USD spent on that key, limit and limit_remaining are its own spend cap where one is set, and rate_limit is the requests-per-interval it is subject to. Spend is attributed per key, so one key per project turns the account balance into a per-project bill with no tagging of your own.
A workable setup
- One key per project or environment, so usage on GET /v1/key is already the breakdown.
- Log X-Routehook-Request-Id on every call, successful or not.
- Log X-Routehook-Cost beside it, and X-Routehook-Fallback to catch the days something behind a model is struggling.
- Reconcile against GET /v1/generation when a number looks wrong, rather than re-adding your own logs.
- Poll GET /v1/credits on a schedule, and alert on available rather than balance.
Quote the request id
Every response carries the request id on X-Routehook-Request-Id (the same header on every route, images included), and every error body repeats it inside the error object. A support report carrying that id is a lookup. The attempts, the underlying error and the charge are all on one row. Without it, the same report is an investigation.