Email me

Guide · 7 min read

Filed under iOS and Swift App development Guides

How to create an App Store Connect API key and use it in CI

Steps
7
Hands-on
20 min
Reading
7 min
Checked
Oct 23

The short version

In App Store Connect go to Users and Access, Integrations, App Store Connect API, Team Keys, click Generate API Key, pick a role, and download the .p8 file. You get exactly one download. Copy the key ID from the key list and the issuer ID from the top of the same page. Every API call then carries a JWT you sign with that .p8: algorithm ES256, audience appstoreconnect-v1, lifetime under 20 minutes.

Title card reading App Store Connect API key, start to CI. From a Windows PC, no Mac, no 2FA.
One team key, three secrets, no Apple ID password anywhere in CI. Image: Tensorust, made for this page.

What you need

  • A paid Apple Developer Program membership
  • The Admin role on your App Store Connect team
  • A CI system with encrypted secrets (GitHub Actions, GitLab CI or similar)
  • Python 3 or fastlane to sign the token

Part of the article Build an iOS app on Windows: how 21 apps shipped with no Mac. This page answers one question from it in depth.

On this page · 8 sections
  1. Why a key instead of your Apple ID
  2. Generate the key and download it once
  3. Sign a token and test it
  4. Store the key as CI secrets
  5. Use it in fastlane and GitHub Actions
  6. What goes wrong
  7. Related
  8. Sources

A key like this runs our whole iOS pipeline. Between September 10 and October 8, 2026, every Apple call in 139 App Store builds across 21 app repos went through a team API key loaded by fastlane, all started from a Windows PC with no Mac on the desk (the full story is in Build an iOS app on Windows). Below is how to make yours, the exact token code we use, and the four ways it failed for us.

Why a key instead of your Apple ID

A CI runner cannot answer a two-factor prompt. Tools that log in with an Apple ID and password break the first time Apple asks for a code. An API key never asks: you sign a short token with your private key and Apple checks it against the public half it kept. That is why fastlane and scripts like ours use it.

There are two kinds (Apple):

Team key Individual key
Who can create it An Admin on the team Any user with the Generate Individual API Keys permission
Access All apps on the team, limited by the role you pick That user's own apps and roles
Provisioning endpoints (certificates, profiles) Yes, if the role allows No
Sales and Finance, notaryTool Yes, if the role allows No
JWT claim that identifies it iss = issuer ID sub = "user"

For a build pipeline you want a team key: we create the distribution certificate through the API from Windows, and individual keys cannot reach those endpoints at all.

Step 01 to 03: Generate the key and download it once

  1. Sign in to App Store Connect as an Admin. Open Users and Access, then the Integrations tab, then App Store Connect API in the left column, and make sure Team Keys is selected.
  2. Click Generate API Key (or the + button). The name is only a label for you. Under Access, pick the role. Team keys can touch every app on the team whatever the role, so the role is your only fence: pick the smallest one that covers what this key will do.
  3. Click Download API Key next to the new key. You get AuthKey_<KEYID>.p8. Apple says the link "only appears if you haven't downloaded the private key" and that "Apple doesn't keep a copy." Save it somewhere that is not a git repo.
Diagram of four boxes: Integrations tab, then name and role, then download once giving AuthKey file, then what you hold: key ID, issuer ID and the p8 file.
Three things leave App Store Connect: the key ID, the issuer ID and the .p8. Only the .p8 is a secret. Credit: Tensorust, navigation from Apple's docs.

We are deliberately not showing a screenshot of our own Integrations page: it would publish our real key ID and issuer ID. They are not the secret, but there is no reason to hand them out.

Step 04: Sign a token and test it

Apple's token rules are short. Header: alg ES256, kid your key ID, typ JWT. Payload: iss your issuer ID, iat now, exp the expiry, aud appstoreconnect-v1, and an optional scope list. "For most requests, App Store Connect rejects a token with a lifetime greater than 20 minutes," where lifetime is exp minus iat.

This is the function from our Store/asc.py, which runs on Windows with the PyJWT and cryptography packages:

Python code card: a token function that reads the p8 file, builds a payload with iss, iat, exp at now plus 19 minutes and aud appstoreconnect-v1, and encodes it with ES256 and a kid header, followed by a curl call to the v1 apps endpoint with a Bearer token.
The whole auth layer is about 15 lines. Mint a fresh token per run instead of caching one. Credit: Tensorust, from our Store/asc.py.

Test it with one read-only call:

curl -H "Authorization: Bearer $TOKEN" https://api.appstoreconnect.apple.com/v1/apps

A JSON list of your apps means the key, the IDs and the clock all agree. Paste a token into our JWT decoder to check the claims and the expiry before you blame Apple; it decodes in your browser and sends nothing anywhere.

Two notes from running this 139 times. First, our real script backdates iat by 60 seconds to absorb clock drift on runners; if you do that, shorten exp to match, because Apple measures the gap between the two. Second, Apple occasionally answers a valid token with a 401. Our call() helper retries a 401 up to twice with a freshly minted token, and those retries go through.

If you only need read access for a long-running dashboard, Apple accepts tokens of up to six months for some GET-only resources, but only when the token carries a scope that lists GET requests. Everything that writes stays under 20 minutes.

Step 05: Store the key as CI secrets

The .p8 is a text file, but a base64 string is easier to pass around as a single secret. From Git Bash on Windows, with the GitHub CLI:

base64 -w0 AuthKey_XXXXXXXXXX.p8 | gh secret set ASC_KEY_CONTENT
gh secret set ASC_KEY_ID
gh secret set ASC_ISSUER_ID

Piping straight into gh secret set means the base64 never lands in a file you could commit by accident. We do the same with the distribution certificate.

Code card with three gh secret set commands, the fastlane app_store_connect_api_key block reading three environment variables with is_key_content_base64 true, and a workflow step that base64-decodes the key into RUNNER_TEMP and prints only its size.
The key enters CI as three secrets and only becomes a file inside the runner's temp folder. Credit: Tensorust, from our Fastfile and appstore.yml.

Step 06: Use it in fastlane and GitHub Actions

fastlane's app_store_connect_api_key action takes the three values straight from the environment. Set is_key_content_base64: true so it decodes the secret itself. After that, every fastlane action that talks to Apple (build numbers, upload, metadata, submit for review) uses the key, with no Apple ID anywhere.

Some tools want a file path instead of a string. Our workflow decodes the key into $RUNNER_TEMP, which sits outside the checkout and is wiped when the job ends, sets chmod 600, and logs only the byte count, never the contents.

GitHub Actions job list for Chain 1.2: set up job, checkout, select Xcode 26, install xcodegen and fastlane, pick a simulator, generate the project, compile 52 seconds, write the signing key, import the certificate, build upload and submit 2 minutes 31 seconds. Total 4 minutes 9 seconds.
A boring release: every Apple call in this run used the team key. Credit: Tensorust, GitHub Actions screenshot from the parent article, Oct 8 2026.
Troubleshooting

What goes wrong

Four boxes: 401 token rejected, 403 on POST v1 apps because app records cannot be created by API, 403 role too small, and lost p8 with no re-download.
The four failures we actually hit, and what each one meant. Credit: Tensorust.
  • 401 Unauthorized. Lifetime over 20 minutes, a kid and iss from two different keys, or a clock that is off. Decode the token and check exp - iat first.
  • 403 on POST /v1/apps. Our attempt to create an app record by API came back 403. Make the app record once in the web UI; name, subtitle, description, keywords, screenshots and builds are all scriptable after that.
  • 403 on everything else. The token is fine and the role is not. Generate a key with a bigger role instead of reusing the small one for a new job.
  • The .p8 is gone. There is no second download. Revoke the key in Integrations, generate a new one, and update three secrets in every repo. With 21 repos that is 63 gh secret set calls, so script it.

If a key ever leaks (a log, a screenshot, a public repo), revoke it immediately; Apple's docs say to treat it like a password.

Our take: one team key for a solo studio is fine, but give it a name that says where it lives ("ci-github-2026") so revoking it later is not a guessing game. And keep the .p8 out of every folder a coding agent can read and commit.

Sources

All checked October 23, 2026.

Primary

Our own data

  • 139 App Store workflow runs across 21 app repos, September 10 to October 8, 2026: Build an iOS app on Windows, from the GitHub Actions API on October 16, 2026.
  • The 403 on POST /v1/apps, the token and retry code, the fastlane block and the RUNNER_TEMP step: our own pipeline scripts. The token card shows exp at now plus 19 minutes to keep the lifetime under 20; the text describes the real script's values.

Images

  • Title card, key diagram, token card, secrets card and failure diagram: made by Tensorust for this page (Archivo, JetBrains Mono and Silkscreen, SIL Open Font License). Only the placeholder XXXXXXXXXX appears; no real key ID, issuer ID, team ID or key content is shown anywhere.
  • GitHub Actions screenshot: our Chain 1.2 run on October 8, 2026, from the parent article. Step names and timings only.

Credit

Do this next

Every number on this page links to its source and was checked on Oct 23, 2026. Spot an error or a newer figure? Email matt@tensorust.com and it gets fixed. Related: JWT decoder, Have the pipeline built for you.

Read next

Back to The Manual →   All articles →

Get the next one by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ