node-app-store-connect-api

Node library to interface with the App Store Connect API

Usage no npm install needed!

<script type="module">
  import nodeAppStoreConnectApi from 'https://cdn.skypack.dev/node-app-store-connect-api';
</script>

README

App Store Connect API for Node

A library to support Apple's App Store Connect API.

See the WWDC video introducing the API.

Requires Node 16+. (This package uses ES Modules.)

API Keys Required

You'll have to start by creating an API key on the App Store Connect site.

When you're done, you'll have an issuer ID (like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), an API key (like XXXXXXXXXX) and a private key file.

Keep your private keys private. Store them securely, outside of your git repository. If you use Apple's Transporter tool, it recommends storing private keys in your home directory, in a file named like ~/.appstoreconnect/private_keys/AuthKey_XXXXXXXXXX.p8 (where that XXXXXXXXXX is your API key). If you store your private key in that standard location, you don't have to pass us a privateKey; we'll autodetect your key from there.

Usage

import { api } from `node-app-store-connect-api`;

const { fetchJson, create, update } = await api({issuerId, apiKey, privateKey});

// log all apps
const apps = await fetchJson('https://api.appstoreconnect.apple.com/v1/apps'));
console.log(apps);

You can use an absolute URL, or, at your convenience, you can omit the https://api.appstoreconnect.apple.com/v1/ part of the URL.

const apps = await fetchJson('apps');
console.log(apps);

You'll need your app's numeric "app ID" to use the API; the apps endpoint will provide it, or you can get it directly from the App Store Connect web site.

Creating, updating, and removing a new App Store version

We create objects with the create function. It constructs a create request (a POST). It uses the "type" as the URL, and it allows you to pass in a data object as a relationship.

const { fetchJson, create, update, remove } = await api({issuerId, apiKey, privateKey});

// let's use the first app ID, for example
const [app] = await fetchJson('apps');

const appStoreVersion = await create({
  type: 'appStoreVersions',
  attributes: { platform: 'IOS', versionString: '1.0.1' },
  relationships: { app }
);

(Officially, the relationships that we submit to Apple aren't supposed to be entire fetched objects; they're supposed to contain just a data object containing only the type and id of the related object. This API takes care of that detail for you, because writing out relationships the official way is much wordier, but you're allowed to write out relationships by hand, if you prefer.)

const appStoreVersion = await create({
  type: 'appStoreVersions',
  attributes: { platform: 'IOS', versionString: '1.0.1' },
  relationships: { app: { data: { type: "apps", id: app.id } } }
);

We can also update objects with the update function.

await update(appStoreVersion, { attributes: { versionString: '1.0.2' }});

Or you can delete objects with the remove function. (delete is a language keyword in JavaScript!)

await remove(appStoreVersion);

// or, delete by ID, if you don't have the entire object:

await remove({ type: 'appStoreVersions', id: appStoreVersionId });

Uploading an asset (screenshots, app previews)

Uploading an asset (a screenshot or a preview) is a multi-step process.

  • Assets are linked to sets (App Screenshot Sets, App Preview Sets)
  • Sets have a type, corresponding to the screen size of the device. These are defined as strings given by Apple (screenshot display types and preview types).
  • Different versions of an app can have different screenshots, and each app version can have multiple "localizations," allowing you to show different screenshots to users in different countries/languages.

So, to upload a screenshot, you must start by fetching or creating:

  • App
  • App Store Version
  • App Store Version Localization
  • App Screenshot Set (with the selected screenshotDisplayType)
  • App Screenshot

But the "App Screenshot" object is just a "reservation" object, allowing you to do the upload. Apple's documentation explains how to upload assets using the App Screenshot reservation, in a series of "upload operations." This API will take care of that for you.

import { stat, readFile } from 'fs/promises';
import { api } from `node-app-store-connect-api`;

const { fetchJson, create, uploadAsset, pollForUploadSuccess } = 
  await api({issuerId, apiKey, privateKey});

// in real life, you might have to create your
// own app, version, localization, and screenshot set
const [app] = await fetchJson('apps');
const [version] = await fetchJson(
  app.relationships.appStoreVersions.links.related);
const [l10n] = await fetchJson(
  version.relationships.appStoreVersionLocalizations.links.related);
const [appScreenshotSet] = await (fetchJson(
  l10n.relationships.appScreenshots.links.related);

const filePath = '/path/to/myScreenshot.png';
const fileSize = await stat(filePath)).size;

// create the screenshot reservation
const appScreenshot = await create({
  type: 'appScreenshots',
  attributes: {
    fileName: 'myScreenshot.png',
    fileSize: fileSize,
  },
  relationships: { appScreenshotSet }
}});

// upload the asset
await uploadAsset(appScreenshot, await readFile(filePath));
// poll the API for upload success/failure
await pollForUploadSuccess(appScreenshot.links.self);

That's a lot of work. Check out the working samples in the samples directory of this repository.

Pagination

The App Store Connect API can return data in multiple pages. You're meant to request each page one at a time, using the data from the links section.

This API automatically crawls all pages in a response by default. If you'd like that not to happen, you can pass an object containing options to fetchJson like this:

const apps = await fetchJson('apps', { crawlAllPages: false });

But, instead, consider using a limit parameter to limit the number of results:

const apps = await fetchJson('apps?limit=1');

Data and Inclusions

By default, the App Store Connect API endpoint returns all data in a data key, like this:

{
  data: [ /* actual data here */ ]
}

As a convenience, by default, this library automagically returns the data array directly, not wrapped in a {data} object. Otherwise, you'd have to do this, which can be annoying:

const apps = (await fetchJson('apps')).data;
// or this:
const {data: apps} = await fetchJson('apps');

But there is information outside the data key that you might want/need. If you choose to use an include query parameter, like apps?include=appStoreVersions, then the App Store Connect API will return that data as a separate included key, outside the data response:

{
  data: [
    {
      type: "apps",
      id: 123,
      attributes: { /* ... */ },
      relationships: {
        appStoreVersions: { data: [
          { type: "appStoreVersions", id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
        ]}
      }
    }
  ],
  included: [
    {
      type: "appStoreVersions",
      id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      attributes: { /* ... */ },
      relationships: { /* ... */ }
    }
  ]
}

This library will skip/ignore the included data by default.

You can pass an inclusions option to fetchJson to make it include the data key and the included key.

const {data: apps, included: appStoreVersions} =
  await fetchJson('apps?include=appStoreVersions', {inclusions: true});

In our experience, having inclusions be an array is not ideal, especially if you include multiple kinds of objects, like this: fetchJson('apps?include=builds,appStoreVersions')

So, we've also provided an {inclusions: 'tree'} option. If you use that, we'll provide the inclusions as a nested JSON object, where the top-level keys are type names (like builds and appStoreVersions), mapping to another JSON object, mapping IDs to objects.

The result of fetchJson('apps?include=builds,appStoreVersions', {inclusions: 'tree'}) would look like this:

{
  data: [ /* ... */ ],
  included: {
    builds: {
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": {
        type: "builds",
        id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        attributes: { /* ... */ },
        relationships: { /* ... */ }
      }
    },
    appStoreVersions: {
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": {
        type: "appStoreVersions",
        id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        attributes: { /* ... */ },
        relationships: { /* ... */ }
      }
    }
  }
}

Handling inclusions as a tree makes it easier to map from relationships to the included objects.

const {data: apps, included} =
  await fetchJson('apps?include=builds,appStoreVersions', {inclusions: 'tree'});
for (const app of apps) {
  const versionStrings = app.relationships.appStoreVersions.data
    .map(rel => included.appStoreVersions[rel.id])
    .map(appStoreVersion => appStoreVersion.attributes.versionString);
  const buildVersions = app.relationships.builds.data
    .map(rel => included.builds[rel.id])
    .map(build => build.attributes.version);
  console.log({name: app.attributes.name, versionStrings, buildVersions});
}

Raw Requests and Responses

App Store Connect API responses also include a links section and a meta section used mostly for pagination, if you're interested in those. (You typically don't need them, because our API will paginate for you.)

If you want access to the data exactly as App Store Connect provided it, circumventing all of our "helpful" conveniences, the API provides a raw fetch function. The fetch function follows the rules of the standard fetch API, but we automatically add the Authorization header, and prepend https://api.appstoreconnect.apple.com/v1 on relative URLs.

import { api } from `node-app-store-connect-api`;

const { fetch } = await api({issuerId, apiKey, privateKey});

// read the raw JSON from a fetch request
const { data: apps, included: appStoreVersions, links, meta } =
  await fetch('apps?include=appStoreVersions').then(r=>r.json());

// create an appStoreVersion the long way
const appId = apps[0].id;
const { appStoreVersion } = await fetch('/appStoreVersions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ data: {
    type: 'appStoreVersions',
    attributes: { platform: 'IOS', versionString: '1.0.1' },
    relationships: { app: { data: { type: "apps", id: appId } } }
  }})
}).then(r=>r.json());