Signal 号码查询 API:校验注册与身份
Signal 是注重隐私的用户首选的即时通讯工具,用户群体也早已不局限于这一圈层。如果你的产品或研究涉及手机号,知道某个号码有没有在用 Signal,并且能解析出一个稳定的身份标识,就会直接影响消息的渠道选择和联系人补全。Signal 号码查询 API 通过普通的 HTTP 请求就能回答这两个问题。
本文介绍 Signal 的三个接口,演示如何用 Python 正确处理返回结果,并说明这类查询和平台上其他即时通讯检测之间的分工。
第一步:获取 API Key
注册后在控制台复制一个有效 Key。账户赠送免费点数,下面所有调用都可以直接测试。
第二步:校验注册状态
check 是最轻量的接口,接收国际格式的手机号 number,只返回号码本身、归一化后的 e164 格式和 registered。号码前面带不带 + 都可以,接口会统一处理成纯数字。
curl "https://api.opendata-api.com/api/project/signal/check?key=YOUR_API_KEY&number=15551234567"
registered 是三态值,把三种情况都处理到位,才是可靠流程和不稳定流程的区别:
true:该号码已注册 Signal 账号。false:该号码没有注册。null:暂时无法判断,通常是临时性问题(比如超时),message字段会说明原因。请把它当作「稍后重试」,而不是「没有 Signal」。
第三步:解析身份信息
user-info 对单个号码做完整查询。号码已注册时,它会返回账号的公开身份:ACI(账号标识,一个 UUID)和 PNI(手机号标识,通常写作 PNI:<uuid>),在可用时还会附带 identityKey、unrestrictedUnidentifiedAccess、capabilities、badges 等明文元数据。此外还有机器可读的状态(例如 found、not_found、timeout)、便于阅读的 message 以及耗时 durationMs。
curl "https://api.opendata-api.com/api/project/signal/user-info?key=YOUR_API_KEY&number=15551234567"
请留意 fieldsEncrypted 这个字段。它为 true 时,说明显示名称、个人简介和头像被 Signal 隐藏了,因为这些内容用只有对方联系人才持有的资料密钥做了端到端加密。这是 Signal 自身的隐私设计,并不是查询接口的缺陷,所以不要在产品里承诺能为陌生号码返回姓名或头像。
第四步:按 ACI 刷新元数据
如果你已经保存了某个 ACI,可以直接用 profile 获取它的公开资料元数据,不必再通过手机号重新解析。它只返回明文字段:identityKey、unidentifiedAccess、unrestrictedUnidentifiedAccess、capabilities、badges 以及 uuid(与 ACI 相同)。对非联系人,姓名、简介、头像在这条路径上永远不会出现。
curl "https://api.opendata-api.com/api/project/signal/profile?key=YOUR_API_KEY&aci=3bc3a597-297d-4f8c-821a-13aaa9ede5ea"
完整的 Python 示例
下面的函数先调用便宜的 check,只对未确定的结果做重试,并且只对确认已注册的号码调用 user-info:
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.opendata-api.com/api/project/signal"
def get(operation, **params):
params["key"] = API_KEY
resp = requests.get(f"{BASE}/{operation}", params=params, timeout=30)
resp.raise_for_status()
body = resp.json()
# A reply can be wrapped more than once as {"code": ..., "data": {...}}
while isinstance(body, dict) and "code" in body and isinstance(body.get("data"), (dict, list)):
body = body["data"]
return body
def lookup(number, retries=2):
for attempt in range(retries + 1):
data = get("check", number=number)
if data.get("registered") is True:
return {"number": number, "registered": True, "identity": get("user-info", number=number)}
if data.get("registered") is False:
return {"number": number, "registered": False}
time.sleep(2 * (attempt + 1)) # null: transient, back off and retry
return {"number": number, "registered": None, "reason": data.get("message")}
result = lookup("15551234567")
print(result)
if result["registered"]:
info = result["identity"]
print("names withheld by Signal:", info.get("fieldsEncrypted"))
只有三态的 registered 值参与流程控制,其余内容按返回原样保存,所以某个字段在某个账号上缺失时,也不会让循环中断。
Signal 与其他号码检测的分工
不同的即时通讯工具公开的信息量不同,该用哪个接口取决于你要做什么判断:
| 你想知道什么 | 适合的方案 |
|---|---|
| 这个号码有没有 Signal | Signal 的 check |
| 需要稳定的用户标识吗 | user-info(ACI/PNI) |
| 这个号码有没有 Viber | Viber 号码查询 |
| 有没有 Telegram,对应什么用户名 | Telegram 号码检测 |
| 多个渠道里该选哪个 | 线索核验工作流 |
由于 Signal 设计上不向非联系人公开资料,这类查询更适合当作注册状态和身份标识的信号,而不是用来补全个人资料的数据来源。
应用场景
- 渠道路由:号码确实已注册时,才把 Signal 作为联系方式提供给用户。
- 联系人补全:给现有手机号记录补充 Signal 在线状态和 ACI/PNI 标识。
- 身份合并:用 ACI 和 PNI 对记录去重并关联。
- 风控与信任信号:把注册状态和
capabilities与自己的反欺诈规则结合起来。 - 名单分层:按用户使用的加密通讯工具给受众分组。
最佳实践
- 先做号码归一化。 去掉空格和标点,带上国家区号,开头的
+可有可无。 - 先校验,再补全。 先对整份名单跑
check,只对registered = true的号码调用user-info,未注册的号码就不会触发较重的查询。 - 按号码缓存。 保存结果时带上时间戳,按计划周期复查,不要每次打开页面都查一遍。
- 不要把
null当成false。 暂时无法判断不等于结论,请放进重试队列。 - 保存 ACI,不要只存号码。 人们会换手机号,有了 ACI,之后还能用
profile重新获取元数据。 - 保持合规。 只处理你有合法依据使用的号码,并尊重这个隐私优先平台的使用预期。
常见问题
怎么检查一个手机号有没有注册 Signal?
用国际格式的号码调用 check 接口,返回里的 registered 会是 true、false 或 null。null 表示暂时无法判断,请稍后重试,而不要当作未注册处理。
能通过手机号拿到 Signal 用户的姓名或头像吗?
不能。Signal 把显示名称、个人简介和头像用只有联系人才持有的资料密钥做端到端加密。对陌生号码,查询会返回 fieldsEncrypted=true,并隐藏这些字段。
ACI 和 PNI 分别是什么? ACI 是账号的稳定 UUID,PNI 是与手机号绑定的标识。两者都是稳定的标识,可以用来对记录去重和关联。
手机号需要带加号吗?
不需要。完整的国际号码带不带开头的 + 都行,接口会统一成纯数字。但一定要带上国家区号。
每个号码都要调用 user-info 吗?
通常不用。先调用更轻量的 check,只对已注册的号码调用 user-info,因为只有这种情况下才有身份信息可以返回。
立即体验
在 Signal 号码校验页面在线测试,或阅读 check、user-info 与 profile 接口文档。查看定价了解每次请求的成本。