EchoScan API 与接入指南
Browser Verifier 在浏览器中采集并提交信号,成功后返回 EchoScan 服务端签发的 imprint;你的后端据此查询正式 Report。
需要构建自动化评估 Agent?Agent API 发现指南说明产品目录、OpenAPI 合同与 Agent Trial 流程。Workspace 用户可以通过 OAuth 2.1 连接正式 MCP Server,无需共享 Secret API Key。
命令行接入见 CLI 接入。
推荐接入流程
这是一份可以逐项完成的接入清单。首次接入时只需在 Console 填写网站地址,EchoScan 会同时创建 Web App 和默认 Server API Key。滚动或选择任一步骤时,右侧会补充这一阶段的执行边界、实用检查和预期结果。
1. 添加接入网站
前往 Console 的首次网站接入,填写 Browser Verifier 将运行的网站地址,例如 https://shop.example.com。系统会从网站地址提取协议、主机和端口,移除路径、查询和 Fragment,保存精确 Origin,并在一个事务中创建 Web App 和默认 Server API Key。创建后复制公开的 env_... Environment ID。
网站尚未启动时也可先填写 localhost:3000、127.0.0.1:8000 或 [::1]:3000。Console 为这些回环地址默认补 HTTP,普通域名默认补 HTTPS;明确填写的 HTTP(S) 协议会保留。预览显示最终保存的精确 Origin。本地接通后,在网站接入展开原网站详情,点击添加地址,在新的一行追加部署地址并保留本地地址,然后保存原配置。Environment ID 和既有服务器 Key 不变。添加地址不代表已接通;每个网站仍需真实 Submit,且客户后端读取同一次 Imprint 的 Report。
现有免费套餐可用于本地接入。浏览器 Submit 与服务端请求分别按当前套餐规则计量,本地与线上使用相同规则。本地与线上数据仍保存在同一个 Workspace。
2. 保存服务端 API Key
首次网站接入成功后,Console 会显示默认 API Key 的 Secret。它只显示一次,请立即保存到后端的 Secret Manager 或环境变量中,不要放进浏览器代码、日志或版本库。以后可以在 API Key 管理中独立创建、轮换或撤销 Key,不会改变 Web App。
3. 在网页中安装 Browser Verifier
安装 Browser SDK,并在 createEchoScan({ environmentId }) 中填写第一步得到的 Environment ID。Allowed Origins 由 Console 管理,不写进网页代码。
4. 获取并发送 Imprint
在登录、注册、支付等受保护动作发生时调用 run()。把返回的 imprint 随业务请求发送到你自己的服务端。
5. 在后端查询 Report
后端使用第二步保存的 Secret API Key,按 imprint 查询统一 Report Endpoint。正式的设备身份和风险结果来自服务端 Report。
6. 应用业务决策
读取 risk.status、risk.reasons 和 risk.findings,再结合账号、交易和业务上下文决定放行、追加验证、人工复核或拒绝。只向浏览器返回业务需要的最小结果。
浏览器代码和服务端 route 可以位于同一仓库。安全边界取决于运行位置:environmentId 可以公开,API key 只能存在于服务端。
接入成功意味着:浏览器完成真实提交,客户后端成功读取本次 imprint 对应的有效 Report。SUSPICIOUS 或 DECEPTIVE 可以是一份成功取得的报告;采集失败、查询失败与风险状态应分别处理。
获取并发送 Imprint
Direct 模式必须配置固定格式 env_<32 lowercase hex> 的 environmentId。Browser Verifier 在指纹 Submit 及携带诊断关联信息的公钥请求中使用它,不接受 Workspace ID,也不会把 API key 或 Authorization 放进浏览器。
npm install @echoscan/browser-verifier@3.0.5
import { createEchoScan } from '@echoscan/browser-verifier'
const sdk = createEchoScan({
environmentId: 'env_0123456789abcdef0123456789abcdef'
})
const { imprint } = await sdk.run()
npm 接入与 CDN 接入二选一。使用 CDN 时,以下 ESM 与 UMD 代码块也只选一个;示例中的查询路径由你的后端实现。
CDN:ESM(二选一)
<script type="module">
import { createEchoScan } from 'https://cdn.echoscan.org/v1/echoscan.esm.js'
const sdk = createEchoScan({
environmentId: 'env_0123456789abcdef0123456789abcdef'
})
try {
const { imprint } = await sdk.run()
const response = await fetch('/api/echoscan/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imprint })
})
if (!response.ok) throw new Error('Report lookup failed')
// Continue with the minimal business result from your backend.
} catch {
// Collection or lookup failed: stop this action; offer a controlled retry.
} finally {
sdk.dispose()
}
</script>
CDN:UMD(二选一)
<script src="https://cdn.echoscan.org/v1/echoscan.umd.js"></script>
<script>
(async () => {
const sdk = window.EchoScan.createEchoScan({
environmentId: 'env_0123456789abcdef0123456789abcdef'
})
try {
const { imprint } = await sdk.run()
const response = await fetch('/api/echoscan/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imprint })
})
if (!response.ok) throw new Error('Report lookup failed')
// Continue with the minimal business result from your backend.
} catch {
// Collection or lookup failed: stop this action; offer a controlled retry.
} finally {
sdk.dispose()
}
})()
</script>
run() 返回 imprint 及可选的最小 diagnostic 回执。新 Imprint 格式为 imp_<32 lowercase hex>。
Allowed Origins 是浏览器部署保护和错误配置防护,不是秘密认证。只登记完整的 http / https Origin,例如 https://staging.example.com:8443 或 http://localhost:3000;匹配保留 Scheme 和 Port,并统一小写 Host。Path、Query、Fragment、UserInfo、通配符(包括 *.example.com)、裸域名、null、file:// 和扩展 Origin 全部拒绝。空 Allowlist 或缺少 Origin Header 会拒绝所有 Public Submit。
Browser Verifier 初始化输入
| 参数 | 含义 |
|---|---|
environmentId |
可公开的 Browser Environment ID。服务端据此解析可信 Workspace,并校验 Allowed Origins |
运行返回值。 imprint 是 run() 成功后返回的服务端正式报告编号,应随受保护业务动作发送到服务端。
排查一次 Browser Verifier 运行
以下示例面向 Browser Verifier 3.0.5。浏览器与实际提供的 SDK 资源应使用同一版本。
此主版本移除已发布 2.x 的 getLastRunTrace() 和详细错误类导出。接入排障只保留 SDK_VERSION、EchoScanError.category 与最小 diagnostic 回执。direct profile 中,not_sent 表示尚未开始携带关联编号的 API 请求;unconfirmed 表示编号已发送但未收到匹配回执;received 表示确实收到对应 API 操作的持久化受理回执。未确认请求可能没有服务端记录,编号本身不授予查询权限。
import { createEchoScan, SDK_VERSION, EchoScanError } from '@echoscan/browser-verifier'
const sdk = createEchoScan({ environmentId: 'env_0123456789abcdef0123456789abcdef' })
try {
const { imprint } = await sdk.run()
// Send imprint to your server; only the server queries the business Report.
await fetch('/api/echoscan/report', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imprint, sdkVersion: SDK_VERSION })
})
} catch (error) {
const category = error instanceof EchoScanError ? error.category : 'integration_failed'
const support = { sdkVersion: SDK_VERSION, category,
diagnostic: error instanceof EchoScanError ? error.diagnostic : null }
// Provide only this safe summary to support. No automatic diagnostic upload.
void support
} finally {
sdk.dispose()
}
支持人员可以依据最小诊断回执,在授权范围内关联已记录的请求及正式 Report。回执不包含完整的浏览器现场,也不保证未确认请求有服务端记录。
Imprint 只在受控业务流程中传递,完整 Report 留在客户服务端。不要复制原始错误、指纹、IP、Key、Cookie 或带参数 URL。技术失败不是风险判定,缺少信息本身也不是风险结论。
Submit 失败或 Report 查询失败属于技术失败,不是风险判定,应按业务规则保持待处理或拒绝本次动作。仅对可恢复的技术失败做有上限的重试,不要反复检测直到 PASS。成功查到 SUSPICIOUS 或 DECEPTIVE 是已评估的结果,由客户结合业务上下文决定放行、追加验证、复核或拒绝。challenge 的验证流程由客户实现;领取资格、幂等和防重复发码仍由客户后端负责。
发送到服务端
按你的业务 API 结构,把 Imprint 随受保护动作发送到服务端。
await fetch('/api/your-action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...yourActionPayload,
echoscanImprint: imprint
})
})
await fetch('/api/your-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-EchoScan-Imprint': imprint
},
body: JSON.stringify(yourActionPayload)
})
await fetch('/api/echoscan/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imprint })
})
上面的 /api/echoscan/report 是接入方自建的示例路径,不是 EchoScan Endpoint。
最小完整默认接入示例
使用 Node.js 22.22 或受支持的 22.x、pnpm 和 npm 发布的 Browser Verifier。以下为独立演示项目。接入已有项目时,保留原配置,仅合入所需代码。先在 Console 添加 http://localhost:3000,将实际 Environment ID 写入 .env.local,将 Secret 写入 .env.server;两份配置都不要提交到版本库。
package.json
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 3000 --strictPort",
"server": "node --env-file=.env.server server.mjs"
},
"dependencies": {
"@echoscan/browser-verifier": "3.0.5",
"ajv": "8.20.0",
"ajv-formats": "3.0.1"
},
"devDependencies": {
"vite": "7.3.6"
}
}
index.html
<button id="verify" type="button">Verify visit</button>
<p id="result" role="status"></p>
<script type="module" src="/main.js"></script>
main.js
import { createEchoScan } from '@echoscan/browser-verifier'
const sdk = createEchoScan({ environmentId: import.meta.env.VITE_ECHOSCAN_ENVIRONMENT_ID })
const button = document.querySelector('#verify')
const result = document.querySelector('#result')
button.addEventListener('click', async () => {
button.disabled = true
result.textContent = 'Checking…'
try {
const { imprint } = await sdk.run()
const response = await fetch('/api/echoscan/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imprint })
})
if (!response.ok) throw new Error('Report lookup failed')
const { decision } = await response.json()
if (!['allow', 'challenge'].includes(decision)) throw new Error('Invalid decision')
result.textContent = decision === 'allow' ? 'Report retrieved: allow' : 'Report retrieved: additional verification'
} catch {
result.textContent = 'Verification unavailable. Retry when ready.'
} finally {
button.disabled = false
}
})
window.addEventListener('pagehide', () => sdk.dispose(), { once: true })
vite.config.js
export default {
server: { proxy: { '/api/echoscan/report': 'http://127.0.0.1:3001' } }
}
server.mjs
import { createServer } from 'node:http'
import { readFile } from 'node:fs/promises'
import Ajv2020 from 'ajv/dist/2020.js'
import addFormats from 'ajv-formats'
const apiKey = process.env.ECHOSCAN_API_KEY
if (!apiKey) throw new Error('ECHOSCAN_API_KEY is required')
const contract = JSON.parse(await readFile(new URL('./openapi.json', import.meta.url), 'utf8'))
const ajv = new Ajv2020()
addFormats(ajv)
const validateReport = ajv.compile(contract.components.schemas['public.report.schema.json'])
createServer(async (request, response) => {
const send = (status, body) => {
response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
response.end(JSON.stringify(body))
}
if (request.method !== 'POST' || request.url !== '/api/echoscan/report') {
send(404, { error: 'not_found' })
return
}
let imprint
try {
let body = ''
for await (const chunk of request) {
body += chunk
if (Buffer.byteLength(body) > 2048) throw new Error('Request too large')
}
imprint = JSON.parse(body).imprint
if (typeof imprint !== 'string' || !/^imp_[0-9a-f]{32}$/.test(imprint)) throw new Error('Invalid imprint')
} catch {
send(400, { error: 'invalid_request' })
return
}
try {
const upstream = await fetch(`https://api.echoscan.org/api/v1/fingerprint/report/${imprint}`, {
headers: { 'X-API-Key': apiKey, Accept: 'application/json' },
signal: AbortSignal.timeout(5000)
})
if (!upstream.ok) {
// HTTP failures never enter the risk decision branch.
send(503, { error: 'verification_unavailable' })
return
}
const report = await upstream.json()
if (!validateReport(report) || report.imprint !== imprint) {
send(503, { error: 'verification_unavailable' })
return
}
if (report.risk.status === 'UNAVAILABLE') {
send(503, { error: 'verification_unavailable' })
return
}
const decision = report.risk.status === 'PASS' ? 'allow' : 'challenge'
send(200, { decision })
} catch {
send(503, { error: 'verification_unavailable' })
}
}).listen(3001, '127.0.0.1')
.gitignore
.env.local
.env.server
node_modules/
dist/
.env.local
VITE_ECHOSCAN_ENVIRONMENT_ID=env_0123456789abcdef0123456789abcdef
.env.server
ECHOSCAN_API_KEY=<your_server_secret_key>
pnpm install
curl --fail --output openapi.json https://echoscan.org/openapi.json
pnpm server
# In a second terminal:
pnpm dev
启动后打开 http://localhost:3000,点击 Verify visit。浏览器真实 Submit 后,后端以同一 Imprint 查询并按下载的正式 OpenAPI Report Schema 校验;成功只返回 allow 或 challenge,失败显示不可用。示例策略不自动执行登录、付款或资源发放;接入现有业务前仍需保留你自己的认证、授权和追加验证策略。openapi.json 在显式准备步骤下载,普通运行不远程加载 schema。
Report 接入(Lite/Pro)
Lite 与 Pro 使用相同的基础 Report 结构。下面的通用查询在升级后仍然适用;Secret key 只能由服务端代码读取。
查询 report
GET https://api.echoscan.org/api/v1/fingerprint/report/{imprint}
X-API-Key: <your_lite_key>
Accept: application/json
Node.js、Go、Python 和 Rust SDK 封装同一套服务端 HTTP API。
SDK 版本适用范围
本页服务端 SDK 的安装与调用示例固定为 0.2.4,使用当前 Report、History 和 Account Map 路径。0.2.3 的 Pro 方法客户端也使用这些公开路径,可配合具有相应访问权限的 Lite 或 Pro Key;但其 Lite 客户端仍请求已退役的 /api/v1/fingerprint/report-lite/{imprint},不适用于当前基础 Report API。保留 0.2.3 时,请使用 Pro 方法客户端执行这些操作,或升级到 0.2.4;客户端名称不决定套餐。
公开包的安装、导入/编译及合成 HTTP 示例已核验,不代表每种语言 SDK 都已完成生产接入。
npm install @echoscan/echoscan@0.2.4
import { createLiteClient } from '@echoscan/echoscan'
const echoscan = createLiteClient({
apiKey: process.env.ECHOSCAN_API_KEY
})
const report = await echoscan.getReport(imprint)
console.log(report.risk.status)
go get github.com/echoscan/echoscan-go@v0.2.4
package main
import (
"context"
"log"
"os"
echoscan "github.com/echoscan/echoscan-go"
)
func main() {
imprint := "imp_0123456789abcdef0123456789abcdef"
echoscanClient, err := echoscan.NewLiteClient(os.Getenv("ECHOSCAN_API_KEY"))
if err != nil {
log.Fatal(err)
}
report, err := echoscanClient.GetReport(context.Background(), imprint)
if err != nil {
log.Fatal(err)
}
log.Printf("risk_status=%v", report["risk"].(map[string]any)["status"])
}
pip install echoscan==0.2.4
import os
from echoscan import EchoScanLiteClient
imprint = "imp_0123456789abcdef0123456789abcdef"
echoscan_client = EchoScanLiteClient(os.environ["ECHOSCAN_API_KEY"])
report = echoscan_client.get_report(imprint)
print(report["risk"]["status"])
[dependencies]
echoscan = "=0.2.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use std::env;
use echoscan::LiteClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let imprint = "imp_0123456789abcdef0123456789abcdef";
let api_key = env::var("ECHOSCAN_API_KEY")?;
let echoscan = LiteClient::new(&api_key)?;
let report = echoscan.get_report(imprint).await?;
println!("{}", report["risk"]["status"]);
Ok(())
}
**请求计量。**浏览器 Submit 记录在 Browser Environment 下。API Key 与 MCP 的适用计量请求共用 Workspace 服务端额度。服务端计量接口包括 Report GET、Account Map POST、History GET,以及其他适用的受保护 API 调用。进入用量记录环节的每次调用计一次,包括业务处理错误和重复查询同一 Imprint;在此之前被认证、速率或配额检查拒绝的请求不计入该服务端请求量。浏览器使用同一提交 ID、被识别为重复提交的重试,不重复消耗 Submit 额度。
Lite 使用 UTC 自然月;Pro Beta 与付费 Pro 使用当前权益或订阅周期。创建更多 Key 不会增加额度。浏览器 Submit 与服务端调用分别计量,不能等同于独立访客人数。当前额度与周期以 Console 展示为准。
正式 Report
Lite 与 Pro 使用相同的基础 Report 结构;Enterprise Key 也使用这一正式合同。可选字段可能因信号不可用或缺少适用数据而省略。
| 报告内容 | 客户可以了解什么 |
|---|---|
| 报告编号与时间 | 这份报告对应哪一次检测,以及报告生成时间。 |
| 设备连续性 | 当前 Workspace 内的设备 ID、是否曾经出现、累计访问次数,以及有记录时的首次和上次出现时间。 |
| 当前访问风险 | 整体风险状态、产品级风险原因,以及每条原因对应的风险等级。 |
| 浏览器与操作系统 | 识别出的名称、可获得的版本,以及各自的风险状态。 |
| 网络信息 | 可获得的出口 IP、国家或地区、代理相关结果、网络一致性和其他网络上下文。 |
| 近期活动 | 有数据时,提供最近 5 分钟、1 小时和 24 小时的访问次数、不同 IP 数量和不同国家或地区数量。 |
首次记录的设备没有“上次出现时间”,该字段为 null。接入代码应保留 0、false、空集合和 null 的原有含义,并按公开合同处理缺失字段。基础报告中的设备连续性摘要和近期活动,与独立 History API 的指定时间范围查询分别表达。
正常访问示例:
{
"schema_version": "1.0",
"imprint": "imp_0123456789abcdef0123456789abcdef",
"created_at": "2026-07-28T01:30:00Z",
"device": {
"id": "did_A12B34C56D78",
"seen_before": false,
"access_count": 1,
"previous_seen_at": null
},
"risk": {
"status": "PASS",
"reasons": [],
"findings": []
},
"browser": {
"status": "PASS",
"name": "Google Chrome",
"version": "145"
},
"operating_system": {
"status": "PASS",
"name": "Windows",
"version": "11"
},
"network": {
"status": "PASS",
"observed_ip": "203.0.113.10",
"country_code": "JP",
"ip_consistency": "MATCH",
"proxy_detected": false
},
"activity": {
"5m": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
},
"1h": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
},
"24h": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
}
}
}
可疑访问示例:
{
"schema_version": "1.0",
"imprint": "imp_11111111111111111111111111111111",
"created_at": "2026-07-28T01:35:00Z",
"device": {
"id": "did_C98D76E54F32",
"seen_before": false,
"access_count": 1,
"previous_seen_at": null
},
"risk": {
"status": "SUSPICIOUS",
"reasons": [
"SIGNAL_DATA_INCOMPLETE"
],
"findings": [
{
"reason": "SIGNAL_DATA_INCOMPLETE",
"status": "SUSPICIOUS"
}
]
},
"browser": {
"status": "SUSPICIOUS",
"name": "Google Chrome",
"version": "145"
},
"operating_system": {
"status": "PASS",
"name": "Windows",
"version": "11"
},
"network": {
"status": "PASS",
"observed_ip": "198.51.100.20",
"country_code": "US",
"ip_consistency": "MATCH",
"proxy_detected": false
},
"activity": {
"5m": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
},
"1h": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
},
"24h": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
}
}
}
复访设备示例:
{
"schema_version": "1.0",
"imprint": "imp_22222222222222222222222222222222",
"created_at": "2026-07-28T02:00:00Z",
"device": {
"id": "did_A12B34C56D78",
"seen_before": true,
"access_count": 8,
"first_seen_at": "2026-06-10T03:20:00Z",
"previous_seen_at": "2026-07-27T08:40:00Z"
},
"risk": {
"status": "PASS",
"reasons": [],
"findings": []
},
"browser": {
"status": "PASS",
"name": "Google Chrome",
"version": "145"
},
"operating_system": {
"status": "PASS",
"name": "Windows",
"version": "11"
},
"network": {
"status": "PASS",
"observed_ip": "203.0.113.10",
"country_code": "JP",
"ip_consistency": "MATCH",
"proxy_detected": false
},
"activity": {
"5m": {
"events": 1,
"distinct_ips": 1,
"distinct_countries": 1
},
"1h": {
"events": 2,
"distinct_ips": 1,
"distinct_countries": 1
},
"24h": {
"events": 8,
"distinct_ips": 2,
"distinct_countries": 1
}
}
}
字段说明
| 字段 | 返回规则 | 说明 |
|---|---|---|
schema_version |
固定返回。 | 本报告使用的公开数据结构版本。 |
imprint |
固定返回。 | 本次检测报告的唯一编号。 |
created_at |
固定返回,使用 UTC 时间。 | 本次报告的生成时间。 |
device |
固定返回。 | 当前设备在本 Workspace 中的识别结果和历史出现情况。 |
device.id |
固定返回。 | 当前 Workspace 内的设备 ID。 |
device.seen_before |
固定返回。 | 本次访问前是否已经见过该设备。 |
device.access_count |
固定返回,包含本次访问。 | 截至本次,该设备在当前 Workspace 中的累计访问次数。 |
risk |
固定返回。 | 本次访问的总体风险结果。 |
risk.status |
固定返回。 | 本次访问的总体风险等级。 |
browser |
固定返回。 | EchoScan 识别出的浏览器信息及其风险状态。 |
browser.status |
固定返回。 | 本次访问在浏览器身份维度上的风险等级。 |
browser.name |
有结果时返回。 | EchoScan 识别出的浏览器名称。 |
browser.version |
有结果时返回。 | EchoScan 识别出的浏览器版本。 |
operating_system |
固定返回。 | EchoScan 识别出的操作系统信息及其风险状态。 |
operating_system.status |
固定返回。 | 本次访问在操作系统环境维度上的风险等级。 |
operating_system.name |
有结果时返回。 | EchoScan 识别出的操作系统名称。 |
operating_system.version |
有结果时返回。 | EchoScan 识别出的操作系统版本。 |
network |
固定返回。 | 本次访问的网络信息及其风险状态。 |
network.status |
固定返回。 | 本次访问在网络维度上的风险等级。 |
network.observed_ip |
有结果时返回。 | 本次请求到达 EchoScan 时使用的公网出口 IP。 |
network.country_code |
有结果时返回。 | 请求出口 IP 对应的国家或地区代码。 |
有限值说明
| 字段 | 值 | 标题 | 含义 | 接入方业务处理示例 |
|---|---|---|---|---|
device.seen_before |
true |
已见过该设备 | 本次访问前已经见过该设备。 | |
device.seen_before |
false |
首次记录该设备 | 本次访问是该设备的首次记录。 | |
risk.status |
PASS |
当前未发现需要关注的风险 | 本次访问可以按正常业务规则继续处理。 | 继续执行原有业务流程。 |
risk.status |
SUSPICIOUS |
存在需要关注的风险 | 当前结果适合结合账号和业务上下文进一步判断。 | 增加验证、限流或人工复核。 |
risk.status |
DECEPTIVE |
存在较明确的高风险迹象 | 当前访问存在较明确的伪装、自动化或高风险网络迹象。 | 使用更严格的验证、限制或人工审核。 |
risk.status |
UNAVAILABLE |
暂时无法判断 | 关键数据不足,暂时无法判断。 | 提示验证暂不可用,按业务规则保持待处理或拒绝本次动作。 |
browser.status |
PASS |
浏览器信息正常 | 浏览器信息未发现需要关注的异常。 | |
browser.status |
SUSPICIOUS |
浏览器信息需关注 | 浏览器信息存在需要关注的异常。 | |
browser.status |
DECEPTIVE |
浏览器信息高风险 | 浏览器信息存在较明确的伪装迹象。 | |
operating_system.status |
PASS |
操作系统信息正常 | 操作系统信息未发现需要关注的异常。 | |
operating_system.status |
SUSPICIOUS |
操作系统信息需关注 | 操作系统信息存在需要关注的异常。 | |
operating_system.status |
DECEPTIVE |
操作系统信息高风险 | 操作系统信息存在较明确的伪装迹象。 | |
network.status |
PASS |
网络信息正常 | 网络信息未发现需要关注的异常。 | |
network.status |
SUSPICIOUS |
网络信息需关注 | 网络信息存在需要关注的风险。 | |
network.status |
DECEPTIVE |
网络信息高风险 | 网络信息存在较明确的高风险迹象。 |
History 与 Account Map(Lite/Pro)
Lite 与 Pro 均支持 History 和可选 Account Map,包括新建关联与已有快照读取。两者使用相同的服务端 Imprint、服务端 Secret 边界、Endpoint 和基础 Report。Pro 提供更高请求额度、速率和 Browser Environment 容量。
查询 report
基础 Report 查询统一见 Report 接入。以下 SDK 示例也演示独立的 History 操作。
Lite 与 Pro 均支持 History 查询。SDK 的 Lite/Pro 名称表示方法组合,不表示不同 Report 结构或套餐要求。使用提供所需方法的客户端和当前有效的 Workspace Key:
GET https://api.echoscan.org/api/v1/fingerprint/imprint/{imprint}/history?days=7&recent=20
X-API-Key: <your_server_key>
Accept: application/json
GET https://api.echoscan.org/api/v1/fingerprint/imprint/{imprint}/history?from=2026-03-01&to=2026-03-18&recent=20
X-API-Key: <your_server_key>
Accept: application/json
npm install @echoscan/echoscan@0.2.4
import { createProClient } from '@echoscan/echoscan'
const echoscan = createProClient({
apiKey: process.env.ECHOSCAN_API_KEY
})
const report = await echoscan.getReport(imprint)
console.log(report.risk.status, report.risk.reasons)
const history = await echoscan.getHistory(imprint, { days: 7 })
go get github.com/echoscan/echoscan-go@v0.2.4
package main
import (
"context"
"log"
"os"
echoscan "github.com/echoscan/echoscan-go"
)
func main() {
imprint := "imp_0123456789abcdef0123456789abcdef"
days := 7
echoscanClient, err := echoscan.NewProClient(os.Getenv("ECHOSCAN_API_KEY"))
if err != nil {
log.Fatal(err)
}
report, err := echoscanClient.GetReport(context.Background(), imprint)
if err != nil {
log.Fatal(err)
}
history, err := echoscanClient.GetHistory(context.Background(), imprint, echoscan.HistoryQuery{Days: &days})
if err != nil {
log.Fatal(err)
}
log.Printf("risk_status=%v", report["risk"].(map[string]any)["status"])
_ = history
}
pip install echoscan==0.2.4
import os
from echoscan import EchoScanProClient
imprint = "imp_0123456789abcdef0123456789abcdef"
echoscan_client = EchoScanProClient(os.environ["ECHOSCAN_API_KEY"])
report = echoscan_client.get_report(imprint)
history = echoscan_client.get_history(imprint, days=7)
print(report["risk"]["status"])
[dependencies]
echoscan = "=0.2.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use std::env;
use echoscan::{HistoryQuery, ProClient};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let imprint = "imp_0123456789abcdef0123456789abcdef";
let api_key = env::var("ECHOSCAN_API_KEY")?;
let echoscan = ProClient::new(&api_key)?;
let report = echoscan.get_report(imprint).await?;
let _history = echoscan.get_history(imprint, HistoryQuery::Days { days: 7, recent: None }).await?;
println!("{}", report["risk"]["status"]);
Ok(())
}
可选:开启 Account Map(Lite/Pro)
当你需要回答以下问题时,可以使用 Account Map:
- 当前设备关联了多少账号?
- 当前账号使用过多少设备?
- 当前账号和设备以前是否共同出现?
- 一台设备是否在短时间内关联了大量新账号?
- 这种关系是否可能表示账号共享或账号接管?
Lite 与 Pro 均支持新建 Account Map 关联。在服务端 Report 查询中传入稳定的内部用户 ID。
Node.js
const report = await pro.getReport(imprint, {
accountRef: currentUser.id
})
Go
report, err := pro.GetReportWithOptions(
context.Background(),
imprint,
echoscan.ReportOptions{AccountRef: currentUser.ID},
)
if err != nil {
return err
}
Python
report = pro.get_report(
imprint,
account_ref=current_user.id,
)
Rust
use echoscan::ReportOptions;
let report = pro
.get_report_with_options(
imprint,
ReportOptions { account_ref: Some(current_user.id.clone()) },
)
.await?;
Account Map 完全可选。accountRef 应使用稳定、非敏感、不可变的内部用户主键。不要传邮箱、电话号码、真实姓名、昵称或可修改的用户名。Account 关系按 Workspace 隔离。
Report GET 可以直接读取该 Imprint 的已有快照,无需再次传入 accountRef。Account Map 建立关联与读取已有快照是不同操作,恢复 Lite 本身不意味着已有快照消失。Account Map v1 不会自动修改 risk.status;请结合自身业务规则使用这些关系信号。
并发或延迟关联的一致性
Account Map 统计只包含生成响应时已经完成并可见的关联。
并发 POST 响应无法提前包含尚未完成的其他关联。
所有并发或延迟关联完成后,后续 GET 会按服务端事件顺序(先 recorded_at,再 event_id)重新计算历史统计。
较早事件保持自身的历史边界;较晚事件会包含其边界及以前已经可见的关联。
Account Map 返回示例:
{
"account_map": {
"account_seen_before": true,
"relationship_seen_before": false,
"accounts_on_device": 7,
"devices_on_account": 2,
"accounts_first_seen_on_device_1h": 4
}
}
| 字段 | 含义 |
|---|---|
account_seen_before |
当前访问之前,此账号是否已在当前 Workspace 的任意设备上出现过。 |
relationship_seen_before |
当前访问之前,此账号是否曾与当前设备共同出现。 |
accounts_on_device |
与当前设备关联的去重账号数,包含本次关系。 |
devices_on_account |
与当前账号关联的去重设备数,包含本次关系。 |
accounts_first_seen_on_device_1h |
过去一小时内首次与当前设备建立关系的账号数。只有在注册完成流程中传入 accountRef 时,它才可以近似用于观察新注册账号数量;如果只在登录流程传入,它表示首次被 EchoScan 看到的账号,不必然代表账号刚刚创建。 |
Report 字段与风险原因
正式 Report 始终采用这一字段模型。可选值按公开合同、可用信号与适用数据返回;account_map 可以包含已有快照,不保证每次请求都返回。
{
"schema_version": "1.0",
"imprint": "imp_0123456789abcdef0123456789abcdef",
"created_at": "2026-07-28T01:30:00Z",
"device": {
"id": "did_A12B34C56D78",
"seen_before": true,
"access_count": 42,
"first_seen_at": "2026-06-10T03:20:00Z",
"previous_seen_at": "2026-07-27T08:40:00Z"
},
"risk": {
"status": "DECEPTIVE",
"reasons": [
"BROWSER_VERSION_MISMATCH",
"PROXY_DETECTED",
"NETWORK_INCONSISTENT"
],
"findings": [
{
"reason": "BROWSER_VERSION_MISMATCH",
"status": "DECEPTIVE"
},
{
"reason": "PROXY_DETECTED",
"status": "DECEPTIVE"
},
{
"reason": "NETWORK_INCONSISTENT",
"status": "DECEPTIVE"
}
]
},
"browser": {
"status": "DECEPTIVE",
"name": "Google Chrome",
"version": "145"
},
"operating_system": {
"status": "PASS",
"name": "Windows",
"version": "11"
},
"network": {
"status": "DECEPTIVE",
"observed_ip": "198.23.233.104",
"country_code": "US",
"alternate_ip": "126.234.173.23",
"ip_consistency": "MISMATCH",
"proxy_detected": true,
"location": {
"country_name": "United States",
"region": "Illinois",
"city": "Elk Grove Village",
"timezone": "America/Chicago"
},
"provider": "Example Hosting Provider",
"connection_type": "proxy",
"asn": 36352
},
"activity": {
"5m": {
"events": 2,
"distinct_ips": 1,
"distinct_countries": 1
},
"1h": {
"events": 8,
"distinct_ips": 2,
"distinct_countries": 2
},
"24h": {
"events": 21,
"distinct_ips": 4,
"distinct_countries": 3
}
}
}
PASS Report 仍会明确序列化空 reasons 和 findings 数组:
{
"risk": {
"status": "PASS",
"reasons": [],
"findings": []
}
}
各字段的返回条件统一列在下方,有限值只描述公开机器值的产品含义。
产品级风险原因
| Reason | 含义 |
|---|---|
BROWSER_VERSION_MISMATCH |
浏览器版本信息不一致 |
OS_ENVIRONMENT_MISMATCH |
操作系统环境信息不一致 |
AUTOMATION_DETECTED |
检测到自动化访问 |
ENVIRONMENT_INCONSISTENT |
当前设备环境信息不一致 |
PROXY_DETECTED |
检测到代理或 VPN 风险 |
HOSTING_NETWORK_DETECTED |
访问来自托管服务或数据中心网络 |
NETWORK_INCONSISTENT |
网络身份信息不一致 |
LOCATION_INCONSISTENT |
位置相关信息不一致 |
SIGNAL_DATA_INCOMPLETE |
本次检测的关键数据不完整 |
Reason 表达风险类别。整体严重程度由 risk.status 表达;risk.findings 中每条原因还带有其贡献的风险等级。
正式 Report 其他字段
| 字段 | 返回规则 | 说明 |
|---|---|---|
device.first_seen_at |
有结果时返回,使用 UTC 时间。 | 当前 Workspace 首次见到该设备的时间。 |
device.previous_seen_at |
固定返回;首次访问时为 null。 |
本次访问前最近一次见到该设备的时间。 |
risk.reasons |
固定返回。PASS 时返回空数组 []。 |
本次风险结果对应的产品级原因类别。 |
risk.findings |
固定返回。PASS 时返回空数组 []。 |
生成本次结论的原因和严重级别组合。 |
network.alternate_ip |
有结果时返回。 | EchoScan 检测到的客户端公网 IP。 |
network.ip_consistency |
固定返回。 | 客户端公网 IP 与请求出口 IP 的一致性结果。 |
network.proxy_detected |
有结果时返回。 | 是否检测到代理或 VPN 风险。 |
network.location |
有结果时返回。 | 请求出口 IP 对应的网络位置信息。 |
network.location.country_name |
network.location 返回时有结果则返回。 |
请求出口 IP 对应的国家或地区名称。 |
network.location.region |
network.location 返回时有结果则返回。 |
请求出口 IP 对应的地区或一级行政区域。 |
network.location.city |
network.location 返回时有结果则返回。 |
请求出口 IP 对应的城市。 |
network.location.timezone |
network.location 返回时有结果则返回。 |
请求出口 IP 所在网络位置对应的时区。 |
network.provider |
有结果时返回。 | 请求出口 IP 所属的网络组织或服务提供方。 |
network.connection_type |
有结果时返回。 | 请求出口 IP 所属的网络类型。 |
network.asn |
有结果时返回。 | 请求出口 IP 所属的自治系统编号。 |
activity |
有结果时返回。 | 当前设备的近期访问活动摘要。 |
activity.5m |
activity 返回时固定返回,包含本次访问。 |
当前设备最近 5 分钟的访问活动。 |
activity.5m.events |
对应时间窗口返回时固定返回。 | 该时间窗口内的访问次数。 |
activity.5m.distinct_ips |
对应时间窗口返回时固定返回。 | 该时间窗口内出现的不同 IP 数量。 |
activity.5m.distinct_countries |
对应时间窗口返回时固定返回。 | 该时间窗口内出现的不同国家或地区数量。 |
activity.1h |
activity 返回时固定返回,包含本次访问。 |
当前设备最近 1 小时的访问活动。 |
activity.1h.events |
对应时间窗口返回时固定返回。 | 该时间窗口内的访问次数。 |
activity.1h.distinct_ips |
对应时间窗口返回时固定返回。 | 该时间窗口内出现的不同 IP 数量。 |
activity.1h.distinct_countries |
对应时间窗口返回时固定返回。 | 该时间窗口内出现的不同国家或地区数量。 |
activity.24h |
activity 返回时固定返回,包含本次访问。 |
当前设备最近 24 小时的访问活动。 |
activity.24h.events |
对应时间窗口返回时固定返回。 | 该时间窗口内的访问次数。 |
activity.24h.distinct_ips |
对应时间窗口返回时固定返回。 | 该时间窗口内出现的不同 IP 数量。 |
activity.24h.distinct_countries |
对应时间窗口返回时固定返回。 | 该时间窗口内出现的不同国家或地区数量。 |
account_map |
可选;已有快照可读或关联操作返回快照时提供。 | 当前 Imprint 在 Workspace 内的账号关系快照。 |
网络有限值说明
| 字段 | 值 | 标题 | 含义 | 接入方业务处理示例 |
|---|---|---|---|---|
network.ip_consistency |
MATCH |
IP 信息一致 | 网络 IP 信息一致。 | |
network.ip_consistency |
MISMATCH |
IP 信息不一致 | 网络 IP 信息不一致。 | |
network.ip_consistency |
UNKNOWN |
暂无明确结果 | 当前没有形成明确的一致性结果。 | |
network.proxy_detected |
true |
检测到风险 | 检测到代理或 VPN 风险。 | |
network.proxy_detected |
false |
未检测到风险 | 未检测到代理或 VPN 风险。 | |
network.connection_type |
residential |
住宅网络 | 住宅宽带网络。 | |
network.connection_type |
mobile |
移动网络 | 移动运营商网络。 | |
network.connection_type |
corporate |
企业网络 | 企业或机构网络。 | |
network.connection_type |
hosting |
托管网络 | 云服务、服务器托管或数据中心网络。 | |
network.connection_type |
proxy |
中转网络 | 代理、VPN 或类似中转网络。 | |
network.connection_type |
unknown |
未分类 | 暂未归入明确网络类型。 |
History
Lite 与 Pro 均支持 History 指定时间范围查询,与基础 Report 中的连续性摘要分别表达。
{
"imprint": "imp_33333333333333333333333333333333",
"range": {
"from": "2026-03-01",
"to": "2026-03-18",
"days": 18
},
"summary": {
"events": 12,
"truncated": false,
"firstSeenAt": "2026-03-01T08:10:00+09:00",
"lastSeenAt": "2026-03-18T21:34:00+09:00"
},
"timeline": [
{
"date": "2026-03-01",
"count": 2
},
{
"date": "2026-03-18",
"count": 3
}
],
"recent": [
{
"at": "2026-03-18T21:34:00+09:00",
"surface": "login"
}
]
}
days 与 from/to 互斥;from 和 to 必须一起提供,格式固定为 YYYY-MM-DD。
History 字段说明
| 字段 | 含义 |
|---|---|
imprint |
当前 History 查询对应的检测报告编号 |
range |
本次 History 查询采用的时间范围 |
range.from |
查询范围的开始日期 |
range.to |
查询范围的结束日期 |
range.days |
查询范围覆盖的自然日数量 |
summary |
当前查询范围的聚合摘要 |
summary.events |
查询范围内的访问事件总数 |
summary.truncated |
结果是否因为返回上限而被截断 |
summary.firstSeenAt |
查询范围内最早事件的时间 |
summary.lastSeenAt |
查询范围内最近事件的时间 |
timeline |
按日期聚合的访问次数序列 |
timeline.date |
时间线条目的自然日期 |
timeline.count |
该日期的访问事件数量 |
recent |
最近访问事件列表 |
recent.at |
最近事件发生时间 |
recent.surface |
客户传入的业务场景标识 |
线上数据删除
按 Workspace、Browser Environment、设备或账号删除当前线上检测数据。操作异步执行,通过请求回执查询状态。
使用有效的 Workspace Server API Key,在服务端通过 X-API-Key 调用。该 Workspace 的普通服务端 Key 均具备删除能力;Browser Environment ID、Console Cookie、Agent Trial 凭据和 MCP 授权不适用。Workspace 由 Key 确定,请求体不接受 workspace_id。当前提供 HTTP API,服务端 SDK 和 Console 尚无对应删除入口。
选择删除范围
scope |
选择条件 | 删除范围 |
|---|---|---|
workspace |
无附加选择条件 | 当前 Workspace 的全部检测历史及关联数据,包括匿名访问、Account Map 关联和未记录 Environment 的访问。 |
environment |
必填 environment_id |
指定 Browser Environment 的全部检测历史及关联数据,包括匿名访问,不受 Console 显示日期范围限制。 |
device |
必填 device_id;可选 environment_id |
指定设备在所选范围内的访问及关联数据,包括该设备上所有账号产生的访问。 |
account |
必填 account_ref;可选 environment_id |
明确绑定到该 Account Map 引用的访问及关联数据,不根据相同设备或 IP 扩展到未绑定访问。 |
environment_id 使用 Console 网站接入中的 env_...,已停用的 Environment 也可清理。device_id 使用 Report 的 device.id;account_ref 使用绑定 Report 时传入的同一个稳定内部用户 ID。设备和账号请求省略 environment_id 时,覆盖该目标在整个 Workspace 的匹配历史。
删除包含所选访问直接关联的检测记录、Account Map 关联及可确认归属的设备数据。其他 Workspace 的数据保持不变。存在共享设备数据或归属无法确认时,整个请求进入 review_required,不执行部分删除。
提交删除请求
POST /api/v1/data-deletions 返回 HTTP 202 和请求回执。HTTP 202 表示已受理,完成状态以回执的 status 为准。
Browser Environment 范围:
POST https://api.echoscan.org/api/v1/data-deletions
X-API-Key: <your_server_key>
Content-Type: application/json
Accept: application/json
{
"request_id": "0123456789abcdef0123456789abcdef",
"scope": "environment",
"environment_id": "env_0123456789abcdef0123456789abcdef",
"confirmation": "DELETE_LIVE_DATA"
}
| 字段 | 规则 |
|---|---|
request_id |
必填。每个新操作生成随机的 32 位小写十六进制 ID,发送前持久保存;重试复用原 ID。示例 ID 不用于实际请求。 |
scope |
必填。workspace、environment、device 或 account。 |
confirmation |
必填,固定为 DELETE_LIVE_DATA。 |
environment_id |
environment 范围必填;device、account 可选;workspace 必须省略。 |
device_id |
仅 device 范围使用,不能与 account_ref 同时传入。 |
account_ref |
仅 account 范围使用,最长 256 字节,无控制字符或首尾空白。 |
请求体最多 4096 字节。不适用的字段必须省略;null、重复字段和未声明字段均不接受。
Workspace 范围:
POST https://api.echoscan.org/api/v1/data-deletions
X-API-Key: <your_server_key>
Content-Type: application/json
Accept: application/json
{
"request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"scope": "workspace",
"confirmation": "DELETE_LIVE_DATA"
}
设备范围,限定 Browser Environment:
POST https://api.echoscan.org/api/v1/data-deletions
X-API-Key: <your_server_key>
Content-Type: application/json
Accept: application/json
{
"request_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"scope": "device",
"device_id": "did_example_device_1",
"environment_id": "env_0123456789abcdef0123456789abcdef",
"confirmation": "DELETE_LIVE_DATA"
}
账号范围,限定 Browser Environment:
POST https://api.echoscan.org/api/v1/data-deletions
X-API-Key: <your_server_key>
Content-Type: application/json
Accept: application/json
{
"request_id": "cccccccccccccccccccccccccccccccc",
"scope": "account",
"account_ref": "EXAMPLE-customer-42",
"environment_id": "env_0123456789abcdef0123456789abcdef",
"confirmation": "DELETE_LIVE_DATA"
}
受理回执:
{
"request_id": "0123456789abcdef0123456789abcdef",
"scope": "environment",
"environment_id": "env_0123456789abcdef0123456789abcdef",
"status": "queued",
"received_at": "2026-11-05T00:00:00Z",
"copies_status": "not_verified"
}
查询处理结果
GET /api/v1/data-deletions/{requestId} 返回 HTTP 200 和当前回执。使用同一 Workspace 当前有效的 Server API Key,requestId 为提交时保存的 request_id。
GET https://api.echoscan.org/api/v1/data-deletions/0123456789abcdef0123456789abcdef
X-API-Key: <your_server_key>
Accept: application/json
HTTP 200 表示回执读取成功,删除结果由 status 表示。建议间隔数秒轮询并设置超时;超时后保留原请求 ID,用于继续查询或支持核查。
完成回执:
{
"request_id": "0123456789abcdef0123456789abcdef",
"scope": "environment",
"environment_id": "env_0123456789abcdef0123456789abcdef",
"status": "live_data_deleted",
"received_at": "2026-11-05T00:00:00Z",
"finished_at": "2026-11-05T00:00:01Z",
"copies_status": "not_verified",
"deleted": {
"visit_events": 12,
"devices": 3
}
}
删除回执字段
| 字段 | 含义 |
|---|---|
request_id |
删除请求 ID。 |
scope |
提交的删除范围。 |
environment_id |
请求指定 Browser Environment 时返回。 |
status |
当前处理状态。 |
received_at |
请求接收时间。 |
finished_at |
处理结束时间,可选字段。 |
copies_status |
固定为 not_verified,表示副本删除未由此接口验证。 |
reason |
审核或执行失败的原因代码。 |
deleted |
成功后按数据类别返回的删除数量。目标不存在时可以为零;各类别数量不等同于访客数。 |
状态与审核
status |
含义与处理 |
|---|---|
queued |
已受理或正在执行,继续查询。 |
live_data_deleted |
所选范围的当前线上检测数据已删除。 |
review_required |
未执行删除,携请求 ID 和 reason 联系支持。 |
failed |
删除未提交,处理已结束;核查原因后再决定是否创建新请求。 |
审核原因包括 shared_device_data(共享设备数据)、account_key_version_review(Account Map 关联需要核查)和 ownership_review(数据归属需要核查)。执行失败使用 execution_failed。review_required 与 failed 均为终态,不自动重试。
重试与异常处理
相同 request_id 和相同目标重复 POST 返回原回执,不再次执行删除。更改范围或选择条件返回冲突;GET 查询原回执无需重新提交目标。
HTTP / error.code |
处理方式 |
|---|---|
400 / invalid_deletion_request |
核对范围、字段和确认值。 |
401 / invalid_api_key |
检查服务端 Key 是否有效。 |
403 / data_deletion_forbidden |
使用该 Workspace 的普通服务端 Key。 |
404 / data_deletion_environment_not_found |
核对 Environment 是否属于该 Key 的 Workspace。 |
404 / data_deletion_not_found |
当前 Workspace 内未查到回执,不代表删除成功。 |
409 / data_deletion_conflict |
核对原请求 ID 和目标,GET 查询原结果。 |
429 / data_deletion_busy |
遵守 Retry-After: 10;每个 Workspace 最多一个排队请求,新请求至少间隔 10 秒。 |
429 / key_rate_limited |
降低请求与轮询频率。 |
500 / data_deletion_failed |
结果未确认,使用原 ID 和原选择条件重试。 |
503 / data_deletion_unavailable |
暂时不可用,保留原请求 ID。 |
响应丢失时复用原 ID。若 POST 持续返回 503 且 GET 返回 404,停止自动重试,携原请求 ID 联系支持;不要通过新 ID 绕过未确认结果。提交和查询不消耗检测/Report 额度,普通请求限速仍适用。
数据保留边界
Workspace、登录账号、成员、Browser Environment 配置、API Key、安装配置、OAuth 授权、账单及真实用量保持不变。账号注销和完整接入 Reset 分别处理。
live_data_deleted 仅确认当前线上检测数据。备份、导出、日志及研究副本不在此回执的确认范围内,copies_status 保持 not_verified;涉及副本的删除请求需由支持流程单独处理,该状态不授权继续使用副本。
删除不阻止后续采集。清理测试批次前暂停采集,等待 live_data_deleted 后使用新的 SDK 提交 ID 开始下一批。需停止后续采集时,停用对应 Browser Environment 或停止集成调用。
服务端决策
使用 risk.status、risk.reasons 和 risk.findings 作为报告输入,并结合账号、交易和业务上下文。网络详情与 Activity 属于正式 Report;History 是两个套餐均支持的独立时间范围查询。
if (report.risk.status === 'UNAVAILABLE') {
return Response.json({ error: 'verification_unavailable' }, { status: 503 })
}
const decision =
report.risk.status === 'PASS'
? 'allow'
: 'challenge'
return Response.json({ decision })
这个 allow / challenge 分支只是客户自己的示例策略;EchoScan Report API v1 不返回 recommended_action。
完整 Report 应留在服务端,只向浏览器返回业务所需的最小结果。
错误结构
{
"error": {
"code": "invalid_api_key",
"message": "API key is invalid"
}
}
建议业务逻辑优先基于 error.code 判断,error.message 仅用于展示。
错误字段说明
| 字段 | 含义 |
|---|---|
error |
公开错误对象 |
error.code |
供服务端业务逻辑稳定判断的机器错误码 |
error.message |
适合日志或界面展示的错误说明 |
失败阶段与恢复
SDK 3.0 统一使用 EchoScanError.category,并提供 SDK 版本与最小关联回执。不要解析 message,也不要继续导入旧的详细 Error/Transport 类。
| 类别 | 含义与处理 |
|---|---|
configuration、run_in_progress、disposed |
修正配置或生命周期;本次被拒绝的调用没有新的 API 回执。 |
detection_failed |
采集没有可提交结果,业务动作保持待处理或拒绝。 |
cancelled、timeout |
服务端可能已经受理请求,按回执状态排查;失败不代表 PASS。 |
submission_failed |
传输、API 响应或校验失败;提供安全关联回执和类别,不伪造 Report。 |
公钥 GET 会按规则重试临时失败;Browser Verifier 不自动重试 Submit POST。再次 run() 是新采集/提交,不保证复用上次提交 ID。被识别的同 ID 重复 Submit 不重复计量,但新的 run 可能消耗新的用量。
直接 HTTP 调用先检查状态,再读取成功合同。根据 envelope 的 error.code 判断,网络/代理失败不一定返回 JSON。invalid_api_key、origin_not_allowed、quota_exceeded、key_rate_limited 分别需要检查凭据、Origin、额度或退避。HTTP 成功仍需校验 Report 合同及同次 Imprint。
**HTTP 503 quota_state_unavailable。**服务端无法可靠读取或确认当前配额用量状态,因此在受保护操作执行前停止。这是暂时的服务可用性失败,与 HTTP 429 quota_exceeded 不同,不能据此认定额度已耗尽。应显示验证暂不可用,不能返回 PASS。服务恢复后,在已配置的次数上限内重试 GET,避免持续轮询;更换 Key 或升级套餐不能修复配额状态不可用。持续失败时,保留 HTTP 状态、公开错误码和请求标识供支持排查,不记录 Key 或完整 Report。写操作的响应不确定时,先核对已有快照再决定是否重发。
服务端 SDK 在 0.2.3 和 0.2.4 中另行归一化:Node 使用 EchoScanApiError.code/serverCode;Go 使用 APIError.Code/ServerCode;Python 使用 ApiError.code/server_code;Rust 使用 ApiError.code/server_code。此 503 的归一化 code 为 upstream_unavailable,服务端错误码字段保留 quota_state_unavailable。HTTP 状态、请求标识和重试字段依照各语言公开类型,请对照已安装版本的公开 SDK 类型。
服务端 SDK 的 GET 按配置上限重试临时失败。每次进入计量环节的重试或重复 Report GET 都可能消耗服务端用量,包括业务处理错误;在此之前被认证、速率或配额拒绝的请求不计。Account Map POST 是写操作,不应盲目重试或在响应不确定时更换账号引用。上述失败不能落入 PASS 分支。
连接 AI 与 EchoScan MCP
通过标准 MCP,让 Codex、Claude Code、VS Code 或其他兼容客户端直接使用 EchoScan。客户端首次需要访问时会打开浏览器完成 Workspace 授权。
让你的 AI 连接 EchoScan
只需添加一次这个远程地址。需要访问时,客户端会打开 EchoScan 授权;不用把 API Key 填进 AI 客户端。
https://api.echoscan.org/mcp
查看接入方式
在终端执行一次,然后在浏览器中确认 EchoScan Workspace 与权限。
codex mcp add echoscan --url https://api.echoscan.org/mcp
在终端执行一次;Claude Code 首次连接时会发起浏览器授权。
claude mcp add --transport http echoscan https://api.echoscan.org/mcp
把这段服务器配置加入 MCP 配置;VS Code 首次使用时会请求授权。
{
"servers": {
"echoscan": {
"type": "http",
"url": "https://api.echoscan.org/mcp"
}
}
}
按下面的信息创建远程 Streamable HTTP Server。支持 MCP OAuth 的客户端会自动发现授权流程。
Name: EchoScan
Transport: Streamable HTTP
URL: https://api.echoscan.org/mcp
Authentication: OAuth 2.1 + PKCE
主要工具(摘要)为 echoscan_get_report、echoscan_get_history、echoscan_get_usage。完整工具、OAuth scope、启用条件与调用权限见MCP 指南。摘要不代表完整工具清单。
如果 AI 还需要修改 EchoScan 接入代码,请先让它读取:
https://echoscan.org/llms.txthttps://echoscan.org/docs/ai-context.md
这些文件与本页面共同依据同一份公开 API 合同维护,并通过一致性校验。MCP 授权不需要、也不会要求你把 EchoScan API Key 填进 AI 客户端。
CLI 接入
通过命令行创建 EchoScan 接入配置,并检查连接状态。需要 Node.js 22.22 或更新的 22.x 版本。在项目目录运行:
npx @echoscan/cli init --origin https://example.com
在浏览器中授权 EchoScan CLI,并选择 Workspace。创建配置后,按推荐接入流程完成浏览器 SDK 和服务端接入,再检查连接:
npx @echoscan/cli status
npx @echoscan/cli verify
status 查看当前连接状态;verify 在浏览器和服务端均已连接时返回成功。CLI 创建接入配置,不会安装 SDK 或修改业务代码。