账单 API
将你自己工作区的用量与费用数据拉取到你自己的系统中,无需打开控制台。账单密钥是一种 只读 凭证,作用范围仅限一个工作区 - 它只能读取账单数据,不能做别的事。
账单密钥可以读取其工作区中记录的每一次请求,包括之后已被删除或吊销的密钥,但它不能调用任何模型,也不能创建、修改或删除 API 密钥。 在此之外的任何地方使用它都会返回 401 wrong_key_kind.
创建账单密钥
- 打开 Console → Billing API keys (仅工作区所有者可用)。为其命名,可选择限制到 IP 白名单或设置到期时间,然后复制密钥 - 它以
sk-syn-bill-并且只会显示一次。 - 将其作为 Bearer token 调用下方各端点。 可随时在同一页面吊销它 - 吊销会立即使其失效,且不影响工作区中的任何其他内容。
- 密钥与其创建者绑定:如果该创建者不再是工作区所有者,密钥会停止工作并返回
403 key_owner_not_authorized.
认证
每次调用都需要 Authorization: Bearer sk-syn-bill-... ,并针对下方的 base URL 发起请求。账单密钥只能用于这些端点,而这些端点也只接受账单密钥 - 任何其他组合都会返回 401 wrong_key_kind.
Authorization: Bearer sk-syn-bill-... Base URL: https://synthorai.io/api/v1/billing
作用范围是整个工作区:其中的每一个推理密钥都包含在内,即使已被删除的密钥,其历史记录也仍可查询。 api_key_id 和 model 过滤条件只能缩小结果范围,绝不会扩大;每次查询都以该密钥自身的工作区为界。
每次查询都在只读副本上执行,绝不会用主库。未配置副本的进程会返回 503 replica_unavailable 而不是静默回退到主库。
Token 字段: 遵循 OpenAI 的约定 - prompt_tokens 是总输入 token 数,已经包含下面每一项缓存读取与缓存写入,而 completion_tokens 已经包含 reasoning_tokens。两者都不是在总数之外额外叠加的 - 它们是明细拆分,不是额外的 token。
prompt_tokens 中的缓存读取部分即 cached_tokens;缓存写入部分即 cache_write_tokens,在 /records 的行中会按 TTL 进一步拆分为 cache_write_5m_tokens 和 cache_write_1h_tokens。
金额字段: cost_usd 是实际收取的金额。 byok_list_price_usd 是 BYOK 请求按标价本应花费的金额 - 非 BYOK 请求为 0。不存在 list_price_usd 这个字段。
此处的“错误”指所有已到达 API 并被拒绝的请求 - 包括 402(余额不足)、403、429 等 4xx,以及上游失败。只有我方自己的内部基础设施拒绝会被排除,这与控制台的口径一致。拒绝请求在每台服务器上按 key、状态码、分钟最多记录一次,因此 402、403、429 的次数是下限;成功请求与扣费是完整的。
GET /usage - 汇总用量
GET /usage
按小时或按天预聚合的数据,适合对账和看板。数据来自小时汇总表,并补上自上次汇总以来新写入的请求,因此最近的时间桶不会整整落后一个汇总周期。
| 参数 | 类型 | 说明 |
|---|---|---|
start* | string | 范围起点:RFC 3339 时间戳或类似 2026-09-01 的日期。 |
end | string | 范围终点。默认为当前时间。 |
granularity | string | 分桶粒度:hour 或 day。默认为 day。 |
group_by | string | 用于拆分行的维度:api_key、model,或两者都要(任意子集)。默认为两者都要。 |
api_key_id | integer | 按一个或多个 API 密钥 id 过滤。可重复传入;只会缩小范围,绝不会扩大。 |
model | string | 按一个或多个模型 id 过滤。可重复传入。 |
limit | integer | 每页行数。默认 1000,上限 5000。 |
cursor | string | 不透明的分页游标,取自上一页响应中的 meta.next_cursor。 |
在每一行中,api_key_id 与 api_key_name 只有在 group_by 包含 api_key 时才会出现,model 同理只有在包含 model 时才会出现。当某个桶没有延迟采样时,avg_latency_ms 可能为 null。
示例请求
curl "https://synthorai.io/api/v1/billing/usage?start=2026-09-01&end=2026-09-24&granularity=day" \
-H "Authorization: Bearer sk-syn-bill-..." 示例响应
{
"data": [
{
"bucket_start": "2026-09-23T00:00:00Z",
"bucket_end": "2026-09-24T00:00:00Z",
"api_key_id": 42,
"api_key_name": "prod-backend",
"model": "claude-sonnet-5",
"requests": 1280,
"success_requests": 1265,
"error_requests": 15,
"errors_4xx": 12,
"errors_429": 2,
"errors_5xx": 1,
"prompt_tokens": 512000,
"completion_tokens": 98000,
"cached_tokens": 210000,
"cache_write_tokens": 8000,
"reasoning_tokens": 4000,
"cost_usd": 4.1732,
"byok_list_price_usd": 0,
"byok_requests": 0,
"avg_latency_ms": 812,
"final": true
}
],
"meta": {
"generated_at": "2026-09-24T08:00:00Z",
"data_complete_until": "2026-09-24T06:00:00Z",
"timezone": "UTC",
"currency": "USD",
"next_cursor": null,
"has_more": false
}
} GET /records - 逐请求行
GET /records
每个请求一行,用于增量同步到你自己的数据库。请按 cursor 同步,绝不要按时间同步,并按 record_id 对收到的行去重 - 原因见下方“避免时间间隙”。
| 参数 | 类型 | 说明 |
|---|---|---|
cursor | string | 不透明的同步游标(字符串),取自上一次调用响应中的 meta.next_cursor。cursor 与 start 只能提供一个;它没有范围上限。 |
start | string | 范围起点(RFC 3339 时间戳或日期)。cursor 与 start 只能提供一个;start/end 范围上限为 31 天,而 cursor 没有范围上限。 |
end | string | 范围终点。默认为当前时间。 |
api_key_id | integer | 按一个或多个 API 密钥 id 过滤。可重复传入;只会缩小范围,绝不会扩大。 |
model | string | 按一个或多个模型 id 过滤。可重复传入。 |
limit | integer | 每页行数。默认 500,上限 1000。 |
示例请求
curl "https://synthorai.io/api/v1/billing/records?start=2026-09-24T00:00:00Z&limit=500" \
-H "Authorization: Bearer sk-syn-bill-..." 示例响应
{
"data": [
{
"record_id": "rec_8f2a91c4",
"request_id": "req_9f3c2b1a",
"recorded_at": "2026-09-24T05:58:31Z",
"completed_at": "2026-09-24T05:58:29Z",
"api_key_id": 42,
"api_key_name": "prod-backend",
"model": "claude-sonnet-5",
"status": "success",
"status_code": 200,
"prompt_tokens": 812,
"completion_tokens": 194,
"cached_tokens": 512,
"cache_write_tokens": 64,
"cache_write_5m_tokens": 64,
"cache_write_1h_tokens": 0,
"cost_usd": 0.00412,
"byok_list_price_usd": 0,
"is_byok": false,
"is_stream": true,
"duration_ms": 1340,
"ttft_ms": 210
}
],
"meta": {
"next_cursor": "eyJvIjoiODgxNDA5MCJ9",
"has_more": false,
"window_final": true,
"safe_until": "2026-09-24T05:58:31Z",
"latest_recorded_at": "2026-09-24T05:58:31Z",
"data_complete_until": "2026-09-24T05:58:00Z"
}
} | 参数 | 说明 |
|---|---|
next_cursor | 用于下一次调用的游标。始终存在。 |
has_more | 此时用该游标再次调用是否会返回更多行。 |
window_final | 只有在提供了 end 且它早于或等于 safe_until 时才为 true - 此时你要求的范围才可确保完整。否则不存在或为 false。 |
safe_until | 调用方可信为已定稿的最新 recorded_at;比这更新的行仍在被暂时保留。 |
latest_recorded_at | 所有行(无论是否已定稿)中最新的 recorded_at。 |
data_complete_until | 与 /freshness 返回的值相同 - 按小时汇总表完整到什么时间,用于与 /usage 交叉核对。 |
时间基准: /usage 按 completed_at 分桶(即请求完成的时间)。/records 的 start/end 过滤则按 recorded_at 过滤(即行被写入的时间,在高负载下可能滞后于完成时间)。要将 /records 与 /usage 对齐,请自行按 completed_at 而非 recorded_at 对同步到的行分桶。
GET /records/export - 流式导出
GET /records/export
过滤条件与 /records 相同,以换行分隔的 JSON(application/x-ndjson)流式返回 - 专为大批量补齐历史数据设计,而非分页列表。每个工作区同一时间只能有一个导出流;第二个会收到 429 too_many_concurrent_requests。
示例请求
curl -N "https://synthorai.io/api/v1/billing/records/export?cursor=eyJvIjoiODgxNDAzMiJ9" \
-H "Authorization: Bearer sk-syn-bill-..." 示例响应
{"record_id":"rec_8f2a91c4","request_id":"req_9f3c2b1a","model":"claude-sonnet-5","status":"success","cost_usd":0.00412}
{"record_id":"rec_8f2a91c5","request_id":"req_9f3c2b1b","model":"claude-sonnet-5","status":"success","cost_usd":0.00398}
{"_meta":{"next_cursor":"eyJvIjoiODgxNDA5MSJ9","rows":2,"complete":true,"window_final":true,"generated_at":"2026-09-24T06:00:02Z"}} 该流的最后一行是一个 _meta 对象:
| 参数 | 说明 |
|---|---|
next_cursor | 用于下一次导出调用的游标。 |
rows | 此次流式传输中写入的记录数。 |
complete | false 表示该流在耗尽整个范围之前就已停止 - 请用 cursor=next_cursor 继续。 |
window_final | 与 /records 中含义相同:只有在导出的 end 早于或等于 safe_until 时才为 true。 |
generated_at | 该行的写入时间。 |
stopped_because | complete 为 false 时出现:row_cap、scan_cap(已扫完单次允许的最大 id 范围但结果未满,常见于过滤条件很窄时)、time_cap、query_error 或 window_not_final。 |
GET /summary - 初步统计与异常
GET /summary
总量、按费用排名的模型与密钥,以及该范围的错误率,再加上一个 anomalies 列表(high_error_rate、rate_limited、data_not_final、rollup_delayed),每一项都带有 info 或 warning 的严重级别以及一段易读的说明文字。尚未定稿范围内的数字仍可能变动 - 见下文。
| 参数 | 类型 | 说明 |
|---|---|---|
start | string | 范围起点。默认为终点前 30 天。 |
end | string | 范围终点。默认为当前时间。 |
api_key_id | integer | 按一个或多个 API 密钥 id 过滤。可重复传入;只会缩小范围,绝不会扩大。 |
model | string | 按一个或多个模型 id 过滤。可重复传入。 |
models_count 与 keys_count 给出 top_models 与 top_keys 背后的总数,而这两个列表最多列出按费用排名前 20 的项目。范围默认为 end 之前的 30 天,用于 start 被省略的情况。
示例请求
curl "https://synthorai.io/api/v1/billing/summary?start=2026-09-17&end=2026-09-24" \
-H "Authorization: Bearer sk-syn-bill-..." 示例响应
{
"data": {
"totals": { "requests": 48210, "cost_usd": 132.55, "error_rate": 0.021 },
"models_count": 6,
"keys_count": 3,
"top_models": [ { "model": "claude-sonnet-5", "cost_usd": 88.10, "byok_list_price_usd": 0 } ],
"top_keys": [ { "api_key_id": 42, "api_key_name": "prod-backend", "cost_usd": 61.30, "byok_list_price_usd": 0 } ],
"anomalies": [
{ "type": "data_not_final", "severity": "info", "message": "The last 2 hours of this range are not yet final." }
]
},
"meta": {
"generated_at": "2026-09-24T06:00:02Z",
"data_complete_until": "2026-09-24T04:00:00Z"
}
} GET /freshness - 数据完整度
GET /freshness
返回 data_complete_until, latest_recorded_at, safe_until 和 pipeline_lag_seconds - 在把刚刚拉取的范围视为已定稿之前,请先轮询这个端点。safe_until 是调用方可信为已完整的最新 recorded_at;比它更新的行可能仍在到达中。
示例请求
curl "https://synthorai.io/api/v1/billing/freshness" \
-H "Authorization: Bearer sk-syn-bill-..." 示例响应
{
"data": {
"data_complete_until": "2026-09-24T04:00:00Z",
"latest_recorded_at": "2026-09-24T05:58:31Z",
"safe_until": "2026-09-24T05:58:31Z",
"pipeline_lag_seconds": 47
},
"meta": { "generated_at": "2026-09-24T06:00:02Z" }
} GET /balance - 工作区当前可用余额
GET /balance
返回工作区当前可用余额,与控制台显示的可用余额一致。专为轮询设计:每 30 到 60 秒查一次就足够。它有独立的速率限制,轮询不会占用数据类端点的额度;余额为零时照常返回。
示例请求
curl "https://synthorai.io/api/v1/billing/balance" \
-H "Authorization: Bearer sk-syn-bill-..." 示例响应
{
"data": {
"available_usd": 1284.37,
"debt_usd": 0,
"source": "live",
"voucher": null,
"scheduled_credits": [
{ "amount_usd": 250, "release_at": "2026-10-01T00:00:00Z" }
],
"scheduled_credit_total_usd": 250
},
"meta": { "generated_at": "2026-09-24T06:00:02Z", "currency": "USD" }
} | 参数 | 说明 |
|---|---|
available_usd | 当前请求实际扣费所用的通用余额,最低为 0。在途请求结算后可能短暂回升;统计消费请用 /usage 或 /records。 |
debt_usd | 欠款:超出已充值额度的用量,没有欠款时为 0。充值会先抵扣欠款,剩余部分才计入 available_usd。 |
source | live:读自实时账本。delayed:实时账本暂时无法读取,数值来自数据库副本,最多可能落后约 30 秒。 |
voucher | 活动/代金券额度,仅用于图像生成(见 applies_to),限所列模型、在 expires_at 之前可用;其他请求只使用 available_usd,券额不计入 available_usd。没有时为 null;过期后 active 为 false。 |
scheduled_credits | 已约定、分期到账的额度(amount_usd、release_at);每一期在 release_at 之后约 10 分钟内计入余额,之前不可使用。scheduled_credit_total_usd 为其合计。 |
错误
每个错误都是 {"error": {"type", "message", "hint"}}:
{
"error": {
"type": "range_too_large",
"message": "the requested range exceeds this endpoint's cap",
"hint": "narrow start/end to at most 31 days, or sync /records with a cursor instead"
}
} | 类型 | HTTP 状态码 | 含义 |
|---|---|---|
invalid_parameter | 400 | 某个查询参数缺失、格式错误或超出范围。 |
range_too_large | 400 | 请求的时间范围或 id 窗口超出该端点允许的上限;hint 字段会说明具体上限。 |
start_too_recent | 400 | start/end 时间窗的开始时间晚于 safe_until,起点边界尚未稳定。请按 Retry-After 头稍后重试,或改用 cursor 同步。 |
wrong_key_kind | 401 | 该凭证不是账单密钥,或账单密钥被用在了这些端点之外。 |
authentication_error | 401 | 密钥缺失、无效、已过期,或被其 IP 白名单拦截。 |
key_owner_not_authorized | 403 | 该密钥的创建者已不再是工作区所有者。账单密钥仅限所有者本人使用,所有权转移的那一刻起就会停止工作;新所有者必须创建自己的密钥。 |
rate_limited | 429 | 该工作区每分钟请求数超过 60 次。请在 Retry-After 响应头指示的时间之后重试。 |
too_many_concurrent_requests | 429 | 该工作区并发请求超过 3 个,或已有一个导出正在进行。 |
replica_unavailable | 503 | 该进程未配置只读副本;API 绝不会回退到主库。 |
query_timeout | 503 | 查询超过了 10 秒的语句超时限制或 15 秒的截止时限。请缩小范围后重试。 |
internal_error | 500 | 意外的服务器错误。请重试,如果持续出现请反馈。 |
速率限制与上限
| 防护措施 | 值 |
|---|---|
| 速率限制 | 每个工作区每分钟 60 次请求,为全实例共享的滑动窗口。 |
| 并发数 | 每个工作区在每台服务器上最多 3 个并发请求,导出流在每台服务器上每个工作区最多 1 个。 |
| 余额轮询 | /balance:每个工作区每分钟 60 次、同时最多 2 个请求,与其他端点分开计数。 |
| 范围上限 | /usage:hour 粒度 31 天,day 粒度 366 天。/summary:最多 366 天。/records 与导出:最多 31 天。 |
| 分页上限 | /usage 每页最多返回 5,000 行,/records 最多 1,000 行。导出以每页 2,000 行流式传输,并在 1,000,000 行处停止 - 可用 cursor 继续。单次导出扫过 2,000,000 个记录 id 后也会停止(stopped_because=scan_cap),并会按数据库负载自动放慢;用 cursor 继续。 |
| 缓存 | 每个响应都带有 Cache-Control: no-store。 |
绝不会返回: channel_id, upstream_request_id, usage_raw, other, content, ip_address, user_agent, cost_detail.
避免时间间隙
请求完成后会异步写入日志(通常几秒钟,积压时会更久),而 /usage 与 /summary 读取的按小时汇总表是按批次写入的。最近几个小时的数据总是有一点不完整。/usage 和 /summary 会补上自上次汇总以来新写入的请求(meta.live_tail 为 true);汇总落后太多时不再补(meta.live_tail 为 false),最近的时间桶是不完整的。
data_complete_until 取以下两者中较早的一个:按小时汇总表的水位线减去当前的处理延迟,或尚未被汇总的最早请求 - 再减去 30 分钟的安全余量,并向下取整到小时。
已定稿的桶是稳定的,只有一个例外:每日对账任务如果发现偏差,仍可能在 48 小时内修正某个小时的数据。/records 永远是某次请求精确数字的权威来源。
每个 /usage 行都带有一个 final 标志: true ,即当其所在的桶结束时间早于或等于 data_complete_until (/freshness 返回的值)时成立。已定稿的桶不会再变化。
- 聚合数据: 按 (bucket_start, api_key_id, model) 将每一行 upsert 到你自己的存储中,并在某个桶仍为 final: false 时重新拉取它。绝不要用新总量减去旧总量的方式来推算增量。
- 原始行: 按 cursor 同步,绝不要按时间同步。请保留 next_cursor,并在下一次调用时从它继续,同时按 record_id(每行稳定的不透明 id)对收到的行去重,以防某次续传重复送达同一行。晚于约 5 分钟的行会被暂时保留 - safe_until 即最新已记录行的时间减去 5 分钟 - 因此一个仍在提交中的行绝不会跑到已发出的游标位置之前,也就不会有数据落入间隙。统计请求数时,请按 request_id 去重计数。
- 限定窗口: 如果你查询的是 start/end 范围而不是按 cursor 同步,只有在 window_final 为 true 时才可信其为完整(只有当你提供了 end 且它早于或等于 safe_until 时才为 true)。如果 window_final 为 false,请稍后用相同范围再次调用,或改为按 cursor 同步。时间窗的开始时间必须不晚于 safe_until;更晚的开始时间会返回 400 start_too_recent,并带 Retry-After 头。
推荐的集成方式
- 每晚账单处理: 每天调用一次 /usage,并使用 granularity=day;对上一次运行中仍为 final: false 的桶重新拉取。
- 逐请求明细: 每小时调用一次 /records,将 cursor 设为你存储的上一次 next_cursor,并在 has_more 为 true 时持续翻页,同时按 record_id 对收到的行去重。
- 仪表盘: 对当前可见范围调用 /summary,而不是自己聚合 /records - 它已经附带了异常列表。
需要以编程方式签发或管理普通 API 密钥,而不是读取账单数据?请参阅 Provisioning Keys。