Public Billing API
The Public Billing API returns spending for the authenticated API key by default. Keys owned by organization administrators can also query spending for their organization. The response includes spending before and after discounts for the range, each day, and each model on each date, plus categorized token usage.
Endpoint
GET https://console.exchangetoken.ai/api/public/v1/billingResponses use application/json. Dates are sorted in ascending order; models within each date are sorted by model identifier. Results are aggregated and do not include individual request records.
Authentication
Use the same API key used for model requests in the Authorization header:
Authorization: Bearer sk-YOUR_API_KEYOmitting scope or setting scope=key queries only the authenticated key's spending, including when the key belongs to an organization administrator. With scope=org, only keys owned by organization administrators (admin/root) can query spending across all keys in their organization. Keys owned by ordinary members receive 403 forbidden.
Permissions use the key owner's current account role. The server determines the organization from that account. Parameters such as token_id, user_id, or org_id that select a query target are not accepted. A console login session is not required. API keys must not be included in query parameters.
Exhausted quotas do not prevent billing queries. Expired, disabled, or deleted keys, disabled accounts, and requests that violate the key's IP restrictions cannot access this endpoint.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Inclusive start date in YYYY-MM-DD format. |
to | string | Yes | Exclusive end date in YYYY-MM-DD format. |
timezone | string | No | IANA time zone for date boundaries and daily grouping. Defaults to UTC. Examples: Asia/Shanghai, America/New_York. |
scope | string | No | Query scope: key for the authenticated key, or org for its organization. Defaults to key when omitted. org requires a key owned by an organization administrator. |
The interval is [from, to). The start date must precede the end date, and the interval must contain no more than 31 calendar days. Supported years range from 1970 to 2299. Duplicate parameters, unsupported parameters, invalid time zones, and invalid scope values (including empty values) return 400.
For example, from=2026-09-01&to=2026-09-08 selects September 1 through September 7 in the requested time zone. To query one day, set to to the following date. Daylight saving time does not change the 31-calendar-day limit. Split longer ranges into adjacent, non-overlapping intervals.
Response fields
| Field | Type | Description |
|---|---|---|
from | string | Inclusive start date. |
to | string | Exclusive end date. |
timezone | string | Time zone used for the query. |
currency | string | Currency, always USD. |
total_amount | string | Total spending before discounts for the interval, with 9 decimal places. |
total_discounted_amount | string | Total spending after discounts for the interval, with 9 decimal places. |
data | array | Daily aggregates. |
data[].date | string | Date in YYYY-MM-DD format. |
data[].total_amount | string | Daily spending before discounts, with 9 decimal places. |
data[].total_discounted_amount | string | Daily spending after discounts, with 9 decimal places. |
data[].models | array | Model aggregates for the date. |
data[].models[].model | string | Model identifier, consistent with the console billing center. |
data[].models[].amount | string | Spending before discounts for this model and date, with 9 decimal places. |
data[].models[].discounted_amount | string | Spending after discounts for this model and date, with 9 decimal places. |
data[].models[].usage | object | Categorized token usage for this model and date. |
All usage fields are non-negative integers:
| Field | Description |
|---|---|
input_tokens | Tokens billed as regular input, excluding tokens assigned to cache reads or cache writes. |
cache_read_tokens | Tokens billed as cache reads. |
cache_write_tokens | Total tokens billed as cache writes, across all cache durations. |
output_tokens | Tokens billed as output. |
reasoning_tokens | Tokens billed separately as reasoning. |
amount and total_amount represent the original charges before discounts. discounted_amount and total_discounted_amount sum the downstream charges after discounts recorded in the bill; historical amounts are not recalculated using current discount settings. Without a discount, the before- and after-discount amounts are equal. Fully waived charges have an after-discount amount of 0.000000000.
Token usage follows billing categories. Reasoning tokens already included in output billing are not counted again in reasoning_tokens; a value of 0 does not imply that the model performed no reasoning. Categories with no usage return 0.
usage is aggregated by billing category without filtering by pricing unit. For models priced in units other than tokens, the returned values may follow the model's metering basis. Clients should use decimal arithmetic for monetary values.
For both before- and after-discount amounts, the range total equals the sum of daily totals, and each daily total equals the sum of its model amounts. Dates without spending return zero amounts and an empty models array.
Example response
The following values illustrate the response structure:
{
"from": "2026-09-01",
"to": "2026-09-03",
"timezone": "UTC",
"currency": "USD",
"total_amount": "1.200000000",
"total_discounted_amount": "0.960000000",
"data": [
{
"date": "2026-09-01",
"total_amount": "1.200000000",
"total_discounted_amount": "0.960000000",
"models": [
{
"model": "example-model",
"amount": "1.200000000",
"discounted_amount": "0.960000000",
"usage": {
"input_tokens": 100000,
"cache_read_tokens": 20000,
"cache_write_tokens": 10000,
"output_tokens": 30000,
"reasoning_tokens": 0
}
}
]
},
{
"date": "2026-09-02",
"total_amount": "0.000000000",
"total_discounted_amount": "0.000000000",
"models": []
}
]
}Examples
cURL
Supply the API key through an environment variable and encode query parameters with --data-urlencode:
export EXCHANGE_TOKEN_API_KEY='sk-YOUR_API_KEY'
curl --fail-with-body --get \
'https://console.exchangetoken.ai/api/public/v1/billing' \
--header "Authorization: Bearer ${EXCHANGE_TOKEN_API_KEY}" \
--data-urlencode 'from=2026-09-01' \
--data-urlencode 'to=2026-09-08' \
--data-urlencode 'timezone=Asia/Shanghai'This example queries the authenticated key by default. To query organization billing with a key owned by an organization administrator, add --data-urlencode 'scope=org'. Response fields and the monetary basis are the same for both scopes.
Python
This example uses the Python standard library and parses monetary values with Decimal:
import json
import os
from decimal import Decimal
from urllib.parse import urlencode
from urllib.request import Request, urlopen
params = urlencode({
"from": "2026-09-01",
"to": "2026-09-08",
"timezone": "UTC",
})
request = Request(
"https://console.exchangetoken.ai/api/public/v1/billing?" + params,
headers={"Authorization": "Bearer " + os.environ["EXCHANGE_TOKEN_API_KEY"]},
)
with urlopen(request, timeout=90) as response:
bill = json.load(response)
print(Decimal(bill["total_amount"]), bill["currency"])
print(Decimal(bill["total_discounted_amount"]), bill["currency"])
for day in bill["data"]:
for item in day["models"]:
print(day["date"], item["model"], Decimal(item["amount"]),
Decimal(item["discounted_amount"]), item["usage"])Errors
Errors use the following JSON structure:
{
"error": {
"type": "invalid_request",
"message": "date range must contain between 1 and 31 calendar days; to is exclusive"
}
}| HTTP status | error.type | Description |
|---|---|---|
400 | invalid_request | Missing dates, invalid dates or range, invalid time zone or scope, duplicate parameters, or unsupported parameters. |
401 | unauthorized | A valid Bearer API key was not supplied. |
403 | forbidden | Key or account status does not permit access, IP restrictions are not satisfied, or the key owner lacks permission to query organization billing. |
404 | not_found | The endpoint does not exist. |
405 | method_not_allowed | The method is unsupported. This endpoint accepts only GET. |
429 | rate_limit_exceeded | The request is temporarily limited. Retry later. |
500 | internal_error | An internal service error occurred. |
503 | service_unavailable / billing_unavailable | The service is busy, or authentication or billing data is temporarily unavailable. |
504 | query_timeout | The query timed out. Retry with a shorter date range. |
Data updates and retries
For 429 responses, wait for the number of seconds specified in the Retry-After header before retrying.
Spending data updates have a short delay. Querying data for the current day is not recommended.
