iOS · build & submit

altool: Unable to authenticate

altool: Unable to authenticate
Error: HTTP status code: 401. Unable to authenticate.

The short answer

Four things have to be true, and this error appears if any one of them is not. In rough order of how often each is the culprit:

  1. The filename must encode the key ID. altool finds the key by name, not by path. It has to be called AuthKey_<KEYID>.p8 — for key ABCD1234EF, exactly AuthKey_ABCD1234EF.p8. A renamed file will not be found no matter where you put it.
  2. It must be in a directory altool searches. The reliable one is ~/.appstoreconnect/private_keys/. Create it if it does not exist and copy the key in.
  3. The issuer ID is not the key ID. The key ID is 10 characters like ABCD1234EF. The issuer ID is a UUID like 69a6de70-0000-0000-0000-000000000000, it is shown once above the whole key list, and it is the same for every key on the team. Swapping them is extremely common because both are copied from the same page.
  4. The key must still exist. Revoked keys fail exactly like this. Check it is still listed in App Store Connect → Users and Access → Integrations.
mkdir -p ~/.appstoreconnect/private_keys
cp AuthKey_ABCD1234EF.p8 ~/.appstoreconnect/private_keys/

xcrun altool --validate-app -f build.ipa -t ios \
  --apiKey ABCD1234EF \
  --apiIssuer 69a6de70-0000-0000-0000-000000000000

Why this happens

The unhelpful part of this error is that it reports an authentication failure for what are usually lookup failures. altool is not told where your key is. It is given a key ID and expected to find the matching file by convention, so a key that is present but misnamed is indistinguishable, from the outside, from a key that was rejected.

Underneath, the key is an elliptic-curve private key used to sign a short-lived ES256 JWT. That token carries the issuer ID as its iss claim and the key ID as the kid header. Apple looks up the public half by kid, within the team identified by iss, and verifies the signature. Swap the two values and the lookup is for a key that does not exist in a team that does not exist — hence a 401 rather than a more specific complaint.

Telling a bad key from an ungranted one

These are different problems with different fixes, and the distinction is visible in the status code if you call the API directly rather than through altool:

  • 401 — the key was rejected. Wrong issuer ID, wrong key ID, revoked key, or a corrupted .p8.
  • 403 — the key is valid and has no permission. It authenticated as a real identity that has not been given a role. Fix this in Users and Access by giving the key App Manager.
  • 404 on a specific app — the key works and the app record does not exist. You cannot upload to an app that has not been created in App Store Connect.

Most pages about this error treat all three as one problem, which is why following them often means re-doing a step that was already correct.

The .p8 downloads once

Worth stating because it changes what "just re-download it" means: App Store Connect lets you download a private key exactly once, at creation. There is no way to retrieve it later. If the file is genuinely lost, the only path is to revoke that key and generate a new one — which is safe to do, and does not affect builds already submitted.

Catching it before you build

All four conditions are checkable before an upload, and the last of them can be settled by actually asking Apple. leas doctor verifies the key parses, then authenticates and distinguishes a rejected key from an ungranted one rather than reporting both as “unable to authenticate”:

▸ iOS
  ✓ API key                     ABCD1234EF · issuer 69a6de70…
  ✗ App Store Connect API       key authenticated but has no access (403)
      → Give the key the App Manager role in Users and Access → Integrations.

Set up leas in 10 minutesFree, MIT licensed, and it never receives your signing keys.

If that didn’t fix it

  • It works locally and not in CI. The runner has no ~/.appstoreconnect/private_keys/. The key has to be written there as part of the job, with the exact AuthKey_<KEYID>.p8 filename.
  • The key was pasted into a secret and is now malformed. A .p8 is newline-sensitive. Store it base64-encoded and decode it in the job rather than pasting the PEM directly.
  • Two teams, one machine. If you belong to more than one Apple team, an issuer ID from the wrong team will authenticate as nobody. The issuer ID is per team, not per key.