For years, Apple has provided web services to developers through the Developer Portal and App Store Connect websites to make app releases, manage signing certificates, and gather reports about how much money you’re making from your apps.
To take advantage of these services, you can log into your App Store Connect account via your browser or through the app, then click around to look at stuff. Or you could instead take advantage of the App Store Connect API, which allows you to access a lot of the same functionality without needing to log into your account and look for it.
Use the API to monitor your app’s sales and downloads, calculate and view the average reviews for your latest release, get notified about and respond to reviews, fetch certificates and profiles, and more, using your own tooling instead of a website that only you and other developers can access.
The only thing that’s required to get started with the App Store Connect API is an API key, but to really make the most of out it — let’s say, so you can build your own internal tools powered by App Store Connect data — setting up an API client to authenticate with and make requests out to the App Store Connect API is a good idea.
How to started with an API client for App Store Connect?
The simplest way is to use the OpenAPI definitions. OpenAPI is a specification for defining network call schemas — endpoints to hit as well as the payloads they accept and send back. OpenAPI, as a specification, allows for code to be generated to use these network schemas, and just this past summer Apple announced their own OpenAPI generator for Swift code.
Check out the announcement session Meet Swift OpenAPI Generator from WWDC23.
In this post, we’ll build a Swift Package which can talk to the App Store Connect API for us using the code generated by the OpenAPI generator.
Let's start with a Package.swift manifest:
To use the OpenAPI tools there are 3 dependencies that we need to have: 1) the code generator, 2) the runtime types, and 3) the transport layer (in our case <code>URLSession<code> but there are others such as HTTPClient). We then add the code generator plugin to the main <code>APIClient<code> target so that it can run during a build. You don't want to invoke it manually — the files it generates will be copied to your project's Sources directory and also included from the plugin's output. This means the project won't build because it will have identical files in 2 different places.
Using the OpenAPI generator plugin
With this structure in place let's have a look at the OpenAPI generator. This is the plugin which will generate Swift code for us to interact with the App Store Connect API. In our package it is a plugin for the <code>APIClient<code> target, and inside of the Sources file it needs 2 things in the target's Sources:
- At file called <code>openapi.json<code>. It can have a <code>.yml<code> or <code>.yaml<code> file extension. This is the manifest where the API's definitions are declared.
- A file called <code>openapi-generator-config.yaml<code> This configuration file tells the generator things like which types to create and what their visibility is (public, internal, etc). Here is the full documentation on the generator and what it can configure.
One very important configuration option to call out is filtering. Filtering allows you to take in parts of the OpenAPI document rather than the whole thing, where for example you can filter by Operation Object. This comes in extremely handy for very large definition files like the App Store Connect manifest, where generating without a filter yielded a 20MB file of types, and a 5MB file for the client. These are Swift files that can have a lot of complexity, meaning that it takes a lot of computer resources to parse them in Xcode and they brought a new M3 Max MacBook Pro to a halt when trying to scroll one. Filter is your friend 😀
Once we have these pieces in place we can build the project. This will trigger the generator plugin and get our types and client in place from the OpenAPI manifest. For our purposes, let's try to make an API call to fetch the apps for our account. We can do that using this configuration:
So now let's look at our <code>APIClient<code> class and how it can make the call to this endpoint:
The first thing to note is that these generated method names are weird, and if you just look at the return type on that method it doesn't get any less weird:
<code>Operations.apps_hyphen_get_collection.Output<code>. There's a lot of usage of nested types here, and it will be helpful to consult the API documentation for any specific call you want to perform to see what is supposed to be sent in as an argument and what you can expect back. Navigating the generated Swift code will not be a fun exercise for you. So let's make a simple unit test for our API call and put a breakpoint in the response and see what comes back:
The actual return values and assertions don't matter here just yet as we are only after seeing what the network returns. And that the API call returns an unauthorized request. That brings us to authentication.
Authenticating an App Store Connect API call
Now we need to tell the API who we are and what App Store Connect account we belong to. There are a few steps to getting this done:
- Generate an API key
- Generate an auth token for a given request
- Add the token to each request
Points 2 and 3 can be helped out by our generated API client. We can create a "middleware" type which sits in between sending the request and receiving the response to modify it, and that middleware can inject our authentication credentials as a JSON Web Token (JWT). This requires some special encoding and cryptography to be done right. Thankfully the team behind the Vapor backend framework has us covered with a JWT library. So let's add this dependency:
and add it as a dependency to our <code>APIClient<code> target. We can then create a function to generate the JWT and use it as middleware:
By putting this into our <code>Client<code> initializer we can ensure that our requests get signed correctly:
With this in place you can re-run our test and see that it probably fails! Ironically this is exactly what we want because our method is actually returning the bundle identifiers for your apps and the test asserted that the array was going to be empty.
We have a working App Store Connect API call! Now what?
Now that we have a working call, let's take this one step further and make another API call building off the one we just made. First, a little refactor:
There's a lot going on here, especially conceptually:
- Create a new type that we can use in the API of our package (API in the true Application Programming Interface sense rather than the network call sense). We want to do this so that we can hide any details from the App Store Connect network API from consumers.
- Update our API client method to return the new <code>App<code> type instead of an array of strings and change the name to reflect the method's intentions.
- In the initializer of the <code>App<code> type, have it accept the response payload from the App Store Connect API call. The kind of payloads that we interact with in the OpenAPI client are called DTOs — Data Transfer Objects. Think of them as intermediaries between our apps and the network calls which can be used in a type-safe way.
- Update the JSON parsing part of our API client to return the <code>App<code> type, passing along the schema.
Next we'll take an app that we fetched and grab its associated releases. This will call the v1/apps/{id}/appStoreVersions endpoint. Notice that in the middle of the path is the <code>{id}<code> token for the app's ID in the middle of the path. Pay particular attention to how the <code>app.id<code> property is used below to populate the token:
Like we did with the App type, we're creating a Release type here to serve as our own type that we control. It gets created with the network call's DTO representation of a release. One interesting thing to note in the <code>Components.Schemas.AppStoreVersion<code> definition is that we have to grab the <code>rawValue<code> of the <code>appStoreState<code> property. That's because the OpenAPI generator built out an enum for us! This can be really nice when working with your own API schemas as they provide an exhaustive list of acceptable values.
Next we make the API client method to fetch all the versions for an app. The OpenAPI generated method is called <code>apps_hyphen_appStoreVersions_hyphen_get_to_many_related<code> and it takes in a path argument. We can create that argument with <code>.init(path: app.id)<code> and have Swift infer the type we are creating. There's some debate about the merits of using bare .init in Swift code but this is one place where it seems more acceptable because the full type of the path here is <code>apps_hyphen_appStoreVersions_hyphen_get_to_many_related.Path<code>. Best to let the compiler figure this one out!
Just as before we'll make the network request and examine its response. If things are all good then we assemble the array of Release objects and return them. We can then make our test for this like so:
You can put a breakpoint on the <code>XCTAssert<code> line and examine the resulting releases from our method and see what the API returned.
What a journey we've been on! We looked at the components of the OpenAPI packages Apple has recently released, built out an API client to call the App Store Connect APIs, took a brief detour to get authentication up and running, and made two API calls that we could actually parse and get useful data from. There's a lot more that the App Store Connect API can do for us and this is only the beginning.