📲 Why mobile releases need to be managed in 2025 — Webinar
📲 Why mobile releases need to be managed in 2025 — Webinar

How to upload assets using the App Store Connect API

When you create a new version of your app in App Store Connect, you might also need to upload new assets like screenshots or previews for the App Store. You can do this manually on the App Store Connect website, but if you have a lot of localizations to support and your assets change often across versions, this can be a tedious, error-prone, and time-consuming process.

Thankfully, you can automate this process using the App Store Connect API, which has a set of endpoints tailored for uploading media such as screenshots and previews. Unfortunately, using these endpoints is not straightforward and they require numerous steps that I will cover in great detail in this article.

While this article uses the App Store Connect API directly, you might want to consider a tool like Fastlane, specifically the <code>deliver<code> action, which abstracts most of the complexity of the API away and provides a more user-friendly interface, or services with built-in App Store Connect API integrations such as Runway (hey, that's us!).

The process

The App Store Connect API has a comprehensive guide that outlines the process of uploading assets and all the steps that developers need to follow. The process can be summarized in 3 key steps:

  1. Making an asset reservation: Before uploading any data, you must first tell the App Store Connect API what kind of asset you will be uploading and how big it is. If successful, the response to this request will contain the different requests you must make to upload the asset as well as the ID and information about the entity for which you are uploading an asset.‍
  2. Uploading the asset: After a successful reservation, you will have all the requested information needed to upload the asset. As part of the reservation, the App Store Connect API will split the asset into one or more parts and provide you with the URL, headers, offset and size required for each of them. You can at this point split the asset into all its different parts and upload them in parallel.‍
  3. Committing the reservation: If (and only if) all parts of the asset have been successfully uploaded, you can then make a request to the App Store Connect API telling it that the asset is complete. In this step, you also need to send the checksum of the full asset to the API.

As you can see in the steps above, the process is complex and involves file handling, data manipulation, cryptography and concurrent network requests, which can complicate things significantly. Over the next few sections, you will learn how to implement each of these steps in a familiar language for mobile developers: Swift.

Uploading a screenshot to App Store Connect

For the rest of the article, we will show how the process works by uploading a single screenshot for a specific version's localization using the App Store Connect API and writing a function for each of the steps mentioned previously.

The first thing you need to do to interact with the App Store Connect API is to create an API key and use it to generate a JWT token that you will use to authenticate all your requests. As we mentioned in our Hitchhiker's guide to the App Store Connect API, you should leverage the power of existing libraries that handle the authentication process for you and give you type-safe access to the API's endpoints.

For this article, we will use the appstoreconnect-swift-sdk, a Swift package that provides a type-safe way to interact with the App Store Connect API. Setting up the SDK is as straightforward as initializing an <code>APIConfiguration<code> object with either a team or individual API key and then using it to create an <code>APIClient<code> object that you can use to make requests to the API:

struct ScreenshotSet {
   let id: String
   let type: ScreenshotDisplayType
}


func uploadScreenshot(_ file: URL, to set: ScreenshotSet) async throws {
   let configuration = try APIConfiguration(
       individualPrivateKeyID: "🔐",
       individualPrivateKey: "🔐"
   )


   let provider = APIProvider(configuration: configuration)
}

Screenshots need to always be part of a set that describes the device type and provides dimension constraints. For this reason, we will need to pass the <code>ScreenshotSet<code> object that we want to upload the screenshot to, as well as the URL of the file we want to upload.

Making an asset reservation

The first step in the process is to make a reservation for the asset you want to upload by making a <code>POST<code> request to the endpoint. The endpoint will then create a screenshot entity and return information about the new entity such as the id as well as all the information needed to upload the asset.

In the body of the <code>POST<code> request, we must send the file name and its size and tell the API which screenshot set we are uploading the asset to:

func createReservation(inSet set: String, fileName: String, imageData: Data, provider: APIProvider) async throws -> (String, [URLRequest]) {
   // 1
   let screenshotSetData = AppScreenshotCreateRequest.Data.Relationships.AppScreenshotSet.Data(type: .appScreenshotSets, id: set)
   let screenshotSet = AppScreenshotCreateRequest.Data.Relationships.AppScreenshotSet(data: screenshotSetData)
   let relationships = AppScreenshotCreateRequest.Data.Relationships(appScreenshotSet: screenshotSet)
   let attributes = AppScreenshotCreateRequest.Data.Attributes(
       fileSize: imageData.count,
       fileName: fileName
   )
   let data = AppScreenshotCreateRequest.Data(
       type: .appScreenshots,
       attributes: attributes,
       relationships: relationships
   )
   let body = AppScreenshotCreateRequest(data: data)


   let reservation = APIEndpoint
       .v1
       .appScreenshots
       .post(body)


   // 2
   let reservationResponse = try await provider.request(reservation)
  
   // 3
   let requests = reservationResponse
       .data
       .attributes?
       .uploadOperations?
       .compactMap { uploadOperation -> URLRequest? in
           guard let urlString = uploadOperation.url,
                 let url = URL(string: urlString),
                 let method = uploadOperation.method,
                 let offset = uploadOperation.offset,
                 let length = uploadOperation.length else {
               return nil
           }
          
           // 4
           let chunk = imageData[offset..<(offset + length)]
          
           // 5
           var request = URLRequest(url: url)
           request.httpMethod = method
           for header in (uploadOperation.requestHeaders ?? []) {
               if let name = header.name {
                   request.setValue(header.value, forHTTPHeaderField: name)
               }
           }
           request.httpBody = chunk
          
           return request
       } ?? []
  
   // 6
   return (reservationResponse.data.id, requests)
}

A lot is going on in the function above, so let's break it down:

  1. We create a <code>POST<code> request to the <code>appScreenshots<code> endpoint with the file name and size in the body of the request through the body's attributes field, which we pass as parameters to the function. We also specify the screenshot set we want to upload the asset to by using the body's relationships field.
  2. We make the request to the API and get the response back.
  3. We parse the response and extract the upload operations to map them into an array of <code>URLRequest<code> objects that we will use to upload the asset.
  4. We get an image data chunk based on the offset and length provided by the API.
  5. We create a <code>URLRequest<code> object for each upload operation and set the method, headers and body of the request based on the information provided by the API.
  6. We return the reservation ID and the array of requests that we will use to upload the asset.
Curious to see how your team's mobile releases stack up?
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?
Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.
Try our free tool ->
Sign up for the Flight Deck — our monthly newsletter.
We'll share our perspectives on the mobile landscape, peeks into how other mobile teams and developers get things done, technical guides to optimizing your app for performance, and more. (See a recent issue here)
The App Store Connect API is very powerful, but it can quickly become a time sink.
Runway offers a lot of the functionality you might be looking for — and more — out‑of‑the‑box and maintenance‑free.
Learn more

Now that we have the method to reserve the asset, let's add it to the <code>uploadScreenshot<code> function we created earlier:

func uploadScreenshot(_ file: URL, to set: ScreenshotSet) async throws {
   let configuration = try! APIConfiguration(
       individualPrivateKeyID: "🔐",
       individualPrivateKey: "🔐"
   )


   let provider = APIProvider(configuration: configuration)


   let imageData = try Data(contentsOf: file)
   let (reservationId, uploadRequests) = try await createReservation(
       in: set.id,
       fileName: file.lastPathComponent,
       imageData: imageData,
       provider: provider
   )
}

Uploading the asset

Now that we have the screenshot ID and the list of <code>URLRequests<code> we need to call, we can start uploading the asset. The most efficient way to do this is to upload all the parts in parallel using structure concurrency and wait for all the requests to finish before moving on to the next step:

func uploadAssets(with requests: [URLRequest]) async throws {
   let session = URLSession.shared


   _ = try await withThrowingTaskGroup(of: Data.self, returning: [Data].self) { taskGroup in
       for request in requests {
           taskGroup.addTask {
               let (data, _) = try await session.data(for: request)
               return data
           }
       }


       var responses = [Data]()
       for try await result in taskGroup {
           responses.append(result)
       }
       return responses
   }
}

As you can see, the method above uses a <code>TaskGroup<code> to parallelize the network requests for all different parts of the asset. This request does not need to be authenticated. As we did before, let's add this method to the <code>uploadScreenshot<code> function:

func uploadScreenshot(_ file: URL, to set: ScreenshotSet) async throws {
   let configuration = try! APIConfiguration(
       individualPrivateKeyID: "🔐",
       individualPrivateKey: "🔐"
   )


   let provider = APIProvider(configuration: configuration)


   let imageData = try Data(contentsOf: file)
   let (reservationId, uploadRequests) = try await createReservation(
       in: set.id,
       fileName: file.lastPathComponent,
       imageData: imageData,
       provider: provider
   )
   try await uploadAssets(with: uploadRequests)
}

Committing the reservation

Last but not least, we need to tell the API that we have successfully uploaded all parts of the asset and that it can now update the status of the entity to <code>UPLOAD_COMPLETE<code>. To do this, we need to make a <code>PATCH<code> request to the endpoint.

The request body should contain the <code>md5<code> checksum of the full asset and a flag that tells the API that the asset has been uploaded:

func commitReservations(for reservation: String, with imageData: Data, provider: APIProvider) async throws {
   let bytes = Array(Crypto.Insecure.MD5.hash(data: imageData).makeIterator())
   let checksum =  bytes.map { String(format: "%02x", $0) }.joined()
  
   let screenshotUpdateRequestAttributes = AppScreenshotUpdateRequest.Data.Attributes(
       sourceFileChecksum: checksum,
       isUploaded: true
   )
   let screenshotUpdateRequestData = AppScreenshotUpdateRequest.Data(
       type: .appScreenshots,
       id: reservation,
       attributes: screenshotUpdateRequestAttributes
   )
   let screenshotUpdateRequest = AppScreenshotUpdateRequest(data: screenshotUpdateRequestData)
   let reservationCommitment = APIEndpoint
       .v1
       .appScreenshots
       .id(reservation)
       .patch(screenshotUpdateRequest)


   _ = try await provider.request(reservationCommitment)
}

Let's finally add this method to the <code>uploadScreenshot<code> function:

func uploadScreenshot(_ file: URL, to set: ScreenshotSet) async throws {
   let configuration = try! APIConfiguration(
       individualPrivateKeyID: "🔐",
       individualPrivateKey: "🔐"
   )


   let provider = APIProvider(configuration: configuration)


   let imageData = try Data(contentsOf: file)
   let (reservationId, uploadRequests) = try await createReservation(
       in: set.id,
       fileName: file.lastPathComponent,
       imageData: imageData,
       provider: provider
   )
   try await uploadAssets(with: uploadRequests)
   try await commitReservations(for: reservationId, with: imageData, provider: provider)
}

And that's it! That's the full process of uploading a screenshot to App Store Connect using the App Store Connect API. You can now use the <code>uploadScreenshot<code> function to upload screenshots for all your localizations and device types in a fully automated way, and using a language you are familiar with!

App Development

Release better with Runway.

Runway integrates with all the tools you’re already using to level-up your release coordination and automation, from kickoff to release to rollout. No more cat-herding, spreadsheets, or steady drip of manual busywork.

Release better with Runway.

Runway integrates with all the tools you’re already using to level-up your release coordination and automation, from kickoff to release to rollout. No more cat-herding, spreadsheets, or steady drip of manual busywork.

Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?

Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.

Looking for a better way to distribute all your different flavors of builds, from one-offs to nightlies to RCs?

Give Build Distro a try! Sign up for Runway and see it in action for yourself.

Release better with Runway.

What if you could get the functionality you're looking for, without needing to use the ASC API at all? Runway offers you this — and more — right out-of-the-box, with no maintenance required.