App Store Connect API authentication: a JWT walkthrough
A step-by-step walkthrough of JWT authentication for the App Store Connect API, covering token structure, Python and Node.js examples, key management best practices, and how to debug common 401 and 403 errors.
The App Store Connect API — Apple's REST interface for managing apps, pricing, metadata, and reports — uses JSON Web Tokens (JWTs) for authentication rather than the more familiar API-key-in-a-header pattern. If you've only used APIs that accept a bearer token you generate once and store forever, Apple's approach requires a small mental shift: every token you send is self-signed, short-lived, and generated fresh for each session.
Understanding why Apple made this choice helps you use the API more confidently. JWTs are stateless: Apple doesn't need to look up your token in a database on every request. Instead, your token carries a cryptographic signature that Apple verifies using a public key Apple already has on file. The upside is low latency on Apple's side; the downside is that you carry more responsibility for key management.
Before you make a single API call, you need three things from App Store Connect: a private key file (.p8), the Key ID tied to that file, and your Issuer ID. Without all three, your JWT won't validate — Apple returns a 401 on every request.
The anatomy of an App Store Connect JWT
A JWT has three Base64URL-encoded parts joined by dots: a header, a payload (claims), and a signature. Apple's variant of the standard isn't exotic, but a handful of fields are non-negotiable.
Header
{
"alg": "ES256",
"kid": "YOUR_KEY_ID",
"typ": "JWT"
}
Apple requires the Elliptic Curve algorithm ES256 — RSA won't work here. The kid field is the 10-character Key ID from App Store Connect (Users and Access → Integrations → Keys).
Payload (claims)
{
"iss": "YOUR_ISSUER_ID",
"iat": 1714900000,
"exp": 1714900900,
"aud": "appstoreconnect-v1"
}
| Claim | Description | Valid values |
|---|---|---|
iss |
Issuer ID — your team's unique identifier | UUID format, copied from App Store Connect |
iat |
Issued-at timestamp | Unix epoch in seconds, UTC |
exp |
Expiry timestamp | Maximum 20 minutes after iat |
aud |
Audience — must be this exact string | "appstoreconnect-v1" |
scope |
Optional: restrict token to specific endpoints | Array of "METHOD /path" strings |
Apple enforces a 20-minute maximum lifetime on tokens. Tokens with an exp more than 20 minutes after iat are silently rejected — Apple does not return a helpful error explaining this. Most production code issues a new token for every session or every batch of requests rather than caching one token across many calls.
Signature
The signature is computed over base64url(header) + "." + base64url(payload) using the private key from your .p8 file. Your JWT library handles this mechanically once you provide the key material. The critical thing to understand is that Apple validates the signature against the public key it has on file for your Key ID — if the keys don't match, you get a 401 with no further detail.
Generating a token in practice
Apple's .p8 file is a PEM-encoded EC private key. Here's the minimal Python approach using PyJWT and the cryptography package — two libraries most iOS tooling already depends on.
import jwt
import time
from pathlib import Path
ISSUER_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
KEY_ID = "XXXXXXXXXX" # 10-char string from App Store Connect
KEY_FILE = Path("AuthKey_XXXXXXXXXX.p8")
private_key = KEY_FILE.read_text()
token = jwt.encode(
payload={
"iss": ISSUER_ID,
"iat": int(time.time()),
"exp": int(time.time()) + 1140, # 19 min — leaves margin for clock drift
"aud": "appstoreconnect-v1",
},
key=private_key,
algorithm="ES256",
headers={"kid": KEY_ID},
)
# token is a string — use as: Authorization: Bearer <token>
Using 19 minutes rather than the full 20 gives you a safety margin for clock drift between your server and Apple's. Apple's documentation notes that clock skew can cause borderline tokens to fail, and the error message you get back ("UNAUTHORIZED") doesn't tell you the token was nearly valid — you're just left debugging.
For Node.js projects, the jsonwebtoken package supports ES256 with the same pattern: pass the .p8 content as the secret and set algorithm: 'ES256'. For Swift server-side code, Vapor's JWTKit library has first-class ES256 support and handles the PEM parsing automatically.
Once you have a valid token, every ASC API call follows the same pattern:
curl -s \
-H "Authorization: Bearer $TOKEN" \
"https://api.appstoreconnect.apple.com/v1/apps?limit=5"
The response format is JSON:API — every resource has a type, id, and attributes object. Pagination is cursor-based via links.next. Most teams build a thin wrapper that handles token generation and pagination together, because you'll encounter both within the first few real requests.
Common authentication errors and what they actually mean
The App Store Connect API returns terse error JSON, which can be disorienting the first time you hit an auth problem. The same 401 status covers several distinct failure modes.
| Error | Likely cause | Fix |
|---|---|---|
| 401 UNAUTHORIZED | Token expired, wrong Key ID, or wrong Issuer ID | Verify kid matches key; re-copy Issuer ID from App Store Connect |
| 403 FORBIDDEN | Key exists but lacks the required access role | Add the correct role under Users and Access → Integrations |
| 401 with NOT_FOUND error code | Key has been revoked or never activated | Generate and upload a new key; update all secrets stores |
| 401 on tokens close to the 20-minute mark | Server clock is drifted, making tokens appear expired | Use NTP sync; keep lifetime at 19 minutes or less |
| 401 with "INVALID_TOKEN" | Wrong algorithm (RS256 instead of ES256) or malformed header | Check alg: "ES256" is set explicitly in JWT library call |
Roles matter more than many tutorials suggest. Apple's API keys have granular access roles: Admin, Finance, Developer, Customer Support, and Sales. If you're fetching Sales and Trends reports, the key needs at least the Finance role. If you're updating prices or subscription configuration, you need Admin. Creating a key with insufficient scope is one of the most common reasons teams hit 403s after getting authentication working.
This role constraint becomes especially relevant when you're building automation that touches pricing — for example, the kind of automated price-update workflow described in our ASC API automation post. A key that works for reading your app list will fail when you try to patch a price point, and the error won't tell you that role, not token validity, is the problem.
Apple limits each team to two active API keys at a time. This is a meaningful operational constraint: if your CI pipeline, your pricing automation, and your local dev environment all need API access, you'll need to share keys between environments — which means a single revocation can take down multiple systems simultaneously. Plan your key architecture before you need to revoke in a hurry.
Key management: the part most tutorials skip
Getting a JWT to work in a test script is the easy part. Managing the underlying keys safely over months and years is where most operational problems emerge.
Never store the .p8 file in source control. This sounds obvious, but .p8 files have appeared in public GitHub repositories — and unlike a password, you can't simply change an Apple API key without generating a new one and distributing it to every system that uses it. The .p8 file should live in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler, or 1Password Secrets Automation are all reasonable choices for teams of different sizes). In CI environments, store it as a base64-encoded environment variable and decode at runtime.
Rotate keys proactively, not reactively. Apple keys don't expire on their own, which means a leaked key stays valid indefinitely until you revoke it. Building a quarterly rotation into your calendar eliminates a category of risk that's otherwise invisible until it materializes.
Treat revocation as a routine operation. Apple's revocation UI is in App Store Connect under Users and Access → Integrations → Keys. Revoking a key takes effect within minutes. Before you revoke, have a plan: which services use this key, where is the secret stored for each, and who will update each one. Running through this checklist proactively (during a rotation) is far less stressful than doing it reactively during an incident.
The Issuer ID is not a secret, but treat it like configuration. The Issuer ID is a UUID that identifies your team — it's not sensitive in the same way as the private key, but keeping it in configuration rather than hardcoded in source makes environment management and rotation easier.
For teams building pricing tools that need to work across multiple App Store territories, it's worth considering whether a single Admin key is the right choice or whether separate Finance-scoped keys for read-only operations reduce the blast radius of any single compromise.
Scoped tokens: the forward-looking security pattern
Apple's documentation introduced a scope claim for App Store Connect JWTs that restricts a token to specific endpoint paths. As of the time of writing this is optional, but it represents a meaningful security improvement for automation scripts with well-defined purposes.
A scoped token includes an extra claim:
{
"iss": "YOUR_ISSUER_ID",
"iat": 1714900000,
"exp": 1714900900,
"aud": "appstoreconnect-v1",
"scope": [
"GET /v1/apps",
"GET /v1/inAppPurchases"
]
}
Apple rejects scoped tokens used against endpoints not in the scope list. For a script that only ever reads Sales and Trends data, adding a scope claim means that even if the token is intercepted, it can't be used to modify pricing or subscription configuration.
This integrates naturally with a defence-in-depth approach to pricing automation: an Admin key generates scoped read tokens for monitoring, and write operations use separately managed tokens that are only generated when an update is actually being pushed. The extra code is modest; the risk reduction is meaningful.
If you're building toward a more complete price-update automation workflow, token scoping is worth incorporating from the start rather than retrofitting. The patterns that make pricing automation safe — idempotency, audit logging, least-privilege access — all compose better when designed together.
Sources and further reading
- Apple Developer Documentation: Generating Tokens for API Requests
- App Store Connect API Reference — Apple Developer
- JWT.io: Introduction to JSON Web Tokens
- Apple Developer Documentation: App Store Connect API Roles
- RFC 7519 — JSON Web Token (JWT) — IETF
- WWDC 2018 Session 303: Automating App Store Connect — Apple Developer
Share this post
Ready to put this into practice?
appsops.store gives you PPP-adjusted pricing across all 175+ App Store territories, App Store Connect API automation, and 39-language localization — all from one dashboard.
Start free →