Manage App Store screenshot sets and individual screenshots for an app version localization via the App Store Connect API.
List all screenshot sets for a given App Store version localization. Each set represents one display type (e.g. iPhone 6.7", iPad Pro 12.9").
asc screenshot-sets list --localization-id <LOCALIZATION_ID>Options:
| Flag | Default | Description |
|---|---|---|
--localization-id |
(required) | App Store version localization ID |
--output |
json |
Output format: json, table, markdown |
--pretty |
false |
Pretty-print JSON |
Examples:
# Default JSON output
asc screenshot-sets list --localization-id abc123def456
# Table view
asc screenshot-sets list --localization-id abc123def456 --output table
# Pipe into jq
asc screenshot-sets list --localization-id abc123def456 | jq '.[].screenshotDisplayType'Table output:
ID Display Type Device Count
-------------------- ------------------------ ------- -----
set-aaa iPhone 6.7" iPhone 5
set-bbb iPad Pro 12.9" (3rd gen) iPad 3
set-ccc Mac mac 0
Create a new screenshot set for a display type within a localization.
asc screenshot-sets create --localization-id <LOCALIZATION_ID> --display-type <TYPE>Options:
| Flag | Default | Description |
|---|---|---|
--localization-id |
(required) | App Store version localization ID |
--display-type |
(required) | Display type raw value (e.g. APP_IPHONE_67) |
--output |
json |
Output format: json, table, markdown |
--pretty |
false |
Pretty-print JSON |
Example:
asc screenshot-sets create --localization-id abc123 --display-type APP_IPHONE_67List individual screenshots within a screenshot set.
asc screenshots list --set-id <SET_ID>Options:
| Flag | Default | Description |
|---|---|---|
--set-id |
(required) | Screenshot set ID |
--output |
json |
Output format: json, table, markdown |
--pretty |
false |
Pretty-print JSON |
Examples:
# Default JSON output
asc screenshots list --set-id set-aaa
# Table view — shows file name, size, dimensions, delivery state
asc screenshots list --set-id set-aaa --output table
# Markdown for documentation
asc screenshots list --set-id set-aaa --output markdownTable output:
ID File Name Size Dimensions State
-------- ---------------- ------- ------------ ---------------
img-001 screen_01.png 2.8 MB 2796 × 1290 Complete
img-002 screen_02.png 2.4 MB 2796 × 1290 Complete
img-003 pending.png 0 B - Awaiting Upload
Upload a screenshot image file to a screenshot set. Internally orchestrates the three-step ASC API flow (reserve → S3 upload → commit).
asc screenshots upload --set-id <SET_ID> --file <PATH>Options:
| Flag | Default | Description |
|---|---|---|
--set-id |
(required) | Screenshot set ID |
--file |
(required) | Local path to the image file |
--output |
json |
Output format: json, table, markdown |
--pretty |
false |
Pretty-print JSON |
Example:
asc screenshots upload --set-id set-aaa --file ./screens/iphone_hero.png# 1. Find your app
asc apps list --output table
# 2. Find or create a version
asc versions list --app <APP_ID> --output table
# 3. List localizations for the version
asc version-localizations list --version-id <VERSION_ID> --output table
# 4. List screenshot sets for a localization
asc screenshot-sets list --localization-id <LOCALIZATION_ID> --output table
# 5. Create a set if needed
asc screenshot-sets create --localization-id <LOCALIZATION_ID> --display-type APP_IPHONE_67
# 6. Upload screenshots
asc screenshots upload --set-id <SET_ID> --file ./screens/screen01.png
# 7. Verify upload
asc screenshots list --set-id <SET_ID> --output tableEach response includes an affordances field with ready-to-run follow-up commands, so an AI agent can navigate the hierarchy without knowing the full command tree.
┌──────────────────────────────────────────────────────────────────────┐
│ Screenshots Feature │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ASC API Infrastructure Domain │
│ ┌──────────────────────┐ ┌────────────────────┐ ┌─────────────┐ │
│ │ GET /v1/ │ │ │ │AppScreenshot│ │
│ │ appStoreVersionLocal │─▶│ SDKScreenshot │─▶│Set (struct) │ │
│ │ izations/{id}/ │ │ Repository │ └─────────────┘ │
│ │ appScreenshotSets │ │ │ ┌─────────────┐ │
│ │ │ │ (implements │─▶│AppScreenshot│ │
│ │ POST /v1/ │ │ ScreenshotRepo- │ │ (struct) │ │
│ │ appScreenshotSets │ │ sitory) │ └─────────────┘ │
│ │ │ │ │ ┌─────────────┐ │
│ │ GET /v1/ │ │ Upload: 3 API │─▶│Screenshot │ │
│ │ appScreenshotSets/ │─▶│ calls internally │ │DisplayType │ │
│ │ {id}/appScreenshots │ │ (reserve → S3 → │ │ (enum) │ │
│ │ │ │ commit) │ └─────────────┘ │
│ │ POST /v1/ │ │ │ ┌─────────────┐ │
│ │ appScreenshots │ └────────────────────┘ │AppStoreVer- │ │
│ │ PATCH /v1/ │ │sionLocaliz- │ │
│ │ appScreenshots/{id} │ │ation │ │
│ └──────────────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ASCCommand Layer │ │
│ │ asc screenshot-sets list --localization-id <id> │ │
│ │ asc screenshot-sets create --localization-id <id> │ │
│ │ asc screenshots list --set-id <id> │ │
│ │ asc screenshots upload --set-id <id> --file <path> │ │
│ └───────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Dependency direction: ASCCommand → Infrastructure → Domain
The domain layer has zero dependency on the SDK or networking. Infrastructure adapts SDK types to domain types. Commands depend only on ScreenshotRepository (the protocol), never on the SDK directly.
An enum mapping all ASC display type raw values to human-readable names and device categories.
public enum ScreenshotDisplayType: String, Sendable, CaseIterable {
case iphone67 = "APP_IPHONE_67"
case ipadPro3gen129 = "APP_IPAD_PRO_3GEN_129"
case desktop = "APP_DESKTOP"
case appleVisionPro = "APP_APPLE_VISION_PRO"
// ... 39 cases total
public var deviceCategory: DeviceCategory { ... }
public var displayName: String { ... } // e.g. "iPhone 6.7\""
}Device categories: iPhone, iPad, mac, watch, appleTV, appleVisionPro, iMessage
A localization record tying a version to a locale (e.g. en-US). Carries its parent versionId for upward navigation.
public struct AppStoreVersionLocalization: Sendable, Equatable, Identifiable, Codable {
public let id: String
public let versionId: String // Parent ID, always injected by Infrastructure
public let locale: String // "en-US", "zh-Hans", etc.
// Affordances
"listScreenshotSets": "asc screenshot-sets list --localization-id <id>"
"listLocalizations": "asc version-localizations list --version-id <versionId>"
}A container of screenshots for one display type within a localization. ASC creates one set per supported display type.
public struct AppScreenshotSet: Sendable, Codable, Equatable, Identifiable {
public let id: String
public let localizationId: String // Parent ID, always injected
public let screenshotDisplayType: ScreenshotDisplayType
public let screenshotsCount: Int // Defaults to 0
// Convenience
public var isEmpty: Bool // screenshotsCount == 0
public var deviceCategory: ScreenshotDisplayType.DeviceCategory
public var displayTypeName: String // e.g. "iPhone 6.7\""
// Affordances
"listScreenshots": "asc screenshots list --set-id <id>"
"listScreenshotSets": "asc screenshot-sets list --localization-id <localizationId>"
}An individual screenshot within a set.
public struct AppScreenshot: Sendable, Codable, Equatable, Identifiable {
public let id: String
public let setId: String // Parent ID, always injected
public let fileName: String
public let fileSize: Int
public let assetState: AssetDeliveryState? // nil if not yet determined
public let imageWidth: Int?
public let imageHeight: Int?
// Convenience
public var isComplete: Bool // assetState == .complete
public var fileSizeDescription: String // "2.8 MB", "512 B"
public var dimensionsDescription: String? // "2796 × 1290", or nil
}public enum AssetDeliveryState: String, Sendable, Codable {
case awaitingUpload = "AWAITING_UPLOAD"
case uploadComplete = "UPLOAD_COMPLETE"
case complete = "COMPLETE"
case failed = "FAILED"
public var isComplete: Bool // ready for App Store submission
public var hasFailed: Bool
public var displayName: String // "Complete", "Failed", etc.
}The DI boundary between the command layer and the API. Annotated with @Mockable for testing.
@Mockable
public protocol ScreenshotRepository: Sendable {
func listLocalizations(versionId: String) async throws -> [AppStoreVersionLocalization]
func createLocalization(versionId: String, locale: String) async throws -> AppStoreVersionLocalization
func listScreenshotSets(localizationId: String) async throws -> [AppScreenshotSet]
func createScreenshotSet(localizationId: String, displayType: ScreenshotDisplayType) async throws -> AppScreenshotSet
func listScreenshots(setId: String) async throws -> [AppScreenshot]
func uploadScreenshot(setId: String, fileURL: URL) async throws -> AppScreenshot
}Sources/
├── Domain/Screenshots/
│ ├── ScreenshotDisplayType.swift # Enum: 39 display types with names + categories
│ ├── AppScreenshotSet.swift # Value type: set container with affordances
│ ├── AppScreenshot.swift # Value type: individual screenshot + AssetDeliveryState
│ └── ScreenshotRepository.swift # @Mockable protocol
│
├── Infrastructure/Screenshots/
│ └── OpenAPIScreenshotRepository.swift # SDKScreenshotRepository: maps SDK → domain
│
└── ASCCommand/Commands/
├── ScreenshotSets/
│ └── ScreenshotSetsCommand.swift # ScreenshotSetsCommand + ScreenshotSetsList + ScreenshotSetsCreate
└── Screenshots/
└── ScreenshotsCommand.swift # ScreenshotsCommand + ScreenshotsList + ScreenshotsUpload
Tests/
├── DomainTests/Screenshots/
│ ├── ScreenshotDisplayTypeTests.swift # Category logic, display names, raw value round-trips
│ ├── AppScreenshotSetTests.swift # isEmpty, delegation, parent ID injection
│ ├── AppScreenshotTests.swift # isComplete, formatting, asset state behavior
│ └── ScreenshotRepositoryTests.swift # Mock protocol usage patterns
├── InfrastructureTests/Screenshots/
│ ├── SDKScreenshotRepositoryTests.swift # Mapping + parent ID injection for list methods
│ └── SDKScreenshotRepositoryCreateTests.swift # Mapping for create methods
├── ASCCommandTests/Commands/Screenshots/
│ ├── ScreenshotsListTests.swift # JSON output and argument passing
│ └── ScreenshotsUploadTests.swift # JSON output and argument passing
├── ASCCommandTests/Commands/ScreenshotSets/
│ ├── ScreenshotSetsListTests.swift # JSON output includes affordances
│ └── ScreenshotSetsCreateTests.swift # JSON output and argument passing
└── DomainTests/TestHelpers/
└── MockRepositoryFactory.swift # makeScreenshotSet(), makeScreenshot(), makeLocalization()
Wiring files modified:
| File | Change |
|---|---|
Sources/Infrastructure/Client/ClientFactory.swift |
Added makeScreenshotRepository(authProvider:) |
Sources/ASCCommand/ClientProvider.swift |
Added makeScreenshotRepository() |
Sources/ASCCommand/ASC.swift |
Added ScreenshotSetsCommand.self + ScreenshotsCommand.self |
| Endpoint | SDK call | Used by |
|---|---|---|
GET /v1/appStoreVersions/{id}/appStoreVersionLocalizations |
.appStoreVersions.id(id).appStoreVersionLocalizations.get() |
listLocalizations |
POST /v1/appStoreVersionLocalizations |
.appStoreVersionLocalizations.post(body) |
createLocalization |
GET /v1/appStoreVersionLocalizations/{id}/appScreenshotSets |
.appStoreVersionLocalizations.id(id).appScreenshotSets.get() |
listScreenshotSets |
POST /v1/appScreenshotSets |
.appScreenshotSets.post(body) |
createScreenshotSet |
GET /v1/appScreenshotSets/{id}/appScreenshots |
.appScreenshotSets.id(id).appScreenshots.get() |
listScreenshots |
POST /v1/appScreenshots |
.appScreenshots.post(body) |
uploadScreenshot step 1: reserve |
| (S3 direct upload) | URLSession with MD5 checksum |
uploadScreenshot step 2: binary |
PATCH /v1/appScreenshots/{id} |
.appScreenshots.id(id).patch(body) |
uploadScreenshot step 3: commit |
The SDK is from appstoreconnect-swift-sdk. SDKScreenshotRepository is marked @unchecked Sendable because APIProvider predates Swift 6 concurrency.
Tests follow the Chicago school TDD pattern: assert on state and return values, not on interactions.
@Test
func `list screenshot sets returns sets for localization`() async throws {
let mock = MockScreenshotRepository()
given(mock).listScreenshotSets(localizationId: .any).willReturn([
MockRepositoryFactory.makeScreenshotSet(id: "set-1", displayType: .iphone67),
])
let result = try await mock.listScreenshotSets(localizationId: "loc-123")
#expect(result[0].screenshotDisplayType == .iphone67)
}Run the full test suite:
swift test
# or
make testThe natural next steps follow the same layer-by-layer pattern:
// 1. Domain protocol (ScreenshotRepository.swift)
func deleteScreenshot(id: String) async throws
func deleteScreenshotSet(id: String) async throws
// 2. Infrastructure SDK calls
APIEndpoint.v1.appScreenshots.id(id).delete
APIEndpoint.v1.appScreenshotSets.id(id).delete
// 3. New subcommands in ScreenshotsCommand / ScreenshotSetsCommand// PATCH /v1/appScreenshotSets/{id}/relationships/appScreenshots
func reorderScreenshots(setId: String, orderedIds: [String]) async throws- Add method to
ScreenshotRepositoryprotocol inSources/Domain/Screenshots/ - Implement in
SDKScreenshotRepositoryinSources/Infrastructure/Screenshots/ - Add subcommand in
Sources/ASCCommand/Commands/Screenshots/orScreenshotSets/ - Write domain tests first (Red → Green → Refactor)