How to Generate App Store Screenshots with an API
Automate localized App Store screenshots with Apple’s API or fastlane, including capture, upload, ordering, processing checks, and CI retries.
To automate App Store screenshots, generate deterministic images for every locale and display target, create an App Screenshot Set for each combination, create an App Screenshot resource for every image, reserve and upload each asset, commit the upload, then poll processing status before ordering the screenshots and submitting the version.
Apple models screenshots as resources rather than as files attached directly to a version. The main resources are appScreenshotSets, which are keyed by localization and display target, and appScreenshots, which belong to a set. Apple’s upload lifecycle is reservation, upload, commit, and processing verification. See the App Store Connect API documentation and Apple’s asset-upload documentation.
What you need before writing code
- An App Store Connect API key with its key ID, issuer ID, and downloaded
.p8private key. - The App Store app’s numeric resource ID.
- The App Store version localization ID for each language you publish.
- The display target required by each device family and storefront.
- Deterministic screenshot files with Apple-approved pixel dimensions and file types.
- A stable directory such as
screenshots/en-US/iphone-6.7/001.png.
Keep marketing copy outside the UI tests that create the screenshots. This lets you regenerate every locale without changing capture logic.
Apple API workflow
- Identify the localization and display target. Read the version localizations and choose the locale whose screenshots you are updating. A set belongs to one localization and one display target.
- Create or find an App Screenshot Set. Reuse an existing set when its locale and display target match. Create a new
appScreenshotSetsresource otherwise. - Create one App Screenshot resource per image. Send the filename and file size, associated with the set. The response contains the asset reservation details and upload operations.
- Reserve the asset. Treat the reservation response as authoritative; do not hard-code upload URLs.
- Upload the bytes. Perform the returned upload operation. Large files can require multiple parts, with the exact headers and byte ranges specified by Apple.
- Commit the upload. Commit only after every part has completed successfully.
- Poll processing. Read the screenshot resource until processing succeeds or fails. Processing is asynchronous.
- Order the set. Send the desired screenshot order after all assets are processed. The order is the order shown in App Store Connect.
- Fail the release when an asset is rejected. Preserve the processing error and file path in CI logs, fix the source image, and repeat the resource upload.
Authentication with an App Store Connect API key
App Store Connect API requests use a JWT signed with the private key. Keep the .p8 file and issuer ID in CI secrets. Grant the key only the App Store Connect role needed by the pipeline. The token normally has a short lifetime, so create it immediately before a batch.
export ASC_KEY_ID="ABC123DEFG"
export ASC_ISSUER_ID="00000000-0000-0000-0000-000000000000"
export ASC_PRIVATE_KEY_PATH="$PWD/AuthKey_ABC123DEFG.p8"
export APP_ID="1234567890"
export VERSION_LOCALIZATION_ID="1234567891"
Minimal resource requests
The following request shapes show the resource relationships. Replace IDs with values from your account and use the exact attributes accepted by the current API version.
POST /v1/appScreenshotSets
{
"data": {
"type": "appScreenshotSets",
"attributes": {
"displayType": "APP_IPHONE_67"
},
"relationships": {
"appStoreVersionLocalization": {
"data": {
"type": "appStoreVersionLocalizations",
"id": "VERSION_LOCALIZATION_ID"
}
}
}
}
}
POST /v1/appScreenshots
{
"data": {
"type": "appScreenshots",
"attributes": {
"fileName": "001.png",
"fileSize": 482193
},
"relationships": {
"appScreenshotSet": {
"data": {
"type": "appScreenshotSets",
"id": "SCREENSHOT_SET_ID"
}
}
}
}
}
Use the upload operations returned for the new screenshot. Do not assume that every response has the same number of operations or that an upload is always single-part.
Python orchestration example
This example uses pyjwt and requests. The JSON:API resource creation and processing loop are complete; the upload function follows the reservation operations returned by Apple, including multipart operations when present.
import base64, hashlib, os, time
from pathlib import Path
import jwt
import requests
API = "https://api.appstoreconnect.apple.com/v1"
KEY_ID = os.environ["ASC_KEY_ID"]
ISSUER = os.environ["ASC_ISSUER_ID"]
PRIVATE_KEY = Path(os.environ["ASC_PRIVATE_KEY_PATH"]).read_text()
def token():
now = int(time.time())
return jwt.encode(
{"iss": ISSUER, "iat": now, "exp": now + 900, "aud": "appstoreconnect-v1"},
PRIVATE_KEY, algorithm="ES256", headers={"kid": KEY_ID}
)
def request(method, path, **kwargs):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {token()}"
headers["Content-Type"] = "application/json"
r = requests.request(method, API + path, headers=headers, timeout=90, **kwargs)
r.raise_for_status()
return r.json() if r.content else None
def create_screenshot(set_id, path):
payload = {"data": {"type": "appScreenshots", "attributes": {
"fileName": path.name, "fileSize": path.stat().st_size
}, "relationships": {"appScreenshotSet": {"data": {
"type": "appScreenshotSets", "id": set_id
}}}}}
return request("POST", "/appScreenshots", json=payload)["data"]
def upload_operations(screenshot, path):
# Apple returns upload operations in the asset reservation response.
for op in screenshot.get("attributes", {}).get("uploadOperations", []):
method = op["method"]
url = op["url"]
headers = {h["name"]: h["value"] for h in op.get("requestHeaders", [])}
with path.open("rb") as f:
body = f.read()
r = requests.request(method, url, headers=headers, data=body, timeout=180)
r.raise_for_status()
def wait_for_processing(screenshot_id):
for _ in range(60):
data = request("GET", f"/appScreenshots/{screenshot_id}")["data"]
state = data["attributes"].get("assetDeliveryState", {}).get("state")
if state in {"COMPLETE", "FAILED"}:
if state == "FAILED":
raise RuntimeError(data["attributes"].get("assetDeliveryState"))
return data
time.sleep(5)
raise TimeoutError("Screenshot processing did not finish")
# Example: create resources in the required order, then process each one.
set_id = os.environ["SCREENSHOT_SET_ID"]
for path in sorted(Path("screenshots/en-US/iphone-6.7").glob("*.png")):
screenshot = create_screenshot(set_id, path)
upload_operations(screenshot, path)
wait_for_processing(screenshot["id"])
cURL request pattern
Generate the JWT in your secret-management step, then pass it as a bearer token. The upload URL and headers must come from the reservation response.
curl -X POST "https://api.appstoreconnect.apple.com/v1/appScreenshots" \
-H "Authorization: Bearer $ASC_JWT" \
-H "Content-Type: application/json" \
--data '{"data":{"type":"appScreenshots","attributes":{"fileName":"001.png","fileSize":482193},"relationships":{"appScreenshotSet":{"data":{"type":"appScreenshotSets","id":"SCREENSHOT_SET_ID"}}}}}'
Node.js request pattern
const body = {
data: {
type: 'appScreenshots',
attributes: { fileName: '001.png', fileSize: 482193 },
relationships: {
appScreenshotSet: { data: { type: 'appScreenshotSets', id: process.env.SCREENSHOT_SET_ID } }
}
}
};
const res = await fetch('https://api.appstoreconnect.apple.com/v1/appScreenshots', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ASC_JWT}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const screenshot = await res.json();
console.log(screenshot.data.id);
Capture the images reliably
- Use XCUITest or equivalent UI automation to reach the same state every time.
- Inject locale, currency, account state, feature flags, and test data before capture.
- Wait for network-backed content and animations to settle before taking the image.
- Use one capture lane for each device family and display target.
- Validate dimensions, color mode, file type, and file size before creating an App Screenshot resource.
- Keep a manifest containing locale, display target, order, source commit, and checksum.
fastlane alternative
fastlane snapshot automates UI-driven captures on multiple simulators and languages and writes files to fastlane/screenshots. A typical lane combines capture_screenshots with upload_to_app_store; deliver can upload screenshots and metadata. frameit adds device frames, orientation, colors, backgrounds, and marketing text in software.
lane :screenshots do
capture_screenshots(
scheme: "MyAppUITests",
languages: ["en-US", "fr-FR", "ja"],
devices: ["iPhone 16 Pro Max", "iPad Pro (13-inch) (M4)"]
)
upload_to_app_store(skip_metadata: true)
end
Ordering and CI design
- Generate all images into a clean build directory.
- Validate the manifest and dimensions before calling Apple.
- Create or locate sets by locale and display target.
- Upload with bounded retries and exponential backoff for transient network errors.
- Poll every resource and fail the build if any processing state is rejected.
- Apply ordering from the manifest only after processing succeeds.
- Archive the generated files, manifest, API responses, and commit SHA.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 or 403 | Expired JWT, wrong audience, key ID, issuer, or insufficient role. | Create a fresh token, verify the .p8 key and IDs, and grant the minimum role that can manage screenshots. |
| 422 when creating a set | Invalid localization, display target, or duplicate relationship. | Read the version localizations, reuse the matching set, and use the display target accepted for that device family. |
| Upload succeeds but processing fails | Wrong dimensions, file type, corrupt bytes, or an unsupported image. | Validate the source before reservation and inspect the returned processing error. |
| Only some screenshots appear | One resource is still processing or was rejected. | Poll every resource and stop submission until all required targets are complete. |
| Order is unexpected | Files were uploaded without an explicit ordering request. | Send the order from a stable manifest after processing. |
| CI times out | Processing is asynchronous and the timeout is too short. | Use a polling deadline appropriate for the batch, log each state transition, and retry only safe operations. |
| Duplicate screenshots | A retry created a new resource after the first request succeeded. | Persist resource IDs and use idempotent job records so retries resume instead of creating duplicates. |
Performance, reliability, and cost
- Capture simulators concurrently, but limit concurrency to what your CI machine can run consistently.
- Upload independent assets in parallel while preserving per-asset retry state.
- Hash files and skip unchanged resources in your own pipeline.
- Refresh JWTs for long batches instead of reusing an expired token.
- Use exponential backoff for network failures and never re-upload a part that already returned success unless the operation requires it.
- Apple API usage and App Store Connect processing are separate from simulator costs and CI minutes; budget those resources independently.
Or skip the browser setup
ScreenshotNeo is useful when the asset you need is a web page or web marketing surface rather than a native simulator screen. It returns a PNG, JPEG, WebP, or PDF from one GET request. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
There is a free plan with 1,000 screenshots a month and no card. Paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Can one screenshot set contain several locales?
No. A set is associated with one App Store version localization and one display target. Create or reuse a set for each combination.
Do I have to upload screenshots in display order?
No. Upload resources, wait for processing, then apply an explicit order.
Should I use the direct API or fastlane?
Use the direct API for custom orchestration, artifact storage, and fine-grained retries. Use fastlane when you want a ready-made simulator capture and delivery pipeline.
Can ScreenshotNeo replace App Store Connect uploads?
No. ScreenshotNeo captures web URLs. App Store Connect still requires App Screenshot Sets, App Screenshot resources, asset uploads, processing checks, and ordering for native app listing assets.


