服务端验证 API
前端拿到的 token 必须在服务端校验后才可信任。接口与 reCAPTCHA / hCaptcha 的 siteverify 兼容(请求体 JSON 与表单编码都接受),从它们迁移时服务端通常只需改一个 URL——对照步骤见迁移指南。
接口
POST https://api.capcat.ai/<site-key>/siteverify
Content-Type: application/json
{ "secret": "<secret_key>", "response": "<captcha_token>" }secret— 控制台创建站点时生成的 secret(仅创建/轮换时展示一次);response— 前端组件产出的 token(表单里的cap-token字段,或solve事件的e.detail.token)。
校验通过返回:
{ "success": true }若站点启用了客户自有的 reCAPTCHA v3,同一个鉴权响应还会带上与 token 绑定的 Google 分数:
{ "success": true, "score": 0.7 }score 范围为 0–1;没有执行 reCAPTCHA v3 的 token 不返回该字段。控制台设置的最低分数会在 Capcat token 签发前执行。把最低分数设为 0,即可允许任意有效的 Google 分数通过,再由你的后端在这里自行判断;Google token 本身的有效性、hostname 与 action 仍会被核验。服务端绝不接受浏览器自报的分数。Google score 越高表示越可信;这与 Capcat 原生 risk_score 越高表示风险越大的方向正好相反。两个字段可以同时出现,绝不能对它们套用相同方向的比较符。
token 一次性有效:校验成功一次后立即失效,重放会返回失败。
token 还会在解题完成 5 分钟后过期(2026 年 11 月之前创建的站点仍为 20 分钟)。token 过期时组件会自动重置,访客等待过久只需重新验证一次。
把 token 绑定到访客
分站点开放中
本节的字段正在按站点逐步开放。你的站点开放之前,remoteip 与 idempotency_key 会被接受但忽略,响应保持上文的格式。
两个可选的请求字段,表单编码同样接受:
{
"secret": "<secret_key>",
"response": "<captcha_token>",
"remoteip": "203.0.113.9",
"idempotency_key": "4f7f0f6e-2b1c-4d5e-9a8b-7c6d5e4f3a2b"
}remoteip— 你的服务端看到的访客 IP:单个 IPv4 或 IPv6 地址,不要传X-Forwarded-For整串;idempotency_key— 见安全重试。
此时成功响应还会说明 token 是何时、在哪里拿到的:
{
"success": true,
"challenge_ts": "2026-11-05T12:00:00.123Z",
"hostname": "shop.example.com",
"remoteip_check": "match"
}challenge_ts— 解题完成的时间(ISO 8601)。hostname— 浏览器上报的解题页面域名。不带Origin的客户端(例如原生 App)不返回该字段。站点配置了允许的域名时,它已经过名单校验;否则请自行与你的域名比对。remoteip_check— 只在你传了remoteip时返回:match— 解题的客户端与remoteip在同一个 /24(IPv4)或 /64(IPv6)网段;mismatch— 不在同一网段:token 很可能是在别处解出后转交过来的,这是打码平台的典型做法;unverifiable— 无法比对:一边是 IPv4、另一边是 IPv6(访客有 IPv6 而你的站点没有时很常见),remoteip不是单个 IP 地址,或这枚 token 没有记录到地址。
Capcat 只保存网段前缀的 keyed 摘要,不保存地址本身。大多数站点上 remoteip_check 只是参考信息:不一致时 success 仍为 true。只有在 Capcat 对站点执行 strict 或 attack 策略时,不一致才会直接判失败:返回 success: false 和 "error-codes": ["token-context-mismatch"]。请把它当作风险信号处理,例如要求再验证一次或转人工审核,而不是当作滥用的证据:移动网络和 VPN 偶尔会在解题和提交之间切换网络。
安全重试
token 只能校验一次,所以 siteverify 调用超时后,你无法判断它是否已被核销。请求里带上 idempotency_key,再用同一个 key、完全相同的参数重试:重试会拿到第一次的结果,而不是「重复使用」的失败。key 为 16–128 个字符,取值 A–Z a–z 0–9 . _ : -,UUID 即可。每枚 token 最多响应 3 次重试,且只在 token 原定的有效期内。
每次校验都生成一个新的 key(crypto.randomUUID()),只在重试这一次调用时复用。不要从 token、用户或会话推导 key:跨请求复用的 key 会让重放的 token 多拿到几次成功。格式错误的 key 会在核销 token 之前被拒绝,返回 HTTP 400 和 "error-codes": ["invalid-idempotency-key"]。
可选风险裁决
希望由业务后端自行决定如何处理风险时,在控制台把站点的 Account Defense 模式设为 Score。当运行状态卡片中的采集、Token metadata 与 Score 回传均显示开启后,成功响应还会返回与这枚一次性 token 绑定的裁决:
{
"success": true,
"action": "signup",
"risk_status": "ok",
"risk_score": 78,
"risk_level": "high",
"risk_reasons": ["browser_automation_exposed", "high_velocity"],
"risk_model_version": "rules-1",
"risk_signal_coverage": 1
}risk_score是0–100的整数,越高表示风险越大;scorer 不可用时为null。risk_level为low | medium | high | unknown,使用站点配置的阈值。risk_status为ok | partial | unavailable;信号缺失不会被静默当成低风险。risk_reasons是有界且带版本的 reason code;记录业务决定时应同时保存risk_model_version,便于审计策略升级前后的差异。risk_signal_coverage为0–1,表示信号覆盖率,不是置信度或概率。
success=false 必须始终拒绝。常见的客户自管策略是:low 放行、medium 增加二次验证或人工审核、high 拒绝;应按受保护操作的敏感度显式处理 unknown、partial 和 unavailable,绝不能把缺失分数强转成零。Score 模式或 operator rollout gate 未开启时,基础兼容响应仍是 { "success": true }。
例如,同时开启两种可选裁决时,业务后端可以保留最终决定权:
const verdict = await response.json();
if (!verdict.success) throw new Error("CAPTCHA verification failed");
if (typeof verdict.score === "number" && verdict.score < 0.3) {
throw new Error("reCAPTCHA risk threshold not met");
}
if (["high", "unknown"].includes(verdict.risk_level)) {
throw new Error("Account Defense policy rejected this action");
}这里的阈值只是示例,不是通用默认值。应针对自己的受保护动作做校准,并明确决定任一可选字段缺失时如何处理。
多语言示例
curl "https://api.capcat.ai/<site-key>/siteverify" \
-X POST \
-H "Content-Type: application/json" \
-d '{ "secret": "<secret_key>", "response": "<captcha_token>" }'const { success } = await (
await fetch("https://api.capcat.ai/<site-key>/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret: "<secret_key>", response: "<captcha_token>" }),
})
).json();
if (!success) throw new Error("人机验证未通过");import requests
success = requests.post(
"https://api.capcat.ai/<site-key>/siteverify",
json={"secret": "<secret_key>", "response": "<captcha_token>"},
).json().get("success")<?php
$ctx = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Content-Type: application/json",
"content" => json_encode([
"secret" => "<secret_key>",
"response" => "<captcha_token>",
]),
],
]);
$data = json_decode(file_get_contents(
"https://api.capcat.ai/<site-key>/siteverify", false, $ctx
), true);
var_dump($data["success"] ?? false);