TL;DR: 4 indie iOS apps, 600+ ASC API calls in 60 days. Here's the JWT auth code, the V1/V2 path traps that cost me 6 hours, and the resource model gotchas.
JWT generation (60 sec)
import jwt, time, uuid
def asc_token(key_id: str, issuer_id: str, p8_path: str) -> str:
with open(p8_path) as f:
priv = f.read()
return jwt.encode(
payload={
"iss": issuer_id,
"iat": int(time.time()),
"exp": int(time.time()) + 1200, # 20 min max
"aud": "appstoreconnect-v1",
},
key=priv,
algorithm="ES256",
headers={"kid": key_id, "typ": "JWT"},
)
3 things you need from ASC:
- Key ID (10-char string)
- Issuer ID (UUID)
- .p8 private key file
Get all three at: appstoreconnect.apple.com → Users and Access → Keys → API Keys.
V1 vs V2 confusion (cost me 4 hours)
Apple's "v2" resources still use /v1/... paths. Example:
# beta tester INVITATIONS — V2 resource
POST /v1/betaTesterInvitations ← yes, /v1/
{"data": {"type": "betaTesterInvitations", "attributes": {...}}}
# beta GROUP relationships — V1
POST /v1/betaGroups/{id}/relationships/betaTesters
There is no /v2/ prefix in URLs even for V2 resource types. The "version" lives in the resource type name only.
Resource 1:1 lock (cost me 2 hours)
appAvailabilities resource is read-only for existing apps. You can POST to create, but you cannot PATCH to update.
To change app availability (e.g., add territories):
- POST
/v1/appAvailabilitieswith new full state (Apple deletes the old) - Or use CDP web UI (no programmatic UPDATE)
I lost 2 hours trying every PATCH variation before finding this in their changelog.
Inline ID format (cost me 30 min)
When creating a resource that references another:
# WRONG — Apple rejects this
{"data": {"type": "betaTesters", "relationships": {
"betaGroups": {"data": [{"type": "betaGroups", "id": "abc123"}]}
}}}
# RIGHT — must use ${...} string format for inline ID
{"data": {"type": "betaTesters", "relationships": {
"betaGroups": {"data": [{"type": "betaGroups", "id": "${beta_group_id}"}]}
}}}
Documented in the Apple changelog as "string template format". Easy to miss.
Tester records are per-app
If you add a tester to App A and App B, you get two tester records with the same email. They have different IDs.
# Get all tester records for an email
tester_records = asc_api.get(f"/v1/betaTesters?filter[email]={email}")
# Returns multiple records, one per app the tester is on
So when "removing" a tester, you must remove from each app's record separately.
Tester state requires group endpoint
The state field on a tester record is always null unless you query through the betaGroup endpoint:
# state always null
GET /v1/betaTesters?filter[email]=foo@bar.com → state: null
# state actually populated
GET /v1/betaGroups/{group_id}/betaTesters → state: "ACCEPTED"
Builds don't accept sort
Apple's /v1/builds endpoint silently ignores the sort parameter. Documented elsewhere as "sort not supported", but the API returns 200 instead of 400.
Workaround: get all builds, sort client-side by attributes.uploadedDate.
My JWT helper class
class ASCAuth:
def __init__(self, key_id, issuer_id, p8_path):
self.key_id = key_id
self.issuer_id = issuer_id
self.p8_path = p8_path
self._token = None
self._expires = 0
def token(self):
now = int(time.time())
if not self._token or self._expires - now < 120:
self._token = asc_token(self.key_id, self.issuer_id, self.p8_path)
self._expires = now + 1200
return self._token
def headers(self):
return {"Authorization": f"Bearer {self.token()}", "Content-Type": "application/json"}
20-min token TTL with 2-min refresh buffer. Reuses the token across requests.
What this lets you automate
- Build state polling (cron job)
- TestFlight tester invitations (batch)
- App availability changes (per territory)
- IAP creation (NOT pricing — pricing requires CDP web UI as of 2026)
- Beta group management
- Localizations CRUD
- Submission for App Review
What it cannot do:
- IAP price tier selection (CDP only)
- Public app version visibility toggles (some require CDP)
- Apple Pay merchant config (manual)
- Paid Apps agreement signing (manual)
Source
Full ASC API helper class with JWT + retry + 18 wrapped endpoints:
AutoApp Dashboard ($39) includes:
-
asc_api.py(JWT auth + 18 endpoints) -
asc_diag.py(diagnostic CLI for build state, tester state, agreement state) -
asc_iap_pricing_v3.py(7-step CDP IAP pricing flow) -
asc_status_poll.py(hourly cron)
If you're building anything indie iOS in 2026, the ASC API is your friend. The CDP fallback is your acquaintance. Plan for both.
Top comments (0)