TL;DR: Apple's App Store Connect API exposes most of what you need for indie launches. Here are the 10 scripts I built across a 60-day experiment that paid back hours of bureaucracy. From my $499 ASC API Toolkit on Gumroad.
Why this matters
The Apple ASC web UI is fine for 1 app. For 4+ apps it becomes a clicking exercise. Apple's App Store Connect API handles 90% of routine bureaucracy via JSON. The remaining 10% (e.g., IAP price tier setup) needs CDP automation — which I covered in previous article.
This article: the 10 scripts I actually use weekly.
The setup
Authenticate via JWT generated from your private .p8 key:
from authlib.jose import jwt
import time
def gen_token(key_id: str, issuer_id: str, key_file: str) -> str:
with open(key_file, 'rb') as f:
private_key = f.read()
headers = {"alg": "ES256", "kid": key_id, "typ": "JWT"}
payload = {
"iss": issuer_id,
"exp": int(time.time()) + 1200, # 20-min token
"aud": "appstoreconnect-v1"
}
return jwt.encode(headers, payload, private_key).decode()
All 10 scripts below use this token in the Authorization: Bearer <token> header.
Script 1: List all your apps
import requests
def list_apps(token: str):
r = requests.get("https://api.appstoreconnect.apple.com/v1/apps",
headers={"Authorization": f"Bearer {token}"})
for app in r.json()["data"]:
print(f"{app['attributes']['bundleId']}: {app['attributes']['name']}")
Output:
com.jiejuefuyou.autochoice: AutoChoice
com.jiejuefuyou.altitudenow: AltitudeNow
com.jiejuefuyou.daysuntil: DaysUntil
com.jiejuefuyou.promptvault: PromptVault
Script 2: List builds per app + their states
def list_builds(token, app_id):
r = requests.get(f"https://api.appstoreconnect.apple.com/v1/builds",
headers={"Authorization": f"Bearer {token}"},
params={"filter[app]": app_id, "limit": 5})
for b in r.json()["data"]:
attrs = b["attributes"]
print(f"{attrs['version']} ({attrs['buildNumber']}): {attrs['processingState']}")
Script 3: Add tester to TestFlight beta group
The undocumented gotcha: new builds don't auto-add to TF groups. You have to associate manually.
def add_build_to_group(token, build_id, group_id):
payload = {"data": [{"type": "builds", "id": build_id}]}
r = requests.post(
f"https://api.appstoreconnect.apple.com/v1/betaGroups/{group_id}/relationships/builds",
headers={"Authorization": f"Bearer {token}", "content-type": "application/json"},
json=payload
)
return r.status_code == 204
This single call saves 4-6 hours of TF debugging per app launch. Worth it.
Script 4: Resend tester invitation
When a tester says "I never got the email":
def resend_invite(token, app_id, tester_id):
payload = {
"data": {
"type": "betaTesterInvitations",
"relationships": {
"betaTester": {"data": {"type": "betaTesters", "id": tester_id}},
"app": {"data": {"type": "apps", "id": app_id}}
}
}
}
r = requests.post("https://api.appstoreconnect.apple.com/v1/betaTesterInvitations",
headers={"Authorization": f"Bearer {token}", "content-type": "application/json"},
json=payload)
return r.status_code == 201
Script 5: Check tester ACCEPTED state
A tester record can exist (INVITED) without being ACCEPTED. INVITED can't install.
def tester_state(token, app_id, tester_email):
r = requests.get("https://api.appstoreconnect.apple.com/v1/betaTesters",
headers={"Authorization": f"Bearer {token}"},
params={"filter[email]": tester_email, "filter[apps]": app_id})
if not r.json()["data"]:
return "NOT FOUND"
state = r.json()["data"][0]["attributes"].get("state", "UNKNOWN")
return state # INVITED / ACCEPTED / etc
Script 6: Check app availability per region
def app_availability(token, app_id):
r = requests.get(f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appAvailability",
headers={"Authorization": f"Bearer {token}"})
return r.json()["data"]["attributes"]
Useful for debugging "App not available in your region" issues.
Script 7: Create IAP
The base IAP creation works via API (price tier is the part that doesn't):
def create_iap(token, app_id, product_id, name, iap_type="NON_CONSUMABLE"):
payload = {
"data": {
"type": "inAppPurchasesV2",
"attributes": {
"name": name,
"productId": product_id,
"inAppPurchaseType": iap_type
},
"relationships": {
"app": {"data": {"type": "apps", "id": app_id}}
}
}
}
r = requests.post("https://api.appstoreconnect.apple.com/v2/inAppPurchases",
headers={"Authorization": f"Bearer {token}", "content-type": "application/json"},
json=payload)
return r.json()
⚠️ V2 API but legacy V1 path (/v1/inAppPurchases is also a thing for Subscription IAPs). Pay attention to inAppPurchasesV2 vs inAppPurchases based on what you need.
Script 8: Verify Apple agreement state
def agreement_state(token):
r = requests.get("https://api.appstoreconnect.apple.com/v1/agreementsForReadyForSale",
headers={"Authorization": f"Bearer {token}"})
# API path varies by year — check current docs
return r.json()
If your paid_apps agreement is unsigned, ALL distribution (including TestFlight) is gated. Check this Day 1.
Script 9: List submissions awaiting review
def pending_submissions(token, app_id):
r = requests.get(f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appStoreVersions",
headers={"Authorization": f"Bearer {token}"},
params={"filter[appStoreState]": "WAITING_FOR_REVIEW,IN_REVIEW"})
return r.json()["data"]
Lets you build a "what's in the queue" dashboard without manual ASC web UI clicks.
Script 10: Get build details + crash reports
def build_details(token, build_id):
r = requests.get(f"https://api.appstoreconnect.apple.com/v1/builds/{build_id}",
headers={"Authorization": f"Bearer {token}"})
return r.json()["data"]["attributes"]
For diagnosing "build rejected" or "processing stuck" states.
What's NOT in this list (and why)
- IAP price tier setup — Apple's API doesn't expose tier selection. CDP + Playwright is the workaround. Full writeup.
- Localization batch — does work via API but gets verbose; my actual implementation handles N apps × M locales × 5 fields each. Worth its own article.
- Crash analytics — Apple's API exposes some but not all; symbolicated crashes need additional tooling.
- Submit for Review — works via API but has 50+ required fields; treating it as a single function loses nuance.
These are in the full ASC API Toolkit ($499) which has 60+ Python scripts including all the edge cases.
The economics
If you have 1 iOS app: API isn't worth it. Web UI is fine.
If you have 4+ apps or are launching multiple per quarter: API saves 30-40 hours per launch cycle. At indie rates, that's $1500-2000 saved per launch. Worth automating.
Source
The 10 scripts above are MIT-licensed: github.com/jiejuefuyou/autoapp-toolkit orchestrator/asc-tools/.
For the production toolkit (60+ scripts including price tier CDP automation, batch localization, etc.): ASC API Toolkit on Gumroad ($499).
Want the 60-day playbook that produced this toolkit (real numbers, real launches): iOS Indie Launch Playbook ($19).
Top comments (0)