APNs 推送通知测试工具
填好载荷,选择推送类型,拖入 .p8 密钥并填写 Key ID 和 Team ID,就能得到一条 curl --http2 命令,在“终端”里把推送发到 api.sandbox.push.apple.com 或 api.push.apple.com。所有字段都会先按苹果的规则检查:4096 字节上限(VoIP 为 5120)、每种推送类型需要的 topic 后缀和优先级、后台推送的 content-available,以及实时活动的各个键。签名在你的浏览器里完成:密钥不上传、不保存。
密钥只留在本页
- •.p8 密钥由你自己的浏览器读取并签名,不会上传;使用期间本页不发出任何网络请求。
- •不保存任何内容:没有 Cookie,也不写入本地存储。刷新页面后,密钥、设备令牌和载荷都会清除。
- •浏览器本身无法向 APNs 发送请求,所以本页给你一条在“终端”里运行的 curl 命令。其中的提供方令牌最长可用一小时,别分享给他人。
请求头名称、推送类型、topic 后缀、优先级、载荷键和大小限制均摘自苹果 APNs 文档,核对日期 2026-10-30。
1. 发往哪里
didRegisterForRemoteNotificationsWithDeviceToken 给出的字节(或实时活动的推送令牌),写成十六进制。空格和 < > 会被去掉。
显示提醒、播放声音或设置角标。
你的 Bundle ID,加上该推送类型需要的后缀。切换推送类型时,后缀会随之替换。
0 表示只尝试一次、不存储;未来某个时间表示在此之前反复尝试;留空则按苹果的默认存储策略。
值相同的通知会在设备上相互替换。最多 64 字节。 0 / 64 字节
一个小写 UUID,APNs 会在响应中原样返回。留空则由 APNs 生成。
2. 用什么方式连接 APNs
把 AuthKey_XXXXXXXXXX.p8 文件拖到这里
也可以打开文件,或在下面粘贴文件内容。Key ID 会按文件名替你填好。
10 个字符,在“证书、标识符和描述文件 > 密钥”中显示在密钥旁边。
10 个字符,在开发者账户的“会员资格详细信息”中查看。
苹果规定:提供方令牌最长有效一小时。更新频率不要高于每 20 分钟一次,也不要低于每 60 分钟一次;在同一连接上更换过于频繁会得到 TooManyProviderTokenUpdates。
3. 载荷
提醒
角标和声音
分组与投递
实时活动
自定义键
苹果会忽略放在 aps 里的自定义键,所以它们放在顶层,例如 {"gameID": "12345678"}。
4. 检查并发送
载荷大小
65 / 4096 字节
按压缩后 JSON 的 UTF-8 字节数计算,也就是 curl 实际发送的内容。一个汉字占 3 字节,大多数表情占 4 字节。
需要修正
- •请填写 apns-topic(你的 Bundle ID,部分推送类型还需加后缀)。
- •请填写设备令牌。在此之前,命令会从 $DEVICE_TOKEN 读取。
- •请填写 Key ID。
- •请填写 Team ID。
在“终端”里运行的 curl 命令
curl -i --http2 \
--header 'apns-push-type: alert' \
--header 'apns-priority: 10' \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{"aps":{"alert":{"title":"Hello","body":"This is a test push."}}}' \
"https://api.sandbox.push.apple.com:443/3/device/$DEVICE_TOKEN"尚未签名:命令会从 $AUTHENTICATION_TOKEN 读取令牌。
macOS 自带的 curl 支持 HTTP/2。收到 200 和 apns-id 响应头表示 APNs 已接受推送,但不保证一定送达设备。
载荷(格式化)
{
"aps": {
"alert": {
"title": "Hello",
"body": "This is a test push."
}
}
}载荷(压缩后,实际发送)
{"aps":{"alert":{"title":"Hello","body":"This is a test push."}}}APNs 返回错误时
响应正文是 JSON,例如 {"reason": "BadDeviceToken"}。苹果列出的原因如下:
- 400 BadCollapseId
- apns-collapse-id 超过 64 字节。
- 400 BadDeviceToken
- 设备令牌无效,或属于另一个服务器(沙盒与生产混用)。
- 400 BadExpirationDate
- apns-expiration 的值无效。
- 400 BadMessageId
- apns-id 不是有效的 UUID。
- 400 BadPriority
- apns-priority 的值无效。
- 400 BadTopic
- apns-topic 无效。
- 400 DeviceTokenNotForTopic
- 设备令牌属于另一个应用或 topic。
- 400 DuplicateHeaders
- 有请求头重复出现。
- 400 IdleTimeout
- 连接空闲时间过长。
- 400 InvalidPushType
- apns-push-type 的值无效。
- 400 MissingDeviceToken
- 路径中没有设备令牌。
- 400 MissingTopic
- 缺少 apns-topic,而证书对应多个 topic。
- 400 PayloadEmpty
- 请求正文为空。
- 400 TopicDisallowed
- 不允许向这个 topic 推送。
- 403 BadCertificate
- 证书无效。
- 403 BadCertificateEnvironment
- 证书属于另一个服务器(沙盒与生产混用)。
- 403 ExpiredProviderToken
- 提供方令牌过期(iat 超过一小时),请重新签名。
- 403 Forbidden
- 不允许该操作。
- 403 InvalidProviderToken
- 提供方令牌无效或签名无法验证:Key ID、Team ID 或密钥不对。
- 403 MissingProviderToken
- 既没有使用证书,也缺少 authorization 请求头。
- 403 UnrelatedKeyIdInToken
- 令牌所用密钥与该连接首次推送所用的密钥无关,请新建连接。
- 403 BadEnvironmentKeyIdInToken
- 该密钥只限用于另一个环境(沙盒与生产)。
- 404 BadPath
- :path 无效。
- 405 MethodNotAllowed
- 只允许 POST。
- 410 ExpiredToken
- 设备令牌已过期。
- 410 Unregistered
- 设备令牌对这个 topic 已不再有效,通常是应用被删除了。请停止向它发送。
- 413 PayloadTooLarge
- 载荷超过 4096 字节(VoIP 为 5120)。
- 429 TooManyProviderTokenUpdates
- 提供方令牌更换太频繁。每 20 分钟最多更换一次。
- 429 TooManyRequests
- 连续向同一设备令牌发送过多推送,请稍后重试。
- 500 InternalServerError
- APNs 内部错误,请稍后重试。
- 503 ServiceUnavailable
- APNs 暂不可用,请稍后重试。
- 503 Shutdown
- 这台 APNs 服务器正在关闭,请重新连接。
它是怎么工作的
- •载荷表单覆盖苹果的 aps 键:alert(文字或本地化键)、badge、sound(含紧急提醒字典)、thread-id、category、content-available、mutable-content、interruption-level、relevance-score、target-content-id、filter-criteria,以及实时活动的键(event、timestamp、content-state、stale-date、dismissal-date、attributes-type、attributes)。自定义键与 aps 并列。也可以直接粘贴写好的载荷。
- •显示的大小是压缩后 JSON 的 UTF-8 字节数,也就是 APNs 计算的方式。一个汉字占 3 字节,大多数表情占 4 字节,所以看起来不长的消息也可能超限。
- •请求头按苹果的列表生成:apns-push-type、带正确后缀的 apns-topic(.voip、.complication、.push-type.liveactivity、.push-type.widgets、.location-query、.voip-ptt 等)、符合各类型规则的 apns-priority(后台推送必须为 5)、apns-expiration、apns-collapse-id(最多 64 字节)和 apns-id。
- •使用令牌方式时,本页用 Web Crypto 和你的 .p8 密钥签名 JWT,头部为 {alg: ES256, kid},声明为 {iss: Team ID, iat},与 App Store Connect 令牌页使用同一个签名器。苹果接受的令牌最长一小时,且更换频率不要高于每 20 分钟一次。
- •使用证书方式时,命令用 --cert 和 --key 指向你填写的文件。浏览器无法自己发送这个请求(APNs 需要服务器发起的 HTTP/2,并且拒绝网页直接访问),所以要在“终端”里运行命令。
它做不到的事
- •它不会发送推送。curl 命令由你自己运行;本页不发出任何网络请求。
- •它不保存密钥、令牌或载荷。刷新页面就全部清除。
- •它无法判断设备令牌是否真实、是否属于你的应用、是否与所选服务器匹配,这些由 APNs 回答(BadDeviceToken、DeviceTokenNotForTopic)。
- •它只检查 content-state 和 attributes 是否为 JSON,不检查是否与你的 Swift ActivityAttributes 类型一致。
- •它不涉及实时活动频道的广播推送,也不涉及频道管理接口。
常见问题
- BadDeviceToken、TopicDisallowed、InvalidProviderToken 是什么意思?
- BadDeviceToken(400):设备令牌无效,最常见的原因是把开发环境的令牌发到了生产服务器,或反过来。TopicDisallowed(400):不允许向这个 apns-topic 推送,例如 Bundle ID 不属于你的团队。InvalidProviderToken(403):JWT 无效或签名无法验证,通常是 Key ID 或 Team ID 填错,或者密钥与 kid 不对应。苹果的完整原因列表在本页 curl 命令下方。
- 沙盒和生产 APNs 服务器有什么区别?
- api.sandbox.push.apple.com 用于从 Xcode 安装、aps-environment 为 development 的应用;api.push.apple.com 用于 TestFlight、Ad Hoc 和 App Store 版本。每个版本拿到的设备令牌只能用于对应的服务器。密钥可以两边通用,也可以只限其中一个,取决于你创建时的设置。
- 推送通知的载荷最大能有多大?
- 除 VoIP 外,所有推送类型都是 4096 字节;VoIP 为 5120 字节。限制按实际发送的 JSON 正文的 UTF-8 字节数计算,所以中文和表情比字符数看起来更占空间。超过上限时 APNs 返回 413 PayloadTooLarge。
- 提供方令牌多久换一次?
- 至少每小时换一次,因为 iat 超过一小时的令牌会被 APNs 拒绝(ExpiredProviderToken);但不要比每 20 分钟一次更频繁,因为在同一连接上更换太勤会得到 TooManyProviderTokenUpdates。两次更换之间,所有推送共用同一个令牌。
- 为什么后台推送收不到?
- 检查苹果要求的三点:apns-push-type 为 background;apns-priority 为 5(10 属于错误,不填也等于 10);aps 字典里只有 "content-available": 1,没有 alert、badge 或 sound。即便如此,系统也把后台推送当作低优先级,可能延后处理;苹果建议每小时不超过两三次。
- apns-topic 应该填什么?
- alert 和 background 推送填 Bundle ID。其他推送类型要加后缀:VoIP 加 .voip,手表复杂功能加 .complication,实时活动加 .push-type.liveactivity,小组件加 .push-type.widgets,控件加 .push-type.controls,位置推送加 .location-query,“按下即说”加 .voip-ptt,File Provider 加 .pushkit.fileprovider。切换推送类型时,本页会替换后缀。