DEV Community

孫昊
孫昊

Posted on

10 ASC API Scripts Every Indie iOS Dev Should Have

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()
Enter fullscreen mode Exit fullscreen mode

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']}")
Enter fullscreen mode Exit fullscreen mode

Output:

com.jiejuefuyou.autochoice: AutoChoice
com.jiejuefuyou.altitudenow: AltitudeNow
com.jiejuefuyou.daysuntil: DaysUntil
com.jiejuefuyou.promptvault: PromptVault
Enter fullscreen mode Exit fullscreen mode

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']}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

⚠️ 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()
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)