🔀 How to untangle and manage build distribution — Webinar, May 16th — Register
🔀 How to untangle and manage build distribution — Webinar, May 16th — Register

How to automatically apply promotional text and ‘What’s new’ notes to new versions in Swift using the App Store Connect API

There are certain pieces of information that you need to provide when creating a new version of your app in App Store Connect that don't tend to change often. For example, the promotional text is a field that rarely changes, yet by default Apple does not populate it for you with the previous version's value when you create a new version.

Another example is the 'What's New' version field. While it is good to change it on releases that contain noteworthy changes, it is also common to have a default message that can be used as a fallback when there is no relevant information to provide to your users.

In this article, we will show you how to use the App Store Connect API to build an automation that pre-fills the information for the two examples above when you create a new version of your app so that you can save some time and effort and remove human error.

Authenticating and setting up the API client

We have previously written about how to leverage the power of the App Store Connect API's OpenAPI definitions to generate a ready-to-use Swift networking client. A library that I help to maintain, AppStoreConnect_Swift_SDK does exactly this and provides a simple and type-safe way to interact with the App Store Connect API all ready to use in the form of a Swift package.

For the rest of the article, we will use the <code>AppStoreConnect_Swift_SDK<code> to make network requests to the App Store Connect API. To get started, all you need to do is to create an API key in App Store Connect and pass the key ID, issuer ID and private key when configuring <code>AppStoreConnect_Swift_SDK<code>'s <code>APIProvider<code>:

import AppStoreConnect_Swift_SDK


let configuration = try APIConfiguration(
   issuerID: "",
   privateKeyID: "",
   privateKey: ""
)


let provider = APIProvider(configuration: configuration)

Creating a new version of your app

The first step to shipping a new version of your app is to create a new <code>appStoreVersion<code> in App Store Connect. This can be done by making a <code>POST<code> request to the <code>appStoreVersions<code> endpoint with a set of attributes:

import AppStoreConnect_Swift_SDK


func createVersion(forApp app: String, withPlatform platform: Platform, andVersion version: String) async throws -> String {
   let appData = AppStoreVersionCreateRequest.Data.Relationships.App.Data(
       type: .apps,
       id: app
   )
   let app = AppStoreVersionCreateRequest.Data.Relationships.App(data: appData)
   let relationships = AppStoreVersionCreateRequest.Data.Relationships(app: .init(data: appData))
   let attributes = AppStoreVersionCreateRequest.Data.Attributes(platform: platform, versionString: version)
   let data = AppStoreVersionCreateRequest.Data(
       type: .appStoreVersions,
       attributes: attributes,
       relationships: relationships
   )
  
   let versionRequest = APIEndpoint
       .v1
       .appStoreVersions
       .post(.init(data: data))
  
   let response = try await provider.request(versionRequest).data
  
   return response.id
}

As you can see in the code above, creating a new version is a simple process. You just need to provide the app id, platform and version strings and make the <code>POST<code> request.

Fetching the promotional text from the previous version information

If you look at the new version of your app you have just created in App Store Connect, you will notice that the promotional text is empty and, as it is not a required field, you can submit and ship the new version without it.

Promotional text doesn't tend to change often and, to avoid forgetting to set it when you create a new version, you can build a small automation that fetches the latest live version's promotional text and applies it to your new version.

import AppStoreConnect_Swift_SDK


func getLatestLiveVersionPromotionalText(forApp app: String, withPlatform platform: Platform) async throws -> [AppStoreLanguage: String] {
   let platform: APIEndpoint.V1.Apps.WithID.AppStoreVersions.GetParameters.FilterPlatform = {
       switch platform {
       case .ios: return .ios
       case .macOs: return .macOs
       case .tvOs: return .tvOs
       case .visionOs: return .visionOs
       }
   }()


   let endpoint = APIEndpoint
       .v1
       .apps
       .id(app)
       .appStoreVersions
       .get(
           parameters: .init(
               filterAppVersionState: [
                   .processingForDistribution,
                   .readyForDistribution,
                   .pendingAppleRelease
               ],
               filterPlatform: [platform],
               fieldsAppStoreVersionLocalizations: [.promotionalText, .locale],
               limit: 1,
               include: [.appStoreVersionLocalizations]
           )
       )


   let response = try await provider.request(endpoint)
   let localizations = response.included?
       .compactMap { item in
           if case .appStoreVersionLocalization(let localization) = item {
               return localization
           }
           return nil
       }


   let easyAccessLocalizations: [String: AppStoreVersionLocalization] = localizations?
       .reduce(into: [String: AppStoreVersionLocalization](), { partialResult, localization in
           partialResult[localization.id] = localization
       }) ?? [:]


   let latestVersionLocalizations = response
       .data
       .first?
       .relationships?
       .appStoreVersionLocalizations?
       .data?
       .reduce(into: [AppStoreLanguage: String]()) { partialResult, localization in
           if let matchedLocalization = easyAccessLocalizations[localization.id],
               let promotionalText = matchedLocalization.attributes?.promotionalText,
               let language = AppStoreLanguage(rawValue: matchedLocalization.attributes?.locale ?? "") {
               partialResult[language] = promotionalText
           }
       }




   return latestVersionLocalizations ?? [:]
}

Note that we create a small helper enum called <code>AppStoreLanguage<code> to represent the different languages that the App Store Connect API supports:

enum AppStoreLanguage: String, CaseIterable, Codable {
   case arabic = "ar-SA"
   case catalan = "ca"
   case chineseSimplified = "zh-Hans"
   case chineseTraditional = "zh-Hant"
   case croatian = "hr"
   case czech = "cs"
   case danish = "da"
   case dutch = "nl-NL"
   case englishUS = "en-US"
   case englishAUS = "en-AU"
   case englishCAN = "en-CA"
   case englishUK = "en-GB"
   case finnish = "fi"
   case french = "fr-FR"
   case frenchCAN = "fr-CA"
   case german = "de-DE"
   case greek = "el"
   case hebrew = "he"
   case hindi = "hi"
   case hungarian = "hu"
   case indonesian = "id"
   case italian = "it"
   case japanese = "ja"
   case korean = "ko"
   case malay = "ms"
   case macedonian = "mk"
   case norwegian = "no"
   case polish = "pl"
   case portuguese = "pt-PT"
   case portugueseBRA = "pt-BR"
   case romanian = "ro"
   case russian = "ru"
   case slovak = "sk"
   case spanish = "es-ES"
   case spanishMEX = "es-MX"
   case swedish = "sv"
   case thai = "th"
   case turkish = "tr"
   case ukrainian = "uk"
   case vietnamese = "vi"
}

Fetching the localized version information

Now that we have created the version and retrieved the promotional text for all localizations in the latest live version, we’ll need to fetch the list of app store version localizations from the API that we can update with promotional text and release notes for each locale:

func fetchAllVersionLocalizations(forVersion versionId: String) async throws -> [(String, AppStoreLanguage)] {
   let endpoint = APIEndpoint
       .v1
       .appStoreVersions
       .id(versionId)
       .appStoreVersionLocalizations
       .get()
  
   return try await provider.request(endpoint)
       .data
       .compactMap { localization in
           guard let locale = localization.attributes?.locale, let language = AppStoreLanguage(rawValue: locale) else {
               return nil
           }
          
           return (localization.id, language)
       }
}

As you can see, we can make a <code>GET<code> request to the <code>appStoreVersionLocalizations<code> endpoint to fetch all the localizations and then map the response to just what we need, a tuple of the localization id and the locale.

Default release notes

Another important step that can remove friction from your releases is to always provide some default release notes when creating a new release. In this case, each release is different and we should not rely on the previous version's release notes.

Instead, we want to provide default release notes that can be used as a fallback when we don't need our product team to provide custom ones. In a similar way to the promotional text, release notes are localized and we need to provide them for each language we support:

// Default Release Notes
let defaultReleaseNotes: [AppStoreLanguage: String] = [
   .englishUK: "Bug fixes and performance improvements.",
   .spanish: "Corrección de errores y mejoras de rendimiento."
]

For the sake of this example, we have defined them in-memory but in a real-world scenario, you could fetch them from a configuration file or even from a CMS for easier management.

Updating version localizations

Let's now write a function that allows us to update fields for each app store version localization. Unfortunately, the App Store Connect API does not have an endpoint to bulk update localizations, so we must make one request per supported locale:

func update(
   promotionalText: String?,
   whatsNew: String?,
   forLanguage language: AppStoreLanguage,
   ofLocalization localization: String
) async throws {
   let attributes = AppStoreVersionLocalizationUpdateRequest.Data.Attributes(
       promotionalText: promotionalText,
       whatsNew: whatsNew
   )
   let data = AppStoreVersionLocalizationUpdateRequest.Data(
       type: .appStoreVersionLocalizations,
       id: localization,
       attributes: attributes
   )
   let request = AppStoreVersionLocalizationUpdateRequest(data: data)
   let endpoint = APIEndpoint
       .v1
       .appStoreVersionLocalizations
       .id(localization)
       .patch(request)
  
   _ = try await provider.request(endpoint)
}

As you can see, the request is straightforward. We need to call the <code>appStoreVersionLocalizations<code> endpoint again but this time with a <code>PATCH<code> method. We then pass two optional strings as parameters: one for the promotional text and one for the what's new.

Putting it all together

Last but not least, we need to put all the pieces together and write some code that orchestrates the creation of a new version, fetching the latest live version's promotional text, fetching all localizations for the newly created version, and updating them with the promotional text and release notes:


import AppStoreConnect_Swift_SDK


// Setup
let configuration = try APIConfiguration(
   issuerID: "",
   privateKeyID: "",
   privateKey: ""
)


let provider = APIProvider(configuration: configuration)


// App Information
let appId = ""
let platform = Platform.ios


// All promotional texts
let lastAvailablePromotionalTexts = try await getLatestLiveVersionPromotionalText(forApp: appId, withPlatform: platform)


// Default release notes
let defaultReleaseNotes: [AppStoreLanguage: String] = [
   .englishUK: "Bug fixes and performance improvements.",
   .spanish: "Corrección de errores y mejoras de rendimiento."
]


// Create a new version
let newVersionId = try await createVersion(forApp: appId, withPlatform: platform, andVersion: "1.0.0")


// Apply content
for (id, language) in try await fetchAllVersionLocalizations(forVersion: newVersionId) {
   try await update(
       promotionalText: lastAvailablePromotionalTexts[language],
       whatsNew: defaultReleaseNotes[language],
       forLanguage: language,
       ofLocalization: id
   )
}

That’s it! With the help of the AppStoreConnectSwift_SDK library, we’ve built a handy automation that uses the App Store Connect API to automate setting promotional text and default release notes on new app store versions, saving you time and manual effort when creating new releases. Here at Runway, we love automating away manual work and repetitive tasks associated with releases, and we have an automation just like this one ready for you to use out of the box with zero code required!

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 — outofthebox and maintenancefree.
Learn more
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.