EchoScan API 与接入指南
EchoScan 面向敏感访问和业务动作提供设备身份与访问风险结果。Browser Verifier 提交检测并返回服务端签发的 imprint;服务端使用 secret API key 查询 Report API v1。
推荐接入流程
- 创建 Browser Environment,并配置精确 Allowed Origins。
- 把公开
environmentId复制进 Browser Verifier,得到{ imprint }。 - 把
imprint随受保护业务动作发送到你的服务端。 - 独立创建服务端 Secret API Key,不把它与 Environment 当成一个对象。
- Secret 只保存到后端,并按 Imprint 查询统一 Report Endpoint。
- 读取
risk.status,Pro 还可读取risk.reasons;只向浏览器返回客户自己的最小业务决策。
浏览器代码和服务端 route 可以位于同一仓库。安全边界取决于运行位置:environmentId 可以公开,API key 只能存在于服务端。
浏览器生成 imprint
Direct 模式必须配置固定格式 env_<32 lowercase hex> 的 environmentId。Browser Verifier 只在指纹 Submit 时使用它,不接受 Workspace ID,也不会把 API key 或 Authorization 放进浏览器。
npm install @echoscan/browser-verifier
import { createEchoScan } from '@echoscan/browser-verifier'
const sdk = createEchoScan({
environmentId: 'env_0123456789abcdef0123456789abcdef'
})
const { imprint } = await sdk.run()
<script type="module">
import { createEchoScan } from 'https://cdn.echoscan.org/v1/echoscan.esm.js'
const sdk = createEchoScan({
environmentId: 'env_0123456789abcdef0123456789abcdef'
})
const { imprint } = await sdk.run()
</script>
<script src="https://cdn.echoscan.org/v1/echoscan.umd.js"></script>
<script>
const sdk = window.EchoScan.createEchoScan({
environmentId: 'env_0123456789abcdef0123456789abcdef'
})
sdk.run().then(({ imprint }) => {
console.log(imprint)
})
</script>
run() 只返回 { imprint }。新 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。
发送到服务端
按你的业务 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。
Lite 接入
Lite 使用统一 Report Endpoint。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。
npm install @echoscan/echoscan
import { createLiteClient } from '@echoscan/echoscan'
const echoscan = createLiteClient({
apiKey: process.env.ECHOSCAN_LITE_KEY
})
const report = await echoscan.getReport(imprint)
console.log(report.risk.status)
go get github.com/echoscan/echoscan-go@latest
package main
import (
"context"
"log"
"os"
echoscan "github.com/echoscan/echoscan-go"
)
func main() {
imprint := "imp_0123456789abcdef0123456789abcdef"
echoscanClient, err := echoscan.NewLiteClient(os.Getenv("ECHOSCAN_LITE_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
import os
from echoscan import EchoScanLiteClient
imprint = "imp_0123456789abcdef0123456789abcdef"
echoscan_client = EchoScanLiteClient(os.environ["ECHOSCAN_LITE_KEY"])
report = echoscan_client.get_report(imprint)
print(report["risk"]["status"])
[dependencies]
echoscan = "0.2.1"
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_LITE_KEY")?;
let echoscan = LiteClient::new(&api_key)?;
let report = echoscan.get_report(imprint).await?;
println!("{}", report["risk"]["status"]);
Ok(())
}
Lite report
Lite 返回最小公开设备身份与风险契约,不包含产品 Reasons、近期 Activity、设备时间和 Pro 网络详情。
正常访问示例:
{
"schema_version": "1.0",
"imprint": "imp_0123456789abcdef0123456789abcdef",
"created_at": "2026-07-28T01:30:00Z",
"device": {
"id": "did_A12B34C56D78",
"seen_before": true,
"access_count": 8
},
"risk": {
"status": "PASS"
},
"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"
}
}
可疑访问示例:
{
"schema_version": "1.0",
"imprint": "imp_11111111111111111111111111111111",
"created_at": "2026-07-28T01:35:00Z",
"device": {
"id": "did_C98D76E54F32",
"seen_before": false,
"access_count": 1
},
"risk": {
"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"
}
}
复访设备示例:
{
"schema_version": "1.0",
"imprint": "imp_22222222222222222222222222222222",
"created_at": "2026-07-28T02:00:00Z",
"device": {
"id": "did_A12B34C56D78",
"seen_before": true,
"access_count": 8
},
"risk": {
"status": "PASS"
},
"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"
}
}
字段说明
| 字段 | 含义 |
|---|---|
schema_version |
公开报告契约版本 |
imprint |
本次检测的服务端正式报告编号 |
created_at |
报告创建完成时间 |
device.id |
已认证 Workspace 内的设备 ID |
device.seen_before |
本次之前是否见过该设备 |
device.access_count |
包含本次在内的设备访问次数 |
risk.status |
本次访问的总体商业风险状态 |
browser |
最终浏览器状态、名称和版本 |
operating_system |
最终操作系统状态、名称和版本 |
network.status |
参与总体 Risk 的网络状态 |
network.observed_ip |
服务端观察到的请求出口 IP |
network.country_code |
Observed IP 对应国家或地区代码 |
状态说明
| status | 含义 |
|---|---|
PASS |
所有商业 Finding 均为 PASS |
SUSPICIOUS |
至少一个 Finding 为 SUSPICIOUS,且没有 DECEPTIVE |
DECEPTIVE |
至少一个 Finding 为 DECEPTIVE |
真实关键数据缺失时,总体结果不会是 PASS。
Pro 接入
Pro 与 Lite 使用相同的浏览器 Imprint、服务端 Secret 边界和统一 Endpoint。客户升级套餐无需更换 Endpoint;Enterprise 当前返回 Pro 契约。
查询 report
GET https://api.echoscan.org/api/v1/fingerprint/report/{imprint}
X-API-Key: <your_pro_key>
Accept: application/json
History 只允许 Pro 和 Enterprise:
GET https://api.echoscan.org/api/v1/fingerprint/imprint/{imprint}/history?days=7&recent=20
X-API-Key: <your_pro_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_pro_key>
Accept: application/json
npm install @echoscan/echoscan
import { createProClient } from '@echoscan/echoscan'
const echoscan = createProClient({
apiKey: process.env.ECHOSCAN_PRO_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@latest
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_PRO_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=%v history=%v", report["risk"], history["summary"])
}
pip install echoscan
import os
from echoscan import EchoScanProClient
imprint = "imp_0123456789abcdef0123456789abcdef"
echoscan_client = EchoScanProClient(os.environ["ECHOSCAN_PRO_KEY"])
report = echoscan_client.get_report(imprint)
history = echoscan_client.get_history(imprint, days=7)
print(report["risk"]["status"], report["risk"]["reasons"], history["summary"])
[dependencies]
echoscan = "0.2.1"
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_PRO_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"], history["summary"]);
Ok(())
}
Pro report
Pro 是 Lite 的严格超集,增加设备连续性时间、产品级 Reasons、可选网络详情和近期 Activity。
{
"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"
]
},
"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,
"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
}
}
}
Pro 的 PASS Report 仍会明确序列化空 Reasons 数组:
{
"risk": {
"status": "PASS",
"reasons": []
}
}
无法可靠获得的可选字段会省略。首次访问时 device.previous_seen_at 为 null。只有识别到另一公网 IP 时才返回 network.alternate_ip。network.ip_consistency 取值为 MATCH、MISMATCH 或 UNKNOWN;network.connection_type 取值为 residential、mobile、corporate、hosting、proxy 或 unknown。
产品级风险原因
| Reason | 含义 |
|---|---|
BROWSER_VERSION_MISMATCH |
浏览器版本信息存在不一致 |
BROWSER_IDENTITY_MISMATCH |
浏览器整体身份信息存在不一致 |
OS_ENVIRONMENT_MISMATCH |
操作系统环境信息存在不一致 |
AUTOMATION_DETECTED |
检测到自动化访问 |
ENVIRONMENT_INCONSISTENT |
当前设备或浏览器环境信息整体不一致 |
PROXY_DETECTED |
检测到代理或 VPN 风险 |
HOSTING_NETWORK_DETECTED |
当前访问来自托管服务或数据中心网络 |
NETWORK_INCONSISTENT |
当前访问的网络身份信息存在不一致 |
LOCATION_INCONSISTENT |
当前访问的位置相关信息存在不一致 |
SIGNAL_DATA_INCOMPLETE |
本次检测缺少形成完整结果所需的关键数据 |
Reason 表达风险类别,不表达严重程度;严重程度只由 risk.status 表达。
Pro 增量字段
| 字段 | 含义 |
|---|---|
device.first_seen_at |
当前 Workspace 首次见到设备的时间 |
device.previous_seen_at |
本次报告之前最近一次访问时间 |
risk.reasons |
稳定、去重的产品级风险类别 |
network.alternate_ip |
EchoScan 识别到的另一公网 IP |
network.ip_consistency |
公网 IP 一致性结果 |
network.proxy_detected |
代理或 VPN 风险标识 |
network.location |
可选国家、地区、城市和时区 |
network.provider |
与 observed_ip 对应的可选 Provider |
network.connection_type |
稳定连接类型机器值 |
network.asn |
与 observed_ip 对应的可选 ASN |
activity.5m、activity.1h、activity.24h |
包含本次访问的事件数、不同 IP 数和不同国家数 |
Pro history
History API 只允许 Pro 和 Enterprise。
{
"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。
服务端决策
使用 risk.status 作为主要报告输入,并结合账号、交易和业务上下文。Pro 还可以使用 risk.reasons、网络详情、Activity 和 History。
const decision =
report.risk.status === 'PASS'
? 'allow'
: 'challenge'
return Response.json({ decision })
这个 allow / challenge 分支只是客户自己的示例策略;EchoScan Report API v1 不返回 recommended_action。
完整 Report 应留在服务端,只向浏览器返回业务所需的最小结果。
错误结构
{
"error": {
"code": "auth_failed",
"message": "Authentication failed"
}
}
建议业务逻辑优先基于 error.code 判断,error.message 仅用于展示。
AI 编码工具接入
让编码 Agent 修改接入前先读取:
https://echoscan.org/llms.txthttps://echoscan.org/docs/ai-context.md
这些文件与本页面来自同一份 Report API v1 事实源。