Managing multiple iOS apps through the App Store Connect web UI is a trap. Every click is a wait. Every page reload is 3 seconds of your life you'll never get back.
I manage 4 iOS apps, 6 TestFlight builds per week, 3 IAP configurations, and daily price updates. I have not clicked through appstoreconnect.apple.com in 6 weeks.
Here's the 10-script workflow I actually use. All run via the App Store Connect API (ASC API).
Prerequisites
# Get your API key from App Store Connect > Users and Access > Keys
# You need Admin role for most operations
export ASC_KEY_ID="YOUR_KEY_ID"
export ASC_ISSUER_ID="YOUR_ISSUER_ID"
export ASC_KEY_FILE="./AuthKey_YOUR_KEY_ID.p8"
1. Submit app for review (without the web UI)
import requests
import time
TOKEN_URL = "https://api.appstoreconnect.apple.com/v1/appSubmissions"
HEADERS = {
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json"
}
def submit_for_review(bundle_id, version_id):
payload = {
"data": {
"relationships": {
"app": {"data": {"id": get_app_id(bundle_id)}},
"appVersion": {"data": {"id": version_id}}
},
"type": "appSubmissions"
}
}
r = requests.post(TOKEN_URL, json=payload, headers=HEADERS)
return r.json()["data"]["id"] # submission ID
submission_id = submit_for_review("com.yourapp.bundle", "123456789")
print(f"Submitted: {submission_id}")
Why this matters: submitting via web requires 4-7 clicks through version selection, IAP confirmation, and export compliance. The API call is one line in a script you can run from CI.
2. Check build processing status
def get_build_state(bundle_id, build_version):
url = f"https://api.appstoreconnect.apple.com/v1/builds"
params = {"filter[app]=": get_app_id(bundle_id), "fields[builds]": "buildVersion,processingState,uploadedDate"}
r = requests.get(url, headers=HEADERS, params=params)
builds = r.json()["data"]
for b in builds:
if b["attributes"]["buildVersion"] == build_version:
state = b["attributes"]["processingState"]
print(f"Build {build_version}: {state}")
return state
return None
# Poll until ready
while True:
state = get_build_state("com.yourapp.bundle", "1.0.0")
if state == "PROCESSING_SUCCEEDED":
print("Ready for submission")
break
elif state == "PROCESSING_FAILED":
print("Build failed - check Xcode Cloud")
break
time.sleep(30)
3. Create an in-app purchase
def create_iap(bundle_id, product_id, pricing_info):
url = "https://api.appstoreconnect.apple.com/v1/inAppPurchases"
payload = {
"data": {
"attributes": {
"name": "Premium Subscription",
"productId": product_id,
"type": "RECURRING"
},
"relationships": {
"app": {"data": {"id": get_app_id(bundle_id)}}
},
"type": "inAppPurchases"
}
}
r = requests.post(url, json=payload, headers=HEADERS)
return r.json()["data"]["id"]
4. Update app pricing (automatic regional adjustment)
def update_app_pricing(bundle_id, price_tier):
app_id = get_app_id(bundle_id)
url = f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appPriceSchemes"
payload = {
"data": {
"attributes": {"territory": "USA", "priceTier": price_tier},
"id": app_id,
"type": "appPriceSchemes"
}
}
r = requests.patch(url, json=payload, headers=HEADERS)
return r.status_code == 200
5. List all TestFlight builds with their beta status
def list_tf_builds(bundle_id):
app_id = get_app_id(bundle_id)
url = f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/builds"
params = {
"fields[builds]": "buildVersion,uploadedDate,betaTestState,expirationDate",
"limit": 10
}
r = requests.get(url, headers=HEADERS, params=params)
builds = r.json()["data"]
for b in builds:
attrs = b["attributes"]
print(f"v{attrs['buildVersion']} | "
f"Tested: {attrs['betaTestState']} | "
f"Expires: {attrs.get('expirationDate', 'N/A')[:10]}")
return builds
6. Get app review decision details
def get_review_decision(bundle_id):
app_id = get_app_id(bundle_id)
url = f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appStoreVersions"
r = requests.get(url, headers=HEADERS, params={"limit": 1, "sort": "-versionString"})
latest = r.json()["data"][0]
state = latest["attributes"]["appStoreState"]
resolution = latest["attributes"].get("reviewResolution", "PENDING")
print(f"State: {state} | Resolution: {resolution}")
return state, resolution
7. Enable/disable IAP availability by territory
def set_iap_availability(iap_id, territories):
url = f"https://api.appstoreconnect.apple.com/v1/inAppPurchases/{iap_id}/inAppPurchaseAvailabilities"
for territory in territories:
payload = {
"data": {
"attributes": {"availableTerritories": {"data": [{"id": territory}]}},
"relationships": {"inAppPurchaseV2": {"data": {"id": iap_id}}},
"type": "inAppPurchaseAvailabilities"
}
}
r = requests.post(url, json=payload, headers=HEADERS)
print(f"{territory}: {r.status_code}")
8. Batch-expiry check across all apps
from datetime import datetime, timedelta
def check_all_builds_expiring(days_ahead=7):
apps = ["com.your.app1", "com.your.app2", "com.your.app3"]
cutoff = datetime.now() + timedelta(days=days_ahead)
for bundle_id in apps:
builds = list_tf_builds(bundle_id)
for b in builds:
exp_str = b["attributes"].get("expirationDate")
if exp_str:
exp_date = datetime.fromisoformat(exp_str.replace("Z", "+00:00"))
if exp_date < cutoff:
days_left = (exp_date - datetime.now()).days
print(f"ALERT: {bundle_id} v{b['attributes']['buildVersion']} "
f"expires in {days_left} days!")
9. Get beta tester feedback summary
def get_beta_feedback(bundle_id):
app_id = get_app_id(bundle_id)
url = f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/betaFeedback"
r = requests.get(url, headers=HEADERS, params={"limit": 25})
feedbacks = r.json().get("data", [])
for fb in feedbacks:
attrs = fb["attributes"]
print(f"[{attrs.get('type', 'N/A')}] {attrs.get('comment', '')[:80]}")
return feedbacks
10. Export compliance status check
def check_export_compliance(bundle_id):
app_id = get_app_id(bundle_id)
url = f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appStoreVersions"
r = requests.get(url, headers=HEADERS, params={"limit": 1})
version = r.json()["data"][0]
ec = version["attributes"].get("exportComplianceDocument", {})
status = ec.get("status", "NOT_REQUIRED")
print(f"Export compliance: {status}")
return status
Putting it together: the weekly CI script
# .github/workflows/asc-weekly.yml
name: App Store Connect Weekly Audit
on:
schedule:
- cron: '0 1 * * 1' # Every Monday 01:00 UTC
jobs:
asc-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check expiring builds
run: python scripts/asc_batch_expiry_check.py
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
What this actually saves
Before: ~45 min/day clicking through App Store Connect web UI.
After: scripts run in < 3 min, all in background, Slack notification on failures.
The ASC API is free to access. All you need is an App Store Connect API key (Admin role for write operations, Developer role for reads). The docs are at developer.apple.com/documentation/appstoreconnectapi.
Get the full toolkit
I packaged all 10 scripts plus 4 additional automation scripts for IAP batch management and build rotation into an ASC API Toolkit on Gumroad ($499). Includes:
- Token management with auto-refresh
- Exponential backoff retry decorators
- Per-app config YAML
- 30-day money-back guarantee
jiejuefuyou.gumroad.com/l/asc-api-toolkit
If this helped you, here are the tools I actually use
ASC API Toolkit — $499 — 60+ Python scripts that hit the v1/v2 endpoints I wrote about
TF Debug Bible — $29 — the TestFlight cache bug workaround via reviewSubmissions API
AutoApp Dashboard — $39 — Flask UI on top of these scripts — manifest-first, no INDEX.md
Top comments (0)