DEV Community

孫昊
孫昊

Posted on

10 App Store Connect API scripts I actually use to manage 4 iOS apps (without clicking through the web UI)

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

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

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

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

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

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

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

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

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

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

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

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

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

See the full Day 60 indie hacker tool stack ->

Top comments (0)