Workflow

Upload Screenshots with fastlane deliver & supply

Automate App Store and Google Play screenshot and metadata uploads with fastlane deliver and supply: folder structure, file naming, locale codes and CI tips.

Updated 7 min read

fastlane’s deliver uploads App Store screenshots from fastlane/screenshots/<locale>/ and metadata from fastlane/metadata/<locale>/*.txt; supply does the same for Google Play from fastlane/metadata/android/<locale>/. Put files in the right folders, authenticate with an API key or service account, and one command replaces every screenshot in every language — locally or on CI.

deliver and supply in one minute

deliver (App Store)supply (Google Play)
Screenshotsfastlane/screenshots/<locale>/fastlane/metadata/android/<locale>/images/<type>Screenshots/
Metadatafastlane/metadata/<locale>/*.txtfastlane/metadata/android/<locale>/*.txt
Device typeDetected from image resolutionChosen by folder name
AuthApp Store Connect API keyGoogle Cloud service account JSON key
Skip binary--skip_binary_upload--skip_upload_apk --skip_upload_aab

Both tools can also download what’s currently live: fastlane deliver download_screenshots and fastlane deliver download_metadata for the App Store, and fastlane supply init for Google Play. Doing that once is the easiest way to see the expected layout for your own app.

App Store: deliver folder structure

fastlane/
├── screenshots/
│   ├── en-US/
│   │   ├── 01_iPhone 6.3in.png      # 1206 × 2622
│   │   ├── 01_iPad 13in.png         # 2064 × 2752
│   │   ├── 02_iPhone 6.3in.png
│   │   └── ...
│   └── de-DE/
│       └── ...
└── metadata/
    ├── copyright.txt
    ├── primary_category.txt
    ├── en-US/
    │   ├── name.txt
    │   ├── subtitle.txt
    │   ├── keywords.txt
    │   ├── description.txt
    │   ├── promotional_text.txt
    │   ├── release_notes.txt
    │   ├── privacy_url.txt
    │   ├── support_url.txt
    │   └── marketing_url.txt
    └── de-DE/
        └── ...
  • Device detection: deliver works out the display class (6.9″ iPhone, 6.3″ iPhone, 13″ iPad, …) from each image’s exact pixel size, so the sizes must be ones App Store Connect accepts — see the screenshot sizes guide.
  • Order: give files a numeric prefix (01_, 02_) so each device’s set sorts in the order you want to show.
  • Limit: up to 10 screenshots per display class and locale.
  • Metadata files are plain UTF-8 text, one field per file. Stay within the character limits — keywords are 100 characters, comma-separated.
  • The default folder: values in fastlane/metadata/default/ apply to every language that doesn’t provide its own file.
12.9-inch iPad Pro
2048 × 2732 is shared by two iPad Pro generations. Recent fastlane versions map it to the 3rd-generation slot by default; to target the 2nd-generation slot, include APP_IPAD_PRO_129 in the file name.

Google Play: supply folder structure

fastlane/metadata/android/
├── en-US/
│   ├── title.txt
│   ├── short_description.txt
│   ├── full_description.txt
│   ├── video.txt
│   ├── changelogs/
│   │   ├── 10203.txt          # matches the versionCode
│   │   └── default.txt        # used when no versionCode file exists
│   └── images/
│       ├── icon.png
│       ├── featureGraphic.png
│       ├── tvBanner.png
│       ├── phoneScreenshots/
│       │   ├── 1_en-US.png
│       │   └── 2_en-US.png
│       ├── sevenInchScreenshots/
│       ├── tenInchScreenshots/
│       ├── tvScreenshots/
│       └── wearScreenshots/
└── de-DE/
    └── ...
  • Folder = device type. Unlike deliver, supply doesn’t inspect resolution; whatever is in phoneScreenshots becomes your phone set.
  • Replace, not append. Uploaded images replace the current images on the listing.
  • Order is alphanumerical by file name. Note that 10_ sorts before 2_; with up to 8 screenshots per type that is rarely a problem, but zero-padding is safest.
  • Feature graphic must be 1024 × 500 — see the feature graphic guide.
  • Set sync_image_upload to skip images that haven’t changed (compared by checksum).

Locale codes differ between stores

App Store Connect and Google Play don’t use the same codes, so the same language can need differently named folders:

LanguageApp Store (deliver)Google Play (supply)
English (US)en-USen-US
Germande-DEde-DE
Italianitit-IT
Japanesejaja-JP
Koreankoko-KR
Spanish (Mexico / Latin America)es-MXes-419
Chinese (Simplified)zh-Hanszh-CN
Hebrewheiw-IL

A folder with a code the store doesn’t recognize is either rejected or ignored, so check the names against each store’s list. The localization guide has the full mapping.

Authentication

App Store Connect API key

Create a key under Users and Access › Integrations in App Store Connect (App Manager access is enough for metadata) and download the .p8 file. Either use the app_store_connect_api_key action in your Fastfile, or save a JSON file and pass --api_key_path:

{
  "key_id": "ABC123XYZ9",
  "issuer_id": "00000000-0000-0000-0000-000000000000",
  "key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
  "in_house": false
}

Google Play service account

In Google Cloud, create a service account, enable the Google Play Android Developer API and download a JSON key. Then invite the service account’s email in Play Console under Users and permissions with permission to edit the store listing. Check the key with fastlane run validate_play_store_json_key json_key:path/to/key.json.

Keep keys out of git
Never commit the .p8 or JSON key. Store them as CI secrets and pass them in as environment variables.

A Fastfile for store assets

This lane uploads screenshots and metadata only — no build, no submission:

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Upload App Store screenshots and metadata (no binary)"
  lane :store_assets do
    api_key = app_store_connect_api_key(
      key_id: ENV["ASC_KEY_ID"],
      issuer_id: ENV["ASC_ISSUER_ID"],
      key_content: ENV["ASC_KEY_P8_BASE64"],
      is_key_content_base64: true
    )

    deliver(
      api_key: api_key,
      app_identifier: "com.example.myapp",
      skip_binary_upload: true,
      overwrite_screenshots: true,
      submit_for_review: false,
      force: true, # skip the HTML preview confirmation
      precheck_include_in_app_purchases: false
    )
  end
end

platform :android do
  desc "Upload Google Play screenshots and listing (no binary)"
  lane :store_assets do
    supply(
      package_name: "com.example.myapp",
      json_key_data: ENV["PLAY_SERVICE_ACCOUNT_JSON"],
      skip_upload_apk: true,
      skip_upload_aab: true,
      skip_upload_changelogs: true
    )
  end
end

Run it with fastlane ios store_assets or fastlane android store_assets. The one-off command-line equivalents:

fastlane deliver --skip_binary_upload --overwrite_screenshots --api_key_path asc_key.json
fastlane supply --skip_upload_apk --skip_upload_aab --json_key play_key.json --package_name com.example.myapp

deliver writes to the App Store version that is currently editable. Create the new version in App Store Connect first, or pass app_version. Changelogs are skipped above because Play release notes belong to a release; upload them with your build instead.

Running it in GitHub Actions

Add gem "fastlane" to a Gemfile, commit Gemfile.lock, and add the four secrets used below. This workflow runs on demand:

# .github/workflows/store-assets.yml
name: Upload store assets
on: workflow_dispatch

jobs:
  app-store:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true
      - run: bundle exec fastlane ios store_assets
        env:
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}

  google-play:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true
      - run: bundle exec fastlane android store_assets
        env:
          PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}

Create the base64 secret with base64 -i AuthKey_ABC123XYZ9.p8 (macOS) and paste the output; paste the service account JSON as-is.

Common pitfalls

  1. “Unknown device” or skipped screenshots. The image isn’t an exact accepted size, or your fastlane version predates a newer display class such as 1206 × 2622. Update fastlane and verify sizes with the screenshot checker.
  2. Alpha channel. App Store Connect rejects PNGs with transparency; flatten them with the resizer. See fixing the dimensions error.
  3. Screenshots added instead of replaced. Without overwrite_screenshots, deliver keeps existing screenshots and you can hit the 10-per-class limit.
  4. Wrong locale folder names — especially ja vs ja-JP and zh-Hans vs zh-CN between the two stores.
  5. Precheck failing with an API key. When deliver runs precheck (before submitting for review), its in-app purchase check doesn’t work with an API key; set precheck_include_in_app_purchases to false.
  6. Header, search and in-app event assets aren’t part of the deliver screenshot folders — upload them in App Store Connect.

With our tools

When you export a ZIP from the editor with the fastlane naming scheme, it is laid out for both tools: App Store screenshots in fastlane/screenshots/<locale>/NN_<size>.png, App Store metadata in fastlane/metadata/<locale>/*.txt, and Google Play images in fastlane/metadata/android/<locale>/images/<folder>/ with the feature graphic and icon alongside, plus Play listing text and a changelogs/default.txt when you include metadata. Locale folders use each store’s own codes. Unzip it into your project root, review the diff, and run your lane. Assets deliver doesn’t handle — header, search and event artwork — land in a separate store-assets/ folder for manual upload.

Frequently asked questions

Where does fastlane deliver look for screenshots?

In fastlane/screenshots, with one subfolder per App Store locale such as en-US or de-DE. deliver detects the device class from each image’s pixel size, so all iPhone and iPad sizes for a language can share one folder.

How do I upload only screenshots and metadata with fastlane deliver?

Run fastlane deliver --skip_binary_upload --overwrite_screenshots, or set skip_binary_upload and overwrite_screenshots to true in the deliver action. overwrite_screenshots removes the existing screenshots before uploading the new ones.

Where does fastlane supply look for Google Play screenshots?

In fastlane/metadata/android/<locale>/images/, in folders named phoneScreenshots, sevenInchScreenshots, tenInchScreenshots, tvScreenshots and wearScreenshots. The feature graphic and icon go directly in images as featureGraphic.png and icon.png.

How do I control the screenshot order in fastlane?

Name the files so they sort in the order you want, for example with a 01_, 02_ prefix. supply uploads screenshots in alphanumerical filename order and replaces the existing set rather than adding to it.

How does fastlane authenticate with App Store Connect on CI?

Use an App Store Connect API key: pass it with the app_store_connect_api_key action or point deliver at a JSON key file with api_key_path. This avoids Apple ID passwords and two-factor prompts.

What credentials does fastlane supply need?

A JSON key for a Google Cloud service account that has been granted access to your app in Play Console. Pass the file path with json_key or the file content with json_key_data.

Related guides

Make your store screenshots now — free

Templates, device mockups, AI translation and exact-size export for both stores. No account, no watermark.