Asc api automation
Skill wei18/apple-dev-skills/apple-dev-skills/skills/asc-api-automation
Reusable Claude Code skills for AI-agent-driven Swift / Apple-platform development — composable via git submodule; aggregates other specialist skill repos
npx -y skills add wei18/apple-dev-skills --skill asc-api-automationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when automating App Store Connect from a CLI or CI — mint the ES256 JWT from an ASC API `.p8` key (`kid`, `iss` = Issuer ID, `aud` `appstoreconnect-v1`, exp ≤ 20 min), then drive `api.appstoreconnect.apple.com` with curl for TestFlight `betaGroups` / `betaBuildLocalizations`, `appStoreVersions` metadata, `reviewSubmissions` submit-for-review, `salesReports` / `analyticsReportRequests`. No fastlane, no Ruby — a CryptoKit token script + curl. Invoke when asked "automate TestFlight / submission / release notes", writing release tooling, or debugging 401 NOT_AUTHORIZED / 429 RATE_LIMIT_EXCEEDED. API-side ops only: build & upload → xcode-cloud-single-track-ci; `.p8` storage / leak prevention → build-time-secret-injection + apple-public-repo-security.
SKILL.md
12.9 KB, as published. Nobody here has run it
App Store Connect API Automation
This catalog's default way to drive App Store Connect from scripts and CI: an Apple-native toolchain — a ~30-line Swift/CryptoKit script mints the JWT, plain curl calls the REST API. No fastlane/spaceship, no Ruby gems, no Homebrew, consistent with the catalog's mise + no-third-party baselines.
When to invoke
- Writing release tooling: distribute a build to TestFlight groups, set what's-new, create the next App Store version, submit for review
- Pulling sales or App Store analytics reports on a schedule
- Debugging ASC API auth failures (401
NOT_AUTHORIZED) or rate limiting (429) - Asked "should I use fastlane for this?"
Scope
Owns: token minting, curl conventions, and the endpoint cookbook below. Does NOT own:
- Building / uploading the binary — there is no REST endpoint for
.ipaupload; builds arrive in ASC via Xcode Cloud (→ [[xcode-cloud-single-track-ci]]), Xcode Organizer, or Transporter. This skill picks up after the build exists in ASC. .p8key storage & leak prevention → [[build-time-secret-injection]] (Layer 2secrets/.env) + [[apple-public-repo-security]] (rotate-first SOP).- What metadata will pass review → [[app-store-review-rejections]]; this skill is how to submit, not what.
Keys: create, scope, store
| Key kind | Where | JWT identity claim | Use for |
|---|---|---|---|
| Team key (default) | Users and Access → Integrations; role-scoped | iss = Issuer ID | CI and shared tooling — pick the least-privilege role that works (App Manager covers release ops; avoid Admin) |
| Individual key | Your user profile → Individual API Key | sub: "user" (no iss) | Personal one-off scripts; inherits your permissions |
Store per [[build-time-secret-injection]] Layer 2:
# secrets/.env (gitignored; .env.example committed)
ASC_KEY_ID=2X9R4HXF34
ASC_ISSUER_ID=57246542-96fe-1a63-e053-0824d011072a
ASC_KEY_PATH=secrets/AuthKey_2X9R4HXF34.p8
Mint the token (CryptoKit, zero dependencies)
Claims (verified against Apple's Generating tokens for API requests, 2026-07): header alg: ES256 (the only accepted algorithm), kid, typ: JWT; payload iss (Issuer ID, the UUID from Users and Access → Integrations — not your Team ID), iat, exp (invalid if more than 20 minutes ahead), aud: "appstoreconnect-v1", optional scope (array of allowed requests like "GET /v1/apps" — pin single-purpose tokens to single endpoints).
scripts/mint-asc-token.swift:
#!/usr/bin/env swift
import CryptoKit
import Foundation
let env = ProcessInfo.processInfo.environment
guard let keyID = env["ASC_KEY_ID"], let issuerID = env["ASC_ISSUER_ID"],
let keyPath = env["ASC_KEY_PATH"] else {
FileHandle.standardError.write(Data("Set ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_PATH (source secrets/.env)\n".utf8))
exit(1)
}
func b64url(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
let now = Int(Date().timeIntervalSince1970)
let header = #"{"alg":"ES256","kid":"\#(keyID)","typ":"JWT"}"#
// Apple rejects exp > 20 min ahead; 10 min leaves slack for clock skew.
let payload = #"{"iss":"\#(issuerID)","iat":\#(now),"exp":\#(now + 600),"aud":"appstoreconnect-v1"}"#
let signingInput = b64url(Data(header.utf8)) + "." + b64url(Data(payload.utf8))
let pem = try String(contentsOfFile: keyPath, encoding: .utf8)
let key = try P256.Signing.PrivateKey(pemRepresentation: pem)
let signature = try key.signature(for: Data(signingInput.utf8)) // ECDSA + SHA-256 = ES256
print(signingInput + "." + b64url(signature.rawRepresentation)) // rawRepresentation = r‖s, the JWT wire format
source secrets/.env
ASC_TOKEN=$(swift scripts/mint-asc-token.swift)
curl -sf -H "Authorization: Bearer $ASC_TOKEN" "https://api.appstoreconnect.apple.com/v1/apps?limit=200"
Mint fresh per run; a job that outlives the token re-mints instead of extending exp.
curl conventions
- JSON:API shape — write calls send
Content-Type: application/jsonwith the body wrapped as{"data": {"type": ..., "id": ..., "attributes": {...}, "relationships": {...}}}. - Pagination — pass
limit=200(the max) and followlinks.nextuntil absent; the small default page size silently truncates lists otherwise. - Rate limit — every response carries
X-Rate-Limit: user-hour-lim:3500; user-hour-rem:…(rolling hour, per key; Apple says actual limits vary). Exceeding returns 429RATE_LIMIT_EXCEEDED— back off and re-queue; a 429 lockout hits everything sharing that key, including CI. - Errors are structured — read
.errors[].detail. Triage: 401 = token/claims problem, 403 = key role orscopeproblem, 409 = resource state problem (e.g. version not in a submittable state).
Endpoint cookbook
| Goal | Call |
|---|---|
| App's ASC id | GET /v1/apps?filter[bundleId]=<bundle-id> |
| Latest processed builds | GET /v1/builds?filter[app]=<appId>&sort=-uploadedDate&limit=5 |
| TestFlight what's-new | GET /v1/builds/<id>/betaBuildLocalizations → PATCH /v1/betaBuildLocalizations/<locId> with attributes.whatsNew |
| Add build to a beta group | POST /v1/betaGroups/<groupId>/relationships/builds with {"data":[{"type":"builds","id":"<buildId>"}]} |
| Create next App Store version | POST /v1/appStoreVersions — relationship app, attributes platform: "IOS", versionString, releaseType (see Release automation below — don't omit) |
| Set description / what's-new | GET /v1/appStoreVersions/<id>/appStoreVersionLocalizations → PATCH /v1/appStoreVersionLocalizations/<locId> |
| Attach build to version | PATCH /v1/appStoreVersions/<id>/relationships/build |
| Set release behavior | PATCH /v1/appStoreVersions/<id> with attributes.releaseType — MANUAL (release yourself), AFTER_APPROVAL (auto-release the instant Apple approves), or SCHEDULED (auto-release at attributes.earliestReleaseDate, ISO 8601) |
| Submit for review | POST /v1/reviewSubmissions (app + platform) → POST /v1/reviewSubmissionItems (reviewSubmission + appStoreVersion) → PATCH /v1/reviewSubmissions/<id> with attributes.submitted: true |
| Daily sales report | GET /v1/salesReports?filter[frequency]=DAILY&filter[reportType]=SALES&filter[reportSubType]=SUMMARY&filter[vendorNumber]=<n> (+ filter[reportDate]=YYYY-MM-DD for a specific day) — response is a gzipped TSV, not JSON: curl -o report.gz then gunzip |
| App Store analytics | POST /v1/analyticsReportRequests (accessType: "ONGOING") once per app, then poll its reports → instances → segments for download URLs |
The review-submission flow is the 2022+ reviewSubmissions model, which replaced the deprecated one-shot appStoreVersionSubmissions.
Release automation: SemVer, changelog, and explicit releaseType
versionString= SemVer, sourced from the build, not reinvented in CI. Set it to the same value as the archived build'sMARKETING_VERSION(CFBundleShortVersionString) — bump that once at the Xcode-project level (→ [[xcode-cloud-single-track-ci]] build-number & version automation), then read it back for theappStoreVersionscall instead of maintaining a second version counter in release tooling.- Changelog →
whatsNew, generated once, written twice. Build the "What's New" text fromgit log <last-tag>..HEAD --oneline(or your conventional-commit tooling) in the release job, thenPATCHit into bothbetaBuildLocalizations(TestFlight) andappStoreVersionLocalizations(App Store) per locale — one generated string, two writes, so testers and reviewers see the same notes. releaseType— set it explicitly, every time. The attribute is optional and Apple's schema documents no default value. The ASC web UI backs the same behavior with an explicit 3-way choice (Manually release this version / Automatically release this version / Automatically release this version after App Review, no earlier than); skippingreleaseTypein a scripted create/update leaves the version's release behavior to an undocumented default — in practice new versions show up as auto-release — instead of a decision your pipeline made on purpose. Default toMANUALin automation unless auto-release is the deliberate intent.
Rationale
- No fastlane by default: spaceship drags a Ruby toolchain into a repo whose only Ruby consumer would be release tooling — against the catalog's mise/no-Homebrew baseline — and inserts a drift layer that breaks whenever ASC changes ahead of a spaceship release. Direct REST against Apple's own docs has zero intermediary.
- CryptoKit mint script: token minting is the only genuinely fiddly step (ES256, raw-signature format); everything after is plain curl. A 30-line Apple-native script beats installing PyJWT or hand-rolling openssl DER conversion.
- Least privilege: token-leak blast radius scales with the key's role; the
scopeclaim can pin a single script to a single request.
Deviation considerations
- Already on fastlane with maintained lanes and a Gemfile → keep it; this skill is the default for repos without a Ruby toolchain, not a migration mandate.
- Heavy tooling (dozens of endpoints, typed models, retries) → generate a client from Apple's published App Store Connect OpenAPI spec instead of hand-rolled curl; still no fastlane required.
- One-off manual task → the ASC web UI is faster; scripting has a floor cost.
Common Mistakes
issset to Team ID — ASC API wants the Issuer ID (UUID); Team ID belongs to other Apple JWTs (e.g. APNs). Symptom: 401NOT_AUTHORIZEDwith a well-formed token.expmore than 20 minutes ahead — token rejected outright; also watch local clock skew oniat.- openssl-signed tokens failing —
openssl dgstemits a DER-encoded signature; JWT ES256 requires the raw 64-byte r‖s form. CryptoKit'srawRepresentationis already correct. - Uploading the binary via REST — no such endpoint exists; route builds through Xcode Cloud / Organizer / Transporter.
- Ignoring pagination — the default page size silently truncates; always
limit=200+ followlinks.next. - Parsing
salesReportsas JSON — it's a gzipped TSV file. - Tight-polling build processing or analytics without reading
X-Rate-Limit— 429 locks out every consumer of the key. - Admin-role key in CI when App Manager or a
scoped token suffices. .p8committed to the repo — stop and run the rotate-first SOP in [[apple-public-repo-security]]; storage layout per [[build-time-secret-injection]].- Omitting
releaseTypeon anappStoreVersionscreate/update — the attribute has no documented default, so an unset value can leave a version on automatic release; it goes live the instant Apple approves it instead of waiting for a deliberate manual release. SetreleaseType: "MANUAL"explicitly unless auto-release is intended.
Review Checklist
- Team key with least-privilege role;
.p8+ IDs stored per build-time-secret-injection Layer 2, nothing tracked by git - Token claims: ES256,
kidheader,iss= Issuer ID (orsub: "user"for individual keys),exp≤ 20 min,audappstoreconnect-v1 - Every collection call sets
limitand followslinks.next - 429 handled with backoff; no tight polling loops
- Write calls use the JSON:API
{"data": {...}}wrapper - No REST binary-upload attempt; build side delegated to xcode-cloud-single-track-ci
-
releaseTypeexplicitly set on everyappStoreVersionscreate/update (MANUALunless auto-release is intended) — never left to ASC's undocumented default -
grepfor key IDs / issuer ID /.p8contents across tracked files returns zero hits
Related skills
- [[xcode-cloud-single-track-ci]] — build & upload side; this skill starts after the build exists in ASC
- [[build-time-secret-injection]] — where
ASC_KEY_ID/ASC_ISSUER_ID/ the.p8live (Layer 2secrets/.env) - [[apple-public-repo-security]] —
.p8leak prevention and the rotate-first SOP - [[app-store-review-rejections]] — what to submit so review passes; this skill is how to submit