API 接口文档

给一个 IP 地址,拿到它此刻的风险判断与网络、地理归属。两个接口共用同一套认证、配额与错误格式,响应一律是 JSON,同一个地址在两边只有一个分。

快速开始

不需要注册,也没有 Key 的前置条件,直接调用即可。返回 JSON;响应里的地名、网络类型、风险类型一律是标准码或稳定 slug,不含中文,方便贵方直接入库与统计,不必在解析层处理中文编码。

curl https://ip99.com/v1/ip/8.8.8.8

加上自助创建的 Key,额度按账户计:

curl -H "X-API-Key: 你的KEY" https://ip99.com/v1/ip/8.8.8.8

分数是什么,不是什么

risk.score(0–100)衡量的是可核验风险证据的新鲜度与强度,随证据变旧而衰减。它不是「这个地址是攻击者」的概率。

把 0–100 的分直接当概率用的接入方,会在阈值上做出完全错误的取舍——这条口径写在文档最前面,也随每条响应一起下发(score_semantics)。

与传统 IP 情报库最大的不同:我们给的是一个会过期的判断,不是一个长期有效的标签。同一个地址,上周是秒拨池,今天还是不是,取决于证据距今多久。所以同一个 IP 隔几小时查两次分数不同是正常现象,不是数据抖动。

字段含义
risk.score0–100。可核验证据的新鲜度与强度,不是攻击概率。
risk.levelnone(0)/ low(1–29)/ medium(30–69)/ high(70–100),由分数推出。none 的意思是「我方没有当前可核验的证据」,不等于安全。
evidence_stateactive 有仍在计分的新鲜证据;stale 见过但证据已过时效窗、分数为 0,可作弱特征;none 我方从未见过。策略平台请按这个字段分流,不要只看分数。
tag_details[].last_seen该类证据最近一次捕获的时刻。标签可能仍在而分数已衰减到 0,那表示证据过期,不表示该地址干净。
score_semantics固定值 capture_evidence_freshness。随每条响应下发,防止接入方把数字悄悄当成概率用。
computed_at本次评分针对的时刻。分随证据变旧而衰减,所以「算的哪一刻」是可核验的一部分,建议一并入库。

两个接口的关系

Pulse 是基础画像的升级版。同一个分、同一份证据底座,但把精度与可回溯性各提高一档:基础画像回答「此刻是什么」,Pulse 还能回答「当时为什么这么判」,并说清这类证据有多硬——观测了多久、几个独立来源、属于哪个资源池。

维度GET /v1/ip/{ip}GET /pulse/v1/ip/{ip}
风险分数risk.scorescore,与基础画像同源同值
风险类型risk.tags[],附每类的最近捕获时刻 tag_details[].last_seenevidence[].risk_type,附粗粒度新鲜度桶 freshnesswithin_2h / within_12h / older
风险等级risk.level不返回,按贵方自己的阈值对 score 分档
布尔信号signals.*不返回
网络与地理归属network + geo专业版及以上返回,与基础画像同一套形状
证据有多硬不提供专业版:单类型分 score、观测天数 obs_days、独立来源数 sources、资源池匿名句柄 channels / risk_clusters;签数据处理协议后再加秒级捕获时刻 captured_at、精确年龄 age_min 与近 30 天命中次数 hits_30d
历史回溯不支持,只答此刻支持 ?at=,可回溯 30 天(专业版及以上)
版本戳meta.data_version数据版本。契约版本与出分曲线版本属于内部实现,2026-09-11 起不再外发。
典型用途实时风控、注册登录、下单前校验——一次调用把要用的字段拿全事后复盘、对账仲裁、策略回溯——需要「当时为什么这么判」

同一个 IP,本产品只有一个分。两个接口的分数同源同值,不存在「画像接口一个分、Pulse 另一个分」。差别在证据的粒度与可回溯性,不在结论。

档位

Pulse 是专业版能力。公开档只给分数与证据类型;单类型分、观测天数、独立来源数、资源池匿名句柄、地址归属,以及 ?at= 的历史回溯,都只对专业版及以上开放;秒级捕获时刻、精确年龄与近 30 天命中次数另需签署数据处理协议。

分级口径是按个人信息法律定性分级,不按付费能力分级:越高档位交付的不是「更多条目」,而是更精确的时间与来源刻画,那正是需要合同与审计才能外发的部分。厂商来源编码任何档位都不外发。

档位能拿到什么
公开档分数、逐条证据的类型与粗粒度新鲜度桶、calibrated 标记、评分时刻与曲线版本。
专业版在上面「两个接口的关系」表里「证据有多硬」与「网络与地理归属」两行的全部字段,并可用 ?at= 回溯历史时刻。
专业版 + 数据处理协议再加秒级捕获时刻 captured_at、精确年龄 age_min、近 30 天命中次数 hits_30d

未达档位时的表现是明确的,不会静默少给:传 ?at= 返回 403 history_requires_professional;已到专业版但缺数据处理协议时,响应带 professional_degraded: "dpa_required"——告诉你字段是被什么扣下的,而不是让它们凭空消失。

开通专业版或签订数据处理协议 →

基础画像:GET /v1/ip/{ip}

GEThttps://ip99.com/v1/ip/{ip}

返回当前时刻的风险判断与归属。Key 可选:不带 Key 也能调用,带上则按账户额度计。

参数位置说明
ip路径合法的 IPv4 或 IPv6 地址。私有地址与保留地址返回 400 reserved_ip

响应示例

下面是一次真实调用(86.111.136.23,此刻判为高风险的代理出口)的原样返回:

{
  "ip": "86.111.136.23",
  "risk": {
    "score": 90,
    "level": "high",
    "tags": ["proxy"],
    "tag_details": [ { "tag": "proxy", "last_seen": "2026-09-10 13:41:22" } ]
  },
  "evidence_state": "active",
  "score_semantics": "capture_evidence_freshness",
  "computed_at": "2026-09-10T06:23:46Z",
  "signals": { "proxy": true, "vpn": false, "dialup_pool": false, "hijacked_proxy": false,
               "osint": false, "crawler": false, "cloud_service": false, "cloud_phone": false,
               "hosting": false, "mobile": false },
  "network": { "asn": 15547, "usage_type": "DYN" },
  "geo": { "continent": "EU", "country": "CH", "region": "Valais", "city": "Sitten",
           "lat": 46.22739, "lon": 7.35559, "tz": "Europe/Zurich" },
  "meta": { "provider": "threathunter", "data_version": "risk:20260910142326 geo:20260910",
            "query_ms": 0, "query_us": 225 },
  "source": "ip99.com",
  "source_url": "https://ip99.com/?ip=86.111.136.23"
}

字段字典

字段说明
risk.tags风险类型 slug 列表,如 proxydialup_poolvpnhijacked_proxycloud_phonecrawler。以稳定 slug 返回,可直接入库。
signals.*布尔信号。hosting / mobile 由网络场景派生,本身不是风险;其余与 tags 一一对应。
network.asn自治系统号。机器身份是 asn——运营商名不返回,拿它到公开注册库反查即可。
network.usage_typeIDC / DYN / MOB / GTW / EDU / GOV / CDN / ORG / DNS / NET / COM / BOGON
geo.continentAS / EU / NA / SA / AF / OC / AN
geo.countryISO 3166-1 alpha-2。
geo.subdivision / geo.admin_code中国的地址给出:subdivision 是 ISO 3166-2(如 CN-ZJ),admin_code 是 GB/T 2260 六位码,精确到区县
geo.region / geo.city中国以外的地址给出,拉丁名。另有 geo.lat / lon / tz
meta.query_us服务端查询耗时(微秒)。query_ms 同时保留,但本站查询通常在 1 毫秒以内,它基本恒为 0。
meta.data_version本次查询用到的风险库与地理库版本。做数据快照对账时以它为准。

Pulse:GET /pulse/v1/ip/{ip}

GEThttps://ip99.com/pulse/v1/ip/{ip}

基础画像的升级版:返回同源同值的分数,把证据逐条摊开,并可按历史时刻复现当时的判定。

参数位置说明
ip路径合法的 IPv4 或 IPv6 地址。
at查询串可选,RFC 3339 时刻,例如 2026-08-26T13:34:36Z可回溯 30 天,需专业版及以上;返回的 computed_at 就是你指定的那一刻。不传则答此刻。

两种被拒绝的情形不会静默回退到「今天的结果」:时刻写得不对或落在未来返回 invalid_time,超出可回溯窗口返回 at_out_of_window(响应体里带 window_from,告诉你最早能问到哪一刻,不必二分试探)。把「那一刻是什么分」答成今天的分,等于说了一句无法被发现的谎话,所以这里只有放行与明确报错两条路。

curl -H "X-API-Key: 你的KEY" \
  "https://ip99.com/pulse/v1/ip/8.8.8.8?at=2026-08-26T13:34:36Z"

响应示例(公开档,实时)

同一次真实调用里,Pulse 对同一个地址的返回:

{
  "ip": "86.111.136.23",
  "risk": {
    "score": 90,
    "level": "high",
    "tags": ["proxy"],
    "tag_details": [{ "tag": "proxy", "last_seen": "2026-09-10T14:20:31+08:00" }]
  },
  "share_tier": "T1",
  "evidence": [
    { "risk_type": "proxy", "freshness": "within_2h", "calibrated": false, "score": 90, "sources": 2 }
  ],
  "evidence_state": "active",
  "resource_pools": ["cl-9f8e7d6c5b"],
  "score_semantics": "capture_evidence_freshness",
  "computed_at": "2026-09-10T06:23:46Z",
  "source": "ip99.com",
  "source_url": "https://ip99.com/?ip=86.111.136.23"
}

专业版会在每条证据里补上「这条证据有多硬」的字段,形如:

{
  "risk_type": "hijacked_shared_proxy",
  "freshness": "within_2h",
  "calibrated": true,
  "score": 96,
  "obs_days": 9,
  "sources": 3
}

资源池的匿名句柄在响应顶层的 resource_pools(契约 v4 起;此前叫 evidence[].channels / risk_clusters)。

字段字典

字段档位说明
risk.score / level / tags / tag_details公开/v1/iprisk 同一套形状、同一个值、同一个曲线版本(契约 v4 起分数下沉到 risk{},顶层不再有 score)。
share_tier公开本条结果可披露的粒度层级。
evidence[].risk_type公开证据类型 slug,与基础画像的 tags 同源。
evidence[].freshness公开粗粒度新鲜度桶:within_2h / within_12h / older。公开档刻意不给秒级时间戳。
evidence[].calibrated公开该类型的曲线是否已针对样本校准。未校准时该条证据的权重由贵方自行判断——我们不下结论,只把状态交给你。
evidence[].inexact / synthesized / sources_unavailable / inferred_width公开证据诚实性标记,有意义时才出现。inexact 表示该条的时刻是下界近似;synthesized 表示这条证据本来就不带细分风险类型(如 IPv6 区间),不是「查了没查到」;inferred_width 大于 1 表示命中的是一个跨多个 /64 的推断区间,你查的这个地址不一定被直接观测到。
flags公开矛盾标记,例如证据与该地址的网络场景冲突时出现。出现才带。
degraded公开true 表示这次出分是降级结果(数据源故障时我们仍然出分)。没有它,客户无从区分「查过,干净」与「没查成」——所以它必须在响应体里,不能只挂在响应头上。
history专业版?at= 时出现:as_of(评分针对的时刻)、resolutioncoveragewindow_fromsettled_until(封板水位),以及数据空洞区间 gaps[]只给一个历史分而不说覆盖到什么程度,as-of 就只是一个不可核验的断言。
evidence[].score / obs_days / sources专业版单类型分、观测天数、独立来源数。sources_degraded 出现时表示来源数还没折叠完,是个偏高估的值。
resource_pools专业版资源池的匿名句柄(顶层数组;v4 前叫 evidence[].channels / risk_clusters)。同账户内稳定、跨账户不可对表——贵方拿到的是「这些 IP 属于同一个资源池」,不是那个资源池叫什么名字。
network / geo专业版地址归属,与基础画像同一套形状。加它是为了让「一批 IP 出一份带地区维度的报告」只跑一次接口,而不是分数跑 Pulse、归属再跑一遍 /v1/ip
evidence[].captured_at / age_min / hits_30d专业版 + 数据处理协议秒级捕获时刻、精确年龄(分钟)、近 30 天命中次数。这三个字段是「每次精确披露都有留痕」的对价,审计写不进去时会先停披露,并在响应里给出 professional_degraded

认证与额度

Key 通过请求头传递,两种写法等价:X-API-Key: 你的KEYAuthorization: Bearer 你的KEY

不要把 Key 放进 URL。查询串会被写进访问日志、浏览器历史与代理记录,而 Key 是长期凭据。检测到 ?key= 时请求会被直接拒绝(400 key_in_query),而不是「照常处理只是不推荐」——只提示不拦截,等于默认接受这种泄露方式。

成功响应与额度类 429 都带当日额度,不需要额外查询;分钟限速的 429(rate_limited)只带 X-RateLimit-*Retry-After

响应头含义
X-Daily-Limit当前档位的每日配额。
X-Daily-Remaining今日剩余次数。
X-Daily-Reset配额重置时间(Unix 秒,北京时间零点)。
X-RateLimit-Limit / X-RateLimit-Remaining每分钟速率上限与本分钟剩余。
Retry-After仅在 429 与 history_unavailable 时出现,距可重试的秒数。

网页查询与 API 共用同一份每日额度:未登录按公网 IP 计,登录后按账户计。当前档位与剩余额度可以用 GET /v1/me 查。

登录 / 注册并创建 API Key →

错误与重试

所有错误都是同一结构,便于统一处理。请按 error.code(稳定的小写标识)分支,不要按 error.message 判断——那是给人读的说明,措辞可能调整。

{ "error": { "code": "invalid_ip", "message": "不是合法的 IPv4/IPv6 地址:..." } }
HTTPcode含义
400invalid_ip不是合法的 IP 地址。
400reserved_ip私有或保留地址,没有公网风险画像。
400key_in_queryKey 出现在查询串里,请改用请求头。
401invalid_keyAPI Key 无效。
401revoked_keyAPI Key 已被吊销。
401key_expiredAPI Key 已过期。
404not_found接口路径不存在。
429rate_limited超出每分钟速率,稍后重试。
429quota_exceeded当日配额用尽,响应带 Retry-After
429quota_exceeded_anon未登录档配额用尽——登录并创建 Key 即提额。

?at= 的专用错误码

这几个码语义互不相同,请不要合并处理:看到码就知道该做什么——改参数、开通档位、还是稍后重试。

HTTPcode含义
400invalid_time不是 RFC 3339 时刻,或是未来时刻。
400at_out_of_window早于本部署能回答的最早时刻,响应体带 window_from
400history_not_enabled本部署没有装历史源——这是配置事实,不是故障。
400history_no_ipv6历史证据库只索引 IPv4,该地址族没有历史。
403history_requires_professional历史回溯需要专业版及以上。
503history_unavailable历史库瞬时不可读,带 Retry-After: 10,可重试。

其中「参数写错、超出窗口、本部署没装历史库」这几类不消耗额度:我方一眼就能判定,既没算出分数也没查一次库,让客户为此付额度说不通。

手机号风险画像:POST /v1/phone/lookup

第二个能力:一个手机号是不是猫池卡、账号卡、二次放号。仅企业客户,Key 需要单独开通这条能力, 额度也独立于 IP 查询——按号码个数计,不按请求次数。免费与注册账户调用会得到 401 / 403,不会消耗任何额度。 开通请走企业版

端点计费用途
POST /v1/phone/lookup1 个号码查一个号,请求体 {"number": "13800001234"}
POST /v1/phone/batch能解析的号码数查一批,请求体 {"numbers": [...]},单次最多 1000 个,结果与输入顺序一一对应;重复号按出现次数计费。
GET /v1/phone/meta/tags不计费标签字典:每个 slug 的英文名与解释(风险标签、卡属性、卡类型、证据状态)。
GET /v1/phone/meta/coverage不计费数据覆盖范围与更新节奏。

号码只走请求体,没有 GET /v1/phone/{号码} 这种形式:手机号是个人信息,放进 URL 会被各层访问日志原样记下。

curl -X POST https://ip99.com/v1/phone/lookup \
  -H "X-API-Key: 你的KEY" -H "Content-Type: application/json" \
  -d '{"number": "13800001234"}'

号码怎么写

常见分隔符(空格、-、括号、点)、全角数字、从表格或聊天窗口复制时夹带的零宽字符与不换行空格都会被正确归一; +86…0086… 是国际写法,013800001234 这种长途前缀也认。不带 + 的裸数字串按这个顺序读: 形如 1[3-9] 开头的 11 位大陆手机号按 +86;不是那个形状、又能拆出国家码且号码本体长度符合该国规划的 (如 12025550001)当作省略了 + 的国际号;其余才按 +86 补全。要精确就带 +

输入里出现字母、引号、emoji 等电话号码里不该有的字符时判为解析失败,不猜、不抠数字:单号查询返回 400 bad_number, 批量里对应那条带内嵌 error,两种都不计费。位数不对的号码(少打一位、座机)能解析、也确实查了库, 照常计费,返回里 query.valid_lengthfalsemiss_reasonunqueryable_bad_length——据此把「输入错了」和「查过了,干净」分开。

响应示例(命中)

{
  "query": {
    "input": "13800001234", "e164": "+8613800001234", "national_number": "13800001234",
    "country_code": 86, "country": {"iso2": "CN"}, "valid_length": true
  },
  "hit": true,
  "evidence_state": "active",
  "risk": {
    "score": 6, "level": "medium",
    "primary_tag": {"code": 2, "slug": "dormant"},
    "tags": [{"code": 2, "slug": "dormant"}]
  },
  "activity": {
    "first_seen": "2026-06-20T10:54:16Z", "last_active": "2026-06-20T10:54:16Z",
    "last_active_days_ago": 83, "observed_days": 0
  },
  "carrier": {
    "province": "CN-BJ", "city_code": "1101", "carrier": "CMCC",
    "attribute": {"code": 0, "slug": "base_carrier"},
    "card_type": {"code": 0, "slug": "standard"}
  },
  "sources": [{"library": "cn", "risk": 6, "tag": {"code": 2, "slug": "dormant"},
               "first_seen": "2026-06-20T10:54:16Z", "last_active": "2026-06-20T10:54:16Z"}],
  "meta": {"request_id": "472ef7d6e49a49fe",
           "data_version": {"cn": "20260911", "sg": "20260911", "cn_minute": "202609120339", "sg_minute": "202609120339"}}
}

未命中时只有 queryhit: falseevidence_state: "none"risk(0 分、空标签)、 miss_reasonmeta

字段字典

字段类型含义
query.inputstring你传进来的原文。
query.e164 / query.national_numberstring归一化后的国际写法与国内号码本体。
query.country_code / query.country.iso2int / string|null国家码与 ISO 3166-1 两位码。+1 号码的 iso2null(北美 25 个地区共用 +1)。
query.valid_lengthbool|null位数是否符合该国号码规划;号码规划未收录的国家为 null(不下结论)。
hitbool库里有没有这个号。false 不等于安全:库里只有已捕获的风险号码。
evidence_statestringactive 有新鲜证据,可直接采信;stale 见过但证据已过期(超 90 天未再捕获,号码可能已流回正常用户),建议降权;none 从未命中。
risk.score / risk.levelint 0–9 / string数据方的原始风险分,原样透出;levelhigh/medium/low/none)只是分档展示,不是处置建议
risk.primary_tag / risk.tags[]{code, slug}主标签与全部标签。slug 是稳定标识(如 sim_farmdormant),中英释义查 /v1/phone/meta/tags
activity.*RFC 3339 / int首次发现、最近活跃、距今天数、观测天数。同样是 9 分,昨天还活跃和三年前活跃过,处置价值完全不同。
carrierobject境内号码给 province(ISO 3166-2,如 CN-BJ)、city_code(GB/T 2260 行政区划码)、carrierCMCC/CUCC/CTCC/…),境外号码没有这三段;attribute(基础运营商 / 虚拟运营商 / 境外)与 card_type(普通卡 / 物联网卡 …)都是 {code, slug}。归属地残缺时缺的子字段直接不出现。
sources[]array命中来自哪个库(cn 境内 / sg 境外)以及各自的分数与时间。
miss_reasonstring只在 hit: false 时出现:not_in_risk_db 查过了库里没有;unqueryable_bad_length 位数不符,本次未命中不具参考性,应当校验输入而不是放行。
meta.request_id / meta.data_versionstring / object报障时带上 request_id;引用结果时保留 data_version

批量响应顶层是 count(输入个数)、billable(实际计费的号码数)、hit_count(命中条数)与 results[]; 每条结果的形状与单号查询相同,解析失败的那条只有 query.inputhit: false 与内嵌 error

额度与响应头

响应头含义
X-Billable-Units本次实际计费的号码数。非 200 的响应一律不计费;200 时按这个数结算,坏号不计。
X-Daily-Limit / X-Daily-Remaining / X-Daily-Reset这条能力自己的当日额度(单位是号码,见 X-Quota-Unit: phone_number),与 IP 查询的额度互不相干。
X-RateLimit-Limit / X-RateLimit-Remaining每分钟额度与本分钟剩余,同样按号码计。
X-Request-Id每次响应都有,报障时一并提供。

批量是全有或全无:剩余额度装不下整批时整批 429 quota_exceeded、不扣费,按 X-Daily-Remaining 缩小批次重试; 一批的号码数超过每分钟额度时返回 429 batch_over_rate_limit——这一批无论何时重试都不会过,要拆小。 GET /v1/mecapabilities.phone 里能看到 used_todayrefunded_todayremaining

错误码

HTTPcode含义
400bad_request请求体不是合法 JSON,或缺少 number / numbers,或字段类型不对。
400bad_number单号查询:号码格式无法解析(响应体仍带 query.input)。
401key_required没带 Key(匿名不能查手机号)。
401invalid_keyKey 无效或已吊销。
403capability_not_eligible账户档位不能开通这条能力。
403capability_not_enabled这把 Key 还没开通手机号能力。
413payload_too_large单次超过 1000 个号码,或请求体超过 1 MiB。
429rate_limited分钟限速,带 Retry-After
429quota_exceeded当日额度装不下这一批,整批不扣费;缩小批次重试。
429batch_over_rate_limit单批号码数超过分钟额度,这一批无论何时重试都不会过,要拆小。
502upstream_unavailable画像库暂时不可达,带 Retry-After: 5;本次不计费。
503phone_disabled能力未启用。
503quota_unavailable计费系统未就绪,稍后重试。

其他端点

端点用途
GET /v1/me当前档位与剩余额度。
POST /v1/phone/lookup手机号风险画像(猫池卡、账号卡、二次放号),独立能力、独立额度,按号码个数计费。见上一节
/mcpModel Context Protocol 端点(Streamable HTTP,POST),工具名 ip_risk_lookup无状态:不下发也不要求 Mcp-Session-Id,可以直接放在普通负载均衡后面。

/mcp 上的失败按 MCP 惯例是 HTTP 200 + isError: true(裸 429 会让客户端把服务器当成坏了)。每个 content[0].text 都是合法 JSON:成功与 GET /v1/ip/{ip} 同一份文档,失败与 REST 同一个错误体 {"error":{"code":…}},额度类还带 retry_after(秒)、resets_atnext_stepsignup_url / pricing_url,用光了直接照着引导登录或拿 Key。参数错误码 missing_ip / invalid_ip / reserved_ip / not_found,额度与凭据用上面「错误与重试」那张表的码;握手方法被限速时是 JSON-RPC error -32000,同一个对象在 error.data.error。登录用的端点是 https://ip99.com/mcp/account:客户端自己弹出登录页(OAuth,Claude Code / Claude.ai / ChatGPT 连接器 / Cursor 都认),不用抄 Key,额度按账户计;/mcp 仍免登录、按来访 IP 计。已授权的客户端在控制台「API Key」面板里能看到并一键断开;英文站用 /en/mcp/account,登录页与控制台随之是英文。字段与示例见英文版 /api#mcp

面向海外开发者与答案引擎的英文版是 ip99.com/api;给机器读的站点摘要见 /llms.txt