Загрузка скриншотов App Store через App Store Connect API
appScreenshotSet для нужной локали и устройства, зарезервировать appScreenshot (Apple выдаёт тебе предподписанные URL для частей файла), отправить байты через PUT, а затем подтвердить резервирование PATCH-запросом с uploaded: true и MD5-хешем файла. Аутентификация — это JWT-токен на 20 минут, подписанный твоим ключом .p8. Ничего сложного, но всё это очень муторно. Вот реальная последовательность действий — и способ обойтись без неё, если строить это самому не хочется.Документация Apple по этой теме точная, но почти нечитаемая — процесс разбросан по десятку справочных страниц без единого сквозного примера. Этот пост — тот самый разбор по шагам, с точными эндпоинтами и названиями полей. Он написан для тех, кто решает, строить ли интеграцию самостоятельно или взять готовый инструмент.
Объектная модель, в которую ты загружаешь данные
Скриншоты не привязываются к приложению напрямую. Они висят на цепочке объектов, и её нужно проходить сверху вниз:
- appStoreVersion — конкретная версия, которую ты редактируешь (должна быть в редактируемом состоянии).
- appStoreVersionLocalization — по одной на каждую локаль App Store (en-US, de-DE, ja и так далее).
- appScreenshotSet — по одному на каждый тип экрана внутри локализации (6.7" iPhone, 13" iPad и т. д.).
- appScreenshot — отдельное изображение, до 10 штук в наборе.
Так что полностью локализованный листинг на 50 языках для двух размеров iPhone и одного размера iPad — это 50 × 3 набора, в каждом из которых до 10 скриншотов. Именно из-за этого умножения люди автоматизируют процесс вместо того, чтобы кликать по веб-интерфейсу.
Шаг 0 — JWT-токен на 20 минут
Каждый запрос, кроме самой загрузки байтов, требует заголовок Authorization: Bearer с JSON Web Token, который ты подписываешь сам. Для этого нужны три вещи из App Store Connect: issuer ID, key ID и файл приватного ключа .p8, который скачивается один раз при создании ключа (повторно Apple его не покажет).
Токен подписывается алгоритмом ES256 — ECDSA на кривой P-256 с SHA-256 — с использованием этого ключа .p8. В заголовке передаётся kid; в payload — издатель как iss, аудитория в виде строки appstoreconnect-v1 и exp, отстоящий от iat не более чем на 20 минут. Apple отклоняет токены с более долгим сроком жизни, поэтому их приходится часто перевыпускать — лучше генерировать новый токен на каждую партию запросов, чем пытаться держать один открытым.
Подпиши его любой JWT-библиотекой с поддержкой ES256 (jsonwebtoken, PyJWT — что используется в твоём стеке), передай содержимое .p8 как ключ и вставляй результат в заголовок Authorization для каждого из нижеописанных запросов.
Шаг 1 — создание набора скриншотов
Набор привязан к одной локализации и одному типу экрана. Отправь POST на /v1/appScreenshotSets с полем screenshotDisplayType в attributes и связью, указывающей на нужный appStoreVersionLocalization.
Распространённые типы экранов: APP_IPHONE_67 (6.7", 1290 × 2796) — текущий размер для больших iPhone, и APP_IPAD_PRO_3GEN_129 (12.9", 2048 × 2732). Полезная деталь: теперь Apple переиспользует скриншоты для самого крупного размера iPhone в моделях меньшего класса, а самого крупного iPad — в меньших iPad, поэтому на практике часто достаточно одного набора для iPhone и одного для iPad на локаль, а не по набору на каждое физическое устройство. В ответе ты получаешь id набора, который передаёшь в следующий запрос. Если набор для этой локали/типа уже существует, используй его повторно, а не создавай дубликат.
Шаг 2 — резервирование скриншота
Файл не загружается одним запросом. Сначала ты его резервируешь: сообщаешь Apple имя файла и точный размер в байтах, а в ответ получаешь план загрузки. Отправь POST на /v1/appScreenshots с атрибутами fileName и fileSize и связью с набором из шага 1.
Самое интересное — в ответе. Внутри атрибутов нового скриншота ты получаешь uploadOperations — массив, описывающий, как именно отправлять байты. Для маленького файла это одна операция; для большого Apple разбивает его на несколько. Каждая запись содержит method (PUT), предподписанный url, байтовые offset и length для отправки, а также requestHeaders, которые нужно прикрепить.
Шаг 3 — отправка байтов через PUT
Для каждой операции считай участок файла начиная с offset длиной length байт и отправь его PUT-запросом по указанному url строго с теми requestHeaders, которые предоставила Apple. Эти URL предподписаны, поэтому JWT здесь передавать не нужно — добавление заголовка Authorization может даже сломать подписанный запрос. Несколько операций можно выполнять параллельно, но Apple ограничивает скорость при агрессивной загрузке, так что лучше оборачивать их в повтор с задержкой, а не отправлять всё разом.
Шаг 4 — коммит с uploaded: true и MD5
Загрузка байтов сама по себе ничего не даёт, пока ты не сообщишь Apple, что резервирование завершено. Отправь PATCH на скриншот с uploaded: true и sourceFileChecksum — MD5-хешем всего файла в виде строки из строчных шестнадцатеричных символов. Это проверка целостности от Apple: если контрольная сумма не совпадает с тем, что получилось на их стороне, обработка завершится ошибкой.
После PATCH-запроса файл уходит в обработку на стороне Apple. Опрашивай поле assetDeliveryState скриншота, пока оно не перейдёт в завершённое состояние — и читай errors, если обработка не удалась: именно там всплывают проблемы с неверными размерами и альфа-каналом. PUT и PATCH могут пройти успешно, а скриншот всё равно будет отклонён через несколько минут в процессе обработки. Не считай, что код 2xx на коммите означает завершение — следи за статусом доставки.
Шаг 5 — порядок отображения
Скриншоты возвращаются в том порядке, в котором были созданы, а это редко совпадает с нужным порядком показа. Порядок набора — отдельная связь. Отправь PATCH на /v1/appScreenshotSets/{id}/relationships/appScreenshots с массивом ID скриншотов — порядок элементов массива и есть порядок отображения в магазине.
Ограничения, которые Apple действительно проверяет
- Только PNG или JPEG. Никаких HEIC и WebP. Если твой рендерер выдаёт что-то другое, сначала конвертируй.
- Без альфа-канала. Убери прозрачность на сплошной фон и экспортируй в RGB. Случайный альфа-канал — одна из самых частых незаметных причин отказа при обработке.
- Точные размеры для каждого типа экрана. Изображение должно точно соответствовать пиксельному размеру типа экрана — без допусков и без масштабирования вверх. Файл 1290 × 2796 подходит только в набор
APP_IPHONE_67и никуда больше. - До 10 штук в наборе. Максимум десять скриншотов на локализацию на тип экрана.
- Только редактируемая версия. Изменять наборы можно только в версии, которая находится в редактируемом состоянии; версия, уже отправленная на проверку, заблокирована.
Почему это сложнее, чем кажется
Каждый отдельный запрос прост. Сложность — во всём, что вокруг: подпись и обновление токена каждые 20 минут, вычисление MD5, разбиение файлов в соответствии с uploadOperations, обработка повторов при частичной загрузке, опрос статуса доставки, сопоставление твоих локалей с кодами локалей Apple — и всё это по 150+ раз для полностью локализованного листинга. Чтобы довести это до надёжной работы, уйдут целые выходные — на аутентификацию, загрузку по частям и обработку ошибок, а дальше это придётся поддерживать при каждом новом типе экрана от Apple.
Если нужна эта последовательность без написания кода, команда deliver из Fastlane оборачивает тот же самый API и является стандартным путём с открытым исходным кодом. Она решает задачу загрузки, но не рисует скриншоты и не пишет тексты описаний.
…или обойтись без всего этого
API умеет только переносить готовые изображения. Кто-то всё равно должен спроектировать карусель, написать тексты и перевести их на каждую локаль — а именно это и отнимает больше всего времени. Mokbi делает это уже сегодня: проектирует скриншоты, пишет тексты для листинга и переводит всё это на 50 языков, а затем экспортирует каждое изображение в точных размерах, которые требует этот API для каждого типа экрана — PNG, RGB, без альфа-канала, нужный размер — так что загрузка не отклоняется на этапе проверки статуса доставки.
А затем публикует. Для App Store Mokbi выполняет под капотом именно эту последовательность reserve-and-commit — загружает твои скриншоты и метаданные и готовит версию к отправке, так что тебе никогда не приходится трогать ни JWT-токен, ни загрузку по частям. Финальный Submit и саму проверку Apple по-прежнему должен инициировать ты сам — всё, что до этого, уже сделано за тебя. Для Google Play публикация идёт напрямую в магазин. В любом случае ты получаешь результат работы API без написания кода — с уже готовыми дизайном, текстами и переводом на 50 языков.