关于 clerk-sms-logs
clerk-sms-logs 是用于排查 Clerk 所发送短信投递过程的 Skill。开发者可借助 Application Logs 区分 Clerk 拦截、服务商拒绝、运营商投递问题及尚未确认的结果。
主要功能
- 01说明五种短信生命周期事件
- 02通过 Dashboard 或 Backend API 查询日志
- 03按时间、号码、用户和 trace 筛选
- 04提供失败诊断步骤
- 05说明事件范围与遥测限制
适用场景
- 01排查未收到验证码
- 02区分发送前拦截与运营商失败
- 03查看某个号码的短信时间线
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ clerk/ skills --skill clerk-sms-logs安装后在 Claude Code 中输入/clerk-sms-logs确认已加载。
附带文件references/events.md
详细介绍
短信日志
回答一个问题:这条由 Clerk 发送的短信发生了什么? sms.*
应用日志会记录 Clerk 代表实例发送的每条短信的送达生命周期——电话验证代码、OTP、密码重置代码——让你无需猜测,就能判断是注册卡住、运营商拒收,还是被 Clerk 拦截。
可以通过两种方式查看:在 Clerk Dashboard → Application Logs 中筛选
sms.* 事件类型,或通过 Backend API
(GET /v1/logs)以编程方式查询——数据相同,且可通过脚本重复调查。
这些是关于终端用户活动的送达遥测数据,不是控制面板操作。
查询日志
Backend API 的 logs 端点需要密钥(sk_*)。最简单的调用方式是使用 Clerk CLI,它会自动为你注入身份验证信息:
# SMS lifecycle events in the last 24h, newest first
SINCE=$(( ($(date +%s) - 86400) * 1000 ))
clerk api "/logs?type=sms.*&event_time_after=${SINCE}&limit=20"
# Just failures, same window
clerk api "/logs?type=sms.failed&event_time_after=${SINCE}&limit=20"
# One phone number's SMS history (exact-match payload filter)
# channel=sms keeps WhatsApp off the timeline — see the note below
clerk api "/logs?type=sms.*&payload_filter[phone_number]=%2B14155550100&payload_filter[channel]=sms&event_time_after=${SINCE}"
# Why a number's sends failed — concrete type unlocks reason/raw_error
clerk api "/logs?type=sms.failed&payload_filter[phone_number]=%2B14155550100&event_time_after=${SINCE}&payload_fields=phone_number,reason,raw_error,rejected_before_send"
# Every event for one message, by its trace
clerk api "/logs?type=sms.*&trace_id=<trace_id>&event_time_after=${SINCE}"
始终限定时间范围。 这些日志量很大,因此应从较窄的
event_time_after 开始(如果大致知道短信发送时间,也可设置
event_time_before);只有在没有结果时才扩大范围——不要扫描整个保留窗口来查找一条短信。将 limit 保持在适中范围(10–50),并使用返回的游标翻页,而不是调高它。该端点会在自适应时间窗口内查询,因此范围内出现较短或空白的页面属于正常情况:持续使用 starting_after 翻页,直到响应表明没有下一页。
WhatsApp 使用相同的 sms.* 事件族(共用一条流水线;channel 负载字段为 sms 或 whatsapp),因此某个电话号码的时间线可能混合两种传输方式。本指南介绍短信——添加 payload_filter[channel]=sms 将结果限定为短信,或查看 channel 字段来区分。
也可以直接调用 Backend API:
SINCE=$(( ($(date +%s) - 86400) * 1000 ))
curl -s "https://api.clerk.com/v1/logs?type=sms.failed&event_time_after=${SINCE}&limit=20" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
| python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin), indent=2))"
主要查询参数(除非某个操作步骤需要,否则均为可选):
| 参数 | 用途 |
|---|---|
type | 事件类型:具体类型(sms.failed)或尾随通配符(sms.*)。使用 payload_filter 时必需。 |
payload_filter[<field>] | 精确匹配负载字段,例如 payload_filter[user_id]=user_123。对值进行 URL 编码(E.164 电话号码中的 + 应转换为 %2B)。 |
trace_id | 关联同一条短信的所有事件。 |
event_time_after / event_time_before | Unix 毫秒时间范围。每次查询至少设置 event_time_after——见上文说明。 |
limit、starting_after、ending_before | 游标分页。 |
默认情况下,列表响应不包含解码后的负载;添加 payload_fields
(以逗号分隔的叶路径)可包含指定字段,也可以通过
GET /v1/logs/{event_time_ms}:{event_id} 完整获取某一行。
reason、raw_error 和 rejected_before_send 仅存在于失败事件中,因此只有在 type 为具体类型 sms.failed 或 sms.undeliverable 时,才能选择或筛选这些字段。在 sms.* 通配符下,payload_fields 和 payload_filter 会根据五种架构的交集(公共字段:phone_number、user_id、phone_number_id、slug、verification_id、source_type、purpose、channel)进行验证,因此在此处请求 reason 会返回 422。使用 sms.* 查看某个号码的完整时间线;切换到 type=sms.failed 以查看失败原因。使用
GET /v1/logs/schemas?type=sms.failed(或使用 ?type=sms.* 查看交集)可发现任意类型允许使用的确切字段。
这些
logs端点较新,且需要在实例上启用相应功能;如果调用返回 404 或没有结果,请先确认该实例已启用短信日志,再将空结果视为“没有这条短信”。
五种生命周期事件
一条短信会经历这一系列事件。每个事件只会在映射状态首次变化时发出一次,因此,送达迅速的短信可能会直接进入 sms.delivered,而不会出现可见的中间状态。
| 事件 | 含义 | 是否为终态? |
|---|---|---|
sms.accepted | 送达服务提供商已接受交接。不能证明短信已送达手机。 | 否 |
sms.delivered | 运营商已确认短信送达手机。 | 是(成功) |
sms.failed | 短信在交接前停止发送,或被服务提供商直接拒绝。包含 reason。 | 是(失败) |
sms.undeliverable | 运营商报告短信无法送达,此时服务提供商已接受交接。包含 reason。 | 是(失败) |
sms.unconfirmed | 服务提供商明确报告结果未知或未确认。没有任何一方的送达证据。 | 否——后续的 delivered / undeliverable 仍可取代此状态 |
理解方式:accepted → delivered 是理想路径。failed 表示短信未能发出(或在入口处被拒);undeliverable 表示短信已发出,但被运营商退回;unconfirmed 表示结果无人知晓。
调试指南
“用户从未收到验证码。” 找到这条短信——查询 sms.*,按电话号码或用户 ID 过滤(payload_filter[phone_number] / payload_filter[user_id],并按上文所述添加 payload_filter[channel]=sms,以排除 WhatsApp)。然后查看该短信的最新事件:
- 完全没有
sms.*事件 → 日志无法证明曾尝试发送短信。先排查查询是否有遗漏:确认已为此实例启用短信日志、时间范围涵盖发送时间,并且已翻页至游标末尾(这些日志尽力提供,详见“范围”)。如果日志可信且仍为空,原因通常出在上游:验证码未创建,或该号码在进入短信管线前已被屏蔽(检查 Protect / 机器人检测事件)。这不是短信送达问题。 sms.failed→ 查看reason。如果存在rejected_before_send,说明 Clerk 已阻止发送(国家/地区屏蔽、月度限额、速率限制)——应调整配置,而不是联系运营商。否则是服务提供商拒绝了发送;请参阅原因表。- 有
sms.accepted,但没有sms.delivered→ 短信已离开 Clerk,但运营商始终未确认。稍等片刻(送达回执可能延迟),如果长时间仍无回执,则将其视为该号码的运营商或手机问题。 sms.undeliverable→ 运营商在接受短信后将其退回。查看reason;destination_unreachable/invalid_phone_number表明问题出在号码本身。sms.unconfirmed→ 目前没有送达信号。它不是终态:后续的sms.delivered或sms.undeliverable仍可取代此状态,因此在得出结论前,请重新查询追踪记录。不要据此推断成功或失败;如果尚未收到确定事件,且用户也没有收到短信,请让他们重试。
“发往 的短信无法发送。” 查找 reason = country_not_supported 的 sms.failed(Clerk 屏蔽了该国家/地区),或 reason = restricted_destination(服务提供商不会向该地区发送短信)。如果 reason = rate_limited 且设置了 rejected_before_send 的事件激增,说明这是 Clerk 自身针对号码或号段的限流——在短信轰炸攻击期间属于预期行为,并非服务中断。
解读失败原因
sms.failed 和 sms.undeliverable 都会携带固定集合中的标准化 reason(可过滤且稳定的契约),以及可选的 raw_error(服务提供商报告的原始错误信息;有报告时会包含,可用于排查特定情况,但因其因服务提供商而异且不稳定,切勿据此过滤)。
完整的原因列表及各值的含义——包括哪些原因只会出现在 Clerk 侧发送前拒绝的情况中——请参阅 references/events.md。
关联一条消息的生命周期
同一条 SMS 的所有事件共享同一个追踪记录。一条消息有多个事件时(例如,先是 accepted,然后是 undeliverable),即使它们的主题可能不同,也会通过同一个追踪 ID 关联起来。使用追踪记录来整理一条消息的时间线,而不是孤立地查看各个事件。
范围与保证
- 仅限由 Clerk 投递的 SMS。 如果你使用自己的 SMS 服务提供商(
delivered_by_clerk = false),这些事件并不能描述你发送的消息。 - 负载中绝不会包含消息正文、验证码或服务提供商的身份信息。 请根据
reason、时间戳以及电话号码 / 用户 ID 进行排查,而不是依赖其中不存在的消息内容。 - 尽力而为的遥测。 与所有应用程序日志一样,这些事件不保证一定送达;缺少事件只能作为较弱的证据。不要构建依赖每个 SMS 事件都能送达的应用逻辑;对于应用必须据此采取行动的投递状态,请使用验证本身的状态。
protect.sms.*(Clerk Protect 的反滥用拦截)属于单独的事件系列;Clerk 端的拦截会显示在该系列中,而不是作为sms.failed出现。
完整的事件 / 原因 / 负载参考:references/events.md。