DEV Community

孫昊
孫昊

Posted on

Apple ASC API: Real JWT Auth + V1/V2 Path Quirks (2026 Edition)

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

3 things you need from ASC:

  1. Key ID (10-char string)
  2. Issuer ID (UUID)
  3. .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
Enter fullscreen mode Exit fullscreen mode

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):

  1. POST /v1/appAvailabilities with new full state (Apple deletes the old)
  2. 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}"}]}
}}}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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)