GitHub - tramlinehq/applelink: Practical API Recipes for App Store Connect Workflows

10 min read Original article ↗

applelink-banner-shado

Practical recipes over the App Store Connect API via Fastlane

Read more about the why in this blog post.

Rationale

Applelink is a small, self-contained, rack-based service using Hanami::API, that wraps over Spaceship and exposes some nice common recipes as RESTful endpoints in an entirely stateless fashion.

These are based on the needs of the framework that Tramline implements over App Store. The API pulls its weight so Tramline has to do as little as possible. Currently, it exposes 17 API endpoints.

In Applelink, a complex recipe, such as release/prepare, will perform the following tasks all bunched up:

  • Ensure that there is an App Store version that we can use for the release, or create a new one
  • Update the release metadata for that release version
  • Enable phased releases, if necessary

Similarly a simple fetch endpoint like release/live will give you the current status of the latest release.

Applelink is a separate service that is not reliant on Tramline’s internal state. It can be used in a standalone way, for e.g. from a CI workflow, or a Slack bot that spits out app release information.

Development

Running

bundle install
just start
just lint # run lint

Auth token

All APIs (except ping) are secured by JWT auth. Please use standard authorization header:

Authorization: Bearer <AUTH_TOKEN>

The AUTH_TOKEN can be generated using HS256 algo and the secret for generating and verifying the token is shared between Tramline/any other client and applelink.

These can be configured using the following env variables:

AUTH_ISSUER=tramline.dev
AUTH_SECRET=password
AUTH_AUD=applelink

These values can be set to whatever you want, as long as they are same between the caller and Applelink.

Example code for generating the token can be taken from this file in the Tramline repo.

In addition to the auth token, you also need the App Store Connect JWT token which is documented here.

Internal API

For the development environment, you can generate the above tokens using the following helper API:

curl -i -X GET http://127.0.0.1:4000/internal/keys?key_id=KEY_ID&issuer_id=ISSUER_ID

{
  "store_token": "eyJraWQiOiJLRVlfSUQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJJU1NVRVJfSUQiLCJpYXQiOjE2ODIwNjA1MzYsImV4cCI6MTY4MjA2MTAzNiwiYXVkIjoiYXBwc3RvcmVjb25uZWN0LXYxIn0.-pFtamhBjsNKLr5Z2Ft2tW9H2NojBF1d8RqQBr7nNZF43KUNGMQIPQyp9BCSrFXJop1k7hk7jJstXRJ-WMH_8Q",
  "auth_token": "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2ODIwNjA1MzYsImV4cCI6MTY4MjA2MTUzNiwiYXVkIjoiYXBwbGVsaW5rIiwiaXNzIjoidHJhbWxpbmUuZGV2In0.HDJJw6o6YK-Jmzpl0Xu4SmlTcGtNeEFI0VIg6fqitdw"
}

This expects the correct env variables to be set for AUTH_TOKEN and the App Store Connect key.p8 file to be present in the Applelink directory along with the relevant KEY_ID and ISSUER_iD being passed to the API.

API

One can also use requests in restclient-mode to interactively play around with the entire API including fetching and refreshing tokens.

Headers

Name Description
Authorization Bearer token signed by tramline
Content-Type Most endpoints expect application/json
X-AppStoreConnect-Key-Id App Store Connect key id acquired from the portal
X-AppStoreConnect-Issuer-Id App Store Connect issuer id acquired from the portal
X-AppStoreConnect-Token App Store Connect expirable JWT signed using the key-id and issuer-id

Fetch metadata for an App

GET /apple/connect/v1/apps/:bundle-id

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app

Success response

{
"id": "1658845856",
"name": "Ueno",
"bundle_id": "com.tramline.ueno",
"sku": "com.tramline.ueno",
"primary_locale": "en-US"
}

Fetch live info for an app

GET /apple/connect/v1/apps/:bundle-id/current_status

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/:bundle-id/current_status

Success response

[
  {
    "name": "Big External Group",
    "builds": [
      {
        "id": "da720570-cb6e-4b25-b82f-790045a6038e",
        "build_number": "10001",
        "status": "BETA_APPROVED",
        "version_string": "1.46.0",
        "release_date": "2023-04-17T07:03:01-07:00"
      },
      {
        "id": "1c4d0eb3-5cec-47f2-a843-949b12a69784",
        "build_number": "9103",
        "status": "BETA_APPROVED",
        "version_string": "1.45.0",
        "release_date": "2023-04-13T00:09:38-07:00"
      }
    ]
  },
  {
    "name": "Small External Group",
    "builds": [
      {
        "id": "e1aa4795-0df2-4d76-b899-8ee95fb8589e",
        "build_number": "10002",
        "status": "BETA_APPROVED",
        "version_string": "1.47.0",
        "release_date": "2023-04-17T10:00:19-07:00"
      },
      {
        "id": "da720570-cb6e-4b25-b82f-790045a6038e",
        "build_number": "10001",
        "status": "BETA_APPROVED",
        "version_string": "1.46.0",
        "release_date": "2023-04-17T07:03:01-07:00"
      }
    ]
  },
  {
    "name": "production",
    "builds": [
      {
        "id": "bf11d7a3-fe1c-4c71-acae-a9dc8af57907",
        "version_string": "1.44.1",
        "status": "READY_FOR_SALE",
        "release_date": "2023-04-11T22:45:25-07:00",
        "build_number": "9086"
      }
    ]
  }
]

Fetch all beta groups for an app

GET /apple/connect/v1/apps/:bundle-id/groups

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/groups

Success response

[{
  	"name": "The Pledge",
  	"id": "fcacfdf7-db62-44af-a0cb-0676e17c251b",
  	"internal": true
  },
  {
  	"name": "The Prestige",
  	"id": "2cd6be09-d959-4ed3-a4e7-db8cabbe44d0",
  	"internal": true
  },
  {
  	"name": "The Trick",
  	"id": "dab66de0-7af2-48ae-97af-cc8dfdbde51d",
  	"internal": true
  },
  {
  	"name": "Big External Group",
  	"id": "3bc1ca3e-1d4f-4478-8f38-2dcae4dcbb69",
  	"internal": false
  },
  {
  	"name": "Small External Group",
  	"id": "dc64b810-1157-4228-825b-eb9e95cc8fba",
  	"internal": false
  }]

Fetch a single build for an app

GET /apple/connect/v1/apps/:bundle-id/builds/:build-number

Path parameters

name type data type description
bundle-id required string app's unique identifier
build-number required integer build number

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/builds/9018

Success response

{
"id": "bc90d402-ed0c-4d05-887f-d300abc104e9",
"build_number": "9018",
"beta_internal_state": "IN_BETA_TESTING",
"beta_external_state": "BETA_APPROVED",
"uploaded_date": "2023-02-22T22:27:48-08:00",
"expired": false,
"processing_state": "VALID",
"version_string": "1.5.0"
}

Update the test notes for a build in TestFlight

PATCH /apple/connect/v1/apps/:bundle-id/builds/:build-number

Path parameters

name type data type description
bundle-id required string app's unique identifier

JSON Parameters

name type data type description
notes required string "test the feature to add release notes to builds"

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
-d '{"notes": "test the feature to add release notes to builds"}' \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/builds/latest

Fetch the latest build for an app

GET /apple/connect/v1/apps/:bundle-id/builds/latest

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/builds/latest

Success response

{
"id": "bc90d402-ed0c-4d05-887f-d300abc104e9",
"build_number": "9018",
"beta_internal_state": "IN_BETA_TESTING",
"beta_external_state": "BETA_APPROVED",
"uploaded_date": "2023-02-22T22:27:48-08:00",
"expired": false,
"processing_state": "VALID",
"version_string": "1.5.0"
}

Assign a build to a beta group

PATCH /apple/connect/v1/apps/:bundle_id/groups/:group-id/add_build

Path parameters

name type data type description
bundle-id required string app's unique identifier
group-id required string beta group id (uuid)

JSON Parameters

name type data type description
build_number required integer build number

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
-d '{"build_number": 9018}' \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/groups/3bc1ca3e-1d4f-4478-8f38-2dcae4dcbb69/add_build

Prepare a release for the app

POST /apple/connect/v1/apps/:bundle_id/release/prepare

Path parameters

name type data type description
bundle-id required string app's unique identifier

JSON Parameters

name type data type description
build_number required integer build number
version required string version name
is_phased_release optional boolean flag to enable or disable phased release, defaults to false
release_type optional string release type, either "MANUAL" or "AFTER_APPROVAL", defaults to "MANUAL"
is_force optional boolean force prepare even if a release is already in progress, defaults to false
metadata required hash { "promotional_text": "this is the app store version promo text", "whats_new": "release notes"}

Example cURL

curl -X POST \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
-d '{"build_number": 9018, "version": "1.6.2", "is_phased_release": true, "metadata": {"promotional_text": "app store version promo text", "whats_new": "release notes"} }' \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/prepare

Submit a release for review

PATCH /apple/connect/v1/apps/:bundle_id/release/submit

Path parameters

name type data type description
bundle-id required string app's unique identifier

JSON Parameters

name type data type description
build_number required integer build number
version optional string version name to update before submission

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
-d '{"build_number": 9018, "version": "1.6.2"}' \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/submit

Cancel a review submission

PATCH /apple/connect/v1/apps/:bundle_id/release/cancel_submission

Path parameters

name type data type description
bundle-id required string app's unique identifier

JSON Parameters

name type data type description
build_number required integer build number
version required string version name

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
-d '{"build_number": 9018, "version": "1.6.2"}' \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/cancel_submission

Fetch the status of current inflight release

GET /apple/connect/v1/apps/:bundle-id/release?build_number=:build-number

Path parameters

name type data type description
bundle-id required string app's unique identifier

Query parameters

name type data type description
build-number required integer build number

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release?build_number=500

Success response

{
  "id": "bd31faa6-6a9a-4958-82de-d271ddc639a8",
  "version_name": "1.8.0",
  "app_store_state": "PENDING_DEVELOPER_RELEASE",
  "release_type": "MANUAL",
  "earliest_release_date": null,
  "downloadable": true,
  "created_date": "2023-02-25T03:02:46-08:00",
  "build_number": "33417",
  "build_id": "31aafef2-d5fb-45d4-9b02-f0ab5911c1b2",
  "phased_release": {
    "id": "bd31faa6-6a9a-4958-82de-d271ddc639a8",
    "phased_release_state": "INACTIVE",
    "start_date": "2023-02-28T06:38:39Z",
    "total_pause_duration": 0,
    "current_day_number": 0
  },
  "details": {
    "id": "ef59d099-6154-4ccb-826b-3ffe6005ed59",
    "description": "The true Yamanote line aural aesthetic.",
    "locale": "en-US",
    "keywords": "japanese, aural, subway",
    "marketing_url": null,
    "promotional_text": null,
    "support_url": "http://tramline.app",
    "whats_new": "We now have the total distance covered by each station across the line!"
  }
}

Start a release after review is approved

PATCH /apple/connect/v1/apps/:bundle_id/release/start

Path parameters

name type data type description
bundle-id required string app's unique identifier

JSON Parameters

name type data type description
build-number required integer build number

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
-d '{"build_number": 9018}' \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/start

Fetch the status of current live release

GET /apple/connect/v1/apps/:bundle-id/release/live

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X GET \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/live

Success response

{
  "id": "bd31faa6-6a9a-4958-82de-d271ddc639a8",
  "version_name": "1.8.0",
  "app_store_state": "READY_FOR_SALE",
  "release_type": "MANUAL",
  "earliest_release_date": null,
  "downloadable": true,
  "created_date": "2023-02-25T03:02:46-08:00",
  "build_number": "33417",
  "build_id": "31aafef2-d5fb-45d4-9b02-f0ab5911c1b2",
  "phased_release": {
    "id": "bd31faa6-6a9a-4958-82de-d271ddc639a8",
    "phased_release_state": "COMPLETE",
    "start_date": "2023-02-28T06:38:39Z",
    "total_pause_duration": 0,
    "current_day_number": 4
  },
  "details": {
    "id": "ef59d099-6154-4ccb-826b-3ffe6005ed59",
    "description": "The true Yamanote line aural aesthetic.",
    "locale": "en-US",
    "keywords": "japanese, aural, subway",
    "marketing_url": null,
    "promotional_text": null,
    "support_url": "http://tramline.app",
    "whats_new": "We now have the total distance covered by each station across the line!"
  }
}

Pause the rollout of current live release

PATCH /apple/connect/v1/apps/:bundle_id/release/live/rollout/pause

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/live/rollout/pause

Resume the rollout of current live release

PATCH /apple/connect/v1/apps/:bundle_id/release/live/rollout/resume

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/live/rollout/resume

Fully release the current live release to all users

PATCH /apple/connect/v1/apps/:bundle_id/release/live/rollout/complete

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/live/rollout/complete

Halt the current live release from distribution

PATCH /apple/connect/v1/apps/:bundle_id/release/live/rollout/halt

Path parameters

name type data type description
bundle-id required string app's unique identifier

Example cURL

curl -X PATCH \
-H "Authorization: Bearer token" \
-H "X-AppStoreConnect-Key-Id: key-id" \
-H "X-AppStoreConnect-Issuer-Id: iss-id" \
-H "X-AppStoreConnect-Token: token" \
-H "Content-Type: application/json" \
http://localhost:4000/apple/connect/v1/apps/com.tramline.app/release/live/rollout/halt