All posts
AUTOMATION

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.

By the AppsOps team · · 7 min read

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.

20 minMaximum JWT lifetime the App Store Connect API allows per token

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

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 →

Related reading