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:
- The filename must encode the key ID.
altoolfinds the key by name, not by path. It has to be calledAuthKey_<KEYID>.p8— for keyABCD1234EF, exactlyAuthKey_ABCD1234EF.p8. A renamed file will not be found no matter where you put it. - 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. - The issuer ID is not the key ID. The key ID is 10 characters like
ABCD1234EF. The issuer ID is a UUID like69a6de70-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. - 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-000000000000Why 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 exactAuthKey_<KEYID>.p8filename. - The key was pasted into a secret and is now malformed. A
.p8is 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.