Skip to content

服务端验证 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)。

校验通过返回:

json
{ "success": true }

若站点启用了客户自有的 reCAPTCHA v3,同一个鉴权响应还会带上与 token 绑定的 Google 分数:

json
{ "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 会被接受但忽略,响应保持上文的格式。

两个可选的请求字段,表单编码同样接受:

json
{
  "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 是何时、在哪里拿到的:

json
{
  "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 绑定的裁决:

json
{
  "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 }。

例如,同时开启两种可选裁决时,业务后端可以保留最终决定权:

js
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");
}

这里的阈值只是示例,不是通用默认值。应针对自己的受保护动作做校准,并明确决定任一可选字段缺失时如何处理。

多语言示例 ​

sh
curl "https://api.capcat.ai/<site-key>/siteverify" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{ "secret": "<secret_key>", "response": "<captcha_token>" }'
js
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("人机验证未通过");
py
import requests

success = requests.post(
    "https://api.capcat.ai/<site-key>/siteverify",
    json={"secret": "<secret_key>", "response": "<captcha_token>"},
).json().get("success")
php
<?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);

基于 Cap(Apache 2.0)构建