SERP API 计费口径:credits_charged、执行失败扣费与退款边界
很多人对 SERP API 的成本核算是这么算的:"一次请求 1 credit,我跑 1000 次就是 1000 credits。"这个算法在大多数日子成立,但在你排查"为什么余额掉得比预期快"的那天会失效------因为文档写得很清楚:执行失败(executed failures)也可能被扣费 ,而 credits_charged 的官方定义是"退款逻辑应用后"的扣费数。这篇把计费口径逐条对齐,给一个能对账的脚本。字段与价格以 SerpBase 官方文档的计费说明 与官网为准。
计费的基本盘
各端点每次成功请求的 credits:
| 端点 | credits |
|---|---|
| /google/search | 1 |
| /google/news | 1 |
| /google/video/search | 1 |
| /google/image/search | 2 |
| /google/maps/search | 2 |
| /google/maps/detail | 2 |
| GET /account/credits | 0(限流 60 次/分) |
信封里有个字段 credits_charged,文档对它的描述是"Credits charged after refund logic is applied"------这是你实际被扣的数字,权威来源。所以做账要以响应里的这个字段为准,不是以"这个端点标价多少"为准。
执行失败也扣费,这是最容易算错的一条
文档 Billing 一节的原话是"Per-request catalog price; executed failures may be charged"。官网价格页说得更直白:timeouts 和 unknown executions(无法判定是否执行的请求)不会自动退款。
翻译成工程语言:
- 请求发出去了、上游执行了、返回了一个错误(比如参数错误、上游没有结果)------这类"已执行的失败"可能照扣。
- 请求超时了、你不知道它到底执行没执行------不自动退款,按已执行处理。
- 只有明确符合退款逻辑的情况,才会在
credits_charged里体现为少扣或不扣。
所以批量脚本里常见的那种"失败了就当没花钱,明天重试"的思路,会让你低估成本。重试本身不是问题,问题是重试前没人把已扣的 credits 记进去。
对账脚本
python
import json, pathlib, requests
API = "https://api.serpbase.dev"
KEY = "你的 API Key"
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}
def balance() -> dict:
"""GET /account/credits,0 credits,限流 60 次/分。"""
resp = requests.get(f"{API}/account/credits", headers=HEADERS, timeout=15)
data = resp.json()
if data.get("status") != 0:
raise RuntimeError(f"{data.get('status')}: {data.get('error')}")
return data
def search(q: str, page: int = 1) -> tuple:
resp = requests.post(f"{API}/google/search", headers=HEADERS,
json={"q": q, "hl": "zh-CN", "gl": "cn", "page": page},
timeout=30)
data = resp.json()
return data, data.get("credits_charged", 0)
def run(keywords: list) -> dict:
before = balance()
spent, failures, rows = 0, 0, []
for kw in keywords:
for page in (1, 2):
data, charged = search(kw, page)
spent += charged
if data.get("status") != 0:
failures += 1
# 失败也记成本:已执行的失败可能照扣
rows.append({"关键词": kw, "页": page, "状态": data.get("status"),
"扣费": charged, "request_id": data.get("request_id")})
continue
rows.append({"关键词": kw, "页": page, "状态": 0,
"扣费": charged, "request_id": data.get("request_id"),
"条数": len(data.get("organic", []))})
pathlib.Path("billing_log.jsonl").write_text(
"\n".join(json.dumps(r, ensure_ascii=False) for r in rows), encoding="utf-8")
after = balance()
return {
"跑前余额": before["credits"],
"跑后余额": after["credits"],
"响应累加扣费": spent,
"失败请求数": failures,
"差额": before["credits"] - after["credits"] - spent,
"记录条数": len(rows),
}
if __name__ == "__main__":
report = run(["SERP API 价格", "谷歌地图数据采集"])
for k, v in report.items():
print(f"{k}: {v}")
关键在最后那个 差额:跑前余额 − 跑后余额 应该等于响应里 credits_charged 的累加值。如果差额不为 0,说明有扣费发生在你记录之外 ------最常见的原因就是失败/超时请求被扣了费但你的脚本只在成功分支里记了账。这个脚本把失败分支也记 charged,就是为了让两边对得上。
三条成本纪律
- 预算按"请求数 × 端点单价 × 1.2"估,不按请求数 × 单价估。 那 20% 是给失败重试和超时留的余量。真要精确,就跑一轮小批量,用
差额字段反推你的实际失败扣费率。 - 余额预检放在批量开头。
GET /account/credits是 0 credits 的端点,跑之前先取一次余额,和len(keywords) × 每词页数 × 端点单价比一下,不够就停。比跑到一半报 1020(余额不足)强------至少那时候你手里有一份完整的部分结果,而不是一份断在中间的 CSV。 expiring_credits要单独看。 余额响应里有credits/permanent_credits/expiring_credits/expiring_expires_at四个字段。如果余额看着够用但大部分是限时 credits,你面对的是排期问题不是预算问题:限时的先用掉,长期任务靠 permanent 那部分。
FAQ
失败自动退款吗? 不保证。文档只说 credits_charged 是"退款逻辑应用后"的数字,并明确"executed failures may be charged";官网进一步说明超时和无法判定的执行不自动退款。所以把 credits_charged 当权威账本,别把"失败=免费"当默认。
价格是多少? 100 次免费额度起步(不绑卡);付费包 Starter 10/20k(0.50/1k)、Growth 50/125k(0.40/1k)、Pro 200/570k(0.35/1k)、Business 500/1500k(0.33/1k)、Enterprise 1000/3300k(0.30/1k)。另有 Starter Boost $3/月 10k credits,但那是限时订阅制(月内有效),标准包的 credits 不过期。具体以官网当天价格为准。
1020 是什么? 余额不足的错误码。遇到它先查 GET /account/credits,别急着重试------重试只会继续扣。
为什么 maps/image 比 search 贵? 标价就是 2 credits 对 1 credits,文档里按端点列明的。做地图网格采集时把格点数先算清楚再跑,超预算了先停。
把对账脚本接进你的定时任务,每周看一眼 差额------这个数字比任何"感觉最近掉得有点快"都可靠。