用 App Store Connect API 上传 App Store 截图
appScreenshotSet,预约一个 appScreenshot(Apple 会返回预签名的分块上传地址),PUT 上传字节数据,然后用 uploaded: true 和文件的 MD5 值 PATCH 这条预约记录。鉴权用的是一个用你的 .p8 密钥签名的 20 分钟有效期 JWT。每一步单独看都不难,但整个流程相当琐碎。下面是完整的实际步骤——以及如果你不想自己搭建,还有捷径可走。Apple 的文档内容准确,但几乎没法读——整个流程分散在十几个参考页面里,没有一个完整示例。这篇文章把完整流程按顺序讲一遍,附上确切的接口地址和字段名。写给正在纠结是自己搭建这套集成,还是直接用现成工具的人。
你上传数据所在的对象模型
截图不会直接挂在你的 App 上,而是挂在一条对象链上,你得从上往下走一遍:
- appStoreVersion——你正在编辑的具体版本(必须处于可编辑状态)。
- appStoreVersionLocalization——每个 App Store 语言各一个(en-US、de-DE、ja 等等)。
- appScreenshotSet——某个语言下每种展示类型各一个(6.7" iPhone、13" iPad 等)。
- appScreenshot——单张图片,每个集合最多 10 张。
所以一个完整本地化到 50 种语言、覆盖两个 iPhone 尺寸和一个 iPad 尺寸的商品页,就是 50 × 3 个集合,每个最多容纳 10 张截图。正是这种乘法关系,才让人宁愿自动化处理,也不愿意在网页界面里一路点击。
第 0 步——20 分钟有效期的 JWT
除了字节上传本身之外,其他每个请求都需要一个 Authorization: Bearer 请求头,里面装着你自己签名的 JSON Web Token。你需要从 App Store Connect 拿到三样东西:issuer ID、key ID,以及创建密钥时下载一次的 .p8 私钥文件(Apple 之后不会再显示它)。
这个 token 用 ES256——基于 P-256 曲线的 ECDSA 加 SHA-256——用那个 .p8 密钥签名。请求头里带 kid;payload 里 iss 是 issuer,audience 是字面字符串 appstoreconnect-v1,exp 距 iat 不能超过20 分钟。有效期更长的一律被 Apple 拒绝,所以你要经常重新生成——每批任务铸造一个新 token,而不是想办法长期保持一个。
用任何支持 ES256 的 JWT 库(jsonwebtoken、PyJWT,随便你技术栈用什么)签名,把 .p8 内容当作密钥传入,然后把结果放进下面每次调用的 Authorization 请求头里。
第 1 步——创建截图集合
一个集合只对应一种语言和一种展示类型。你需要向 /v1/appScreenshotSets 发 POST 请求,attributes 里带上 screenshotDisplayType,relationship 指向你要填充的 appStoreVersionLocalization。
常见的展示类型:APP_IPHONE_67(6.7 英寸,1290 × 2796),也就是目前的大尺寸 iPhone;以及 APP_IPAD_PRO_3GEN_129(12.9 英寸,2048 × 2732)。Apple 现在有个实用的功能:最大尺寸 iPhone 的截图会自动复用到较小的 iPhone 机型上,最大尺寸 iPad 的截图也会复用到较小的 iPad 上——所以实际操作中你往往每种语言只需要一个 iPhone 集合和一个 iPad 集合,而不是每种具体设备都要一个。返回结果里会给你一个集合 id,带到下一步调用里去。如果该语言/类型的集合已经存在,直接复用它,不要重复创建。
第 2 步——预约这张截图
你不会一次性把文件上传完。你要先预约:把文件名和确切的字节大小告诉 Apple,它会返回一个上传方案。向 /v1/appScreenshots 发 POST 请求,带上 fileName 和 fileSize 属性,以及指向第 1 步集合的 relationship。
返回结果才是重点。在新截图对象的属性里,你会拿到 uploadOperations——一个描述具体如何推送字节数据的数组。文件小的话只有一个操作;文件大的话 Apple 会拆成多个。每一条都会给你一个 method(PUT)、一个预签名的 url、要发送的字节 offset 和 length,以及要附加的 requestHeaders。
第 3 步——PUT 上传字节数据
针对每个操作,从文件里读取从 offset 开始、长度为 length 字节的那一段,然后用 Apple 提供的 requestHeaders 原样 PUT 到给定的 url。这些 URL 是预签名的,所以你不要在这里带上你的 JWT——加上 Authorization 请求头反而可能破坏签名请求。多个操作可以并行执行,但 Apple 会对过于激进的上传做限速,所以最好用带退避的重试机制包裹它们,而不是一股脑全部发出去。
第 4 步——用 uploaded: true 和 MD5 提交
光上传字节数据没有任何意义,除非你告诉 Apple 这次预约已经完成。用 uploaded: true 和一个 sourceFileChecksum——整个文件的 MD5,以小写十六进制字符串表示——去 PATCH 这张截图。这是 Apple 的完整性校验:如果校验值和实际收到的数据对不上,处理就会失败。
PATCH 之后,资源会进入 Apple 那边的处理流程。轮询这张截图的 assetDeliveryState,直到它变成完成状态——如果失败,读一下它的 errors,尺寸不对和 alpha 通道之类的问题都会在这里暴露出来。PUT 和 PATCH 都可能成功,但截图仍可能在几分钟后的处理阶段被拒绝。不要以为提交返回 2xx 就万事大吉;要盯着 delivery state 看。
第 5 步——设置显示顺序
截图返回时的顺序就是创建时的顺序,这几乎从来都不是你想要展示的顺序。集合的排序是一个单独的 relationship。用截图 ID 数组去 PATCH /v1/appScreenshotSets/{id}/relationships/appScreenshots——数组里的顺序就是商店页面上的展示顺序。
Apple 实际强制执行的限制
- 只支持 PNG 或 JPEG。不支持 HEIC,不支持 WebP。如果你的渲染器输出的是其他格式,先转换一下。
- 不能有 alpha 通道。把透明区域压平成纯色背景,导出 RGB。杂散的 alpha 通道是处理阶段最常见的静默拒绝原因之一。
- 每种展示类型必须是精确尺寸。图片必须精确匹配该展示类型的像素尺寸——不能有一点偏差,也不能放大凑数。一张 1290 × 2796 的文件只能进
APP_IPHONE_67集合,别无他处。 - 每个集合最多 10 张。每种语言、每种展示类型最多十张截图。
- 只能操作可编辑版本。你只能修改处于可编辑状态的版本的集合;已经在审核中的版本是锁定的。
为什么这比看起来的工作量大得多
每个单独的调用都很简单。成本都在周边的一堆事情上:签名并轮换一个 20 分钟有效期的 token、计算 MD5、按 uploadOperations 切分文件、处理部分上传失败的重试、轮询 delivery state、把你的语言映射到 Apple 的语言代码,再把这一切重复 150 多次才能完成一个真正本地化的商品页。把这套流程做到可靠,需要花上一个周末处理鉴权、分块上传和错误处理——而且 Apple 每加一种展示类型,你就得跟着维护一次。
如果你想直接用这套流程而不用自己写,Fastlane 的 deliver 封装了这同一个 API,是标准的开源方案。它解决了上传问题,但不负责设计截图或撰写文案。
……或者干脆全部跳过
这个 API 只负责搬运已经做好的图片。设计轮播图、撰写文案、再把它们翻译成各种语言,才是真正耗时的部分,而这些都还得有人来做。Mokbi 现在就能做到这一切:它设计截图、起草商品页文案,并把整套内容翻译成50 种语言,然后按这个 API 要求的每种展示类型精确尺寸导出每一张图片——PNG、RGB、无 alpha、尺寸精确——这样上传就不会在 delivery-state 那一步被弹回来。
然后它会负责发布。对于 App Store,Mokbi 在幕后运行的正是这套完整的 reserve-and-commit 流程——上传你的截图和元数据,并把版本准备好等你提交,你完全不用碰 JWT 或分块 PUT。最终的提交和审核仍需要 Apple 来完成;在此之前的一切都替你处理好了。对于 Google Play,它会直接推送到商店。不管哪种情况,你都能拿到这个 API 的效果,却不用写一行代码,设计、文案和 50 种语言的翻译也都已经完成。