agentsclimarketplace

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

Install
npx -y skills add wei18/apple-dev-skills --skill asc-api-automation

Assembled 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 .ipa upload; 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.
  • .p8 key storage & leak prevention → [[build-time-secret-injection]] (Layer 2 secrets/.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 kindWhereJWT identity claimUse for
Team key (default)Users and Access → Integrations; role-scopediss = Issuer IDCI and shared tooling — pick the least-privilege role that works (App Manager covers release ops; avoid Admin)
Individual keyYour user profile → Individual API Keysub: "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/json with the body wrapped as {"data": {"type": ..., "id": ..., "attributes": {...}, "relationships": {...}}}.
  • Pagination — pass limit=200 (the max) and follow links.next until 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 429 RATE_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 or scope problem, 409 = resource state problem (e.g. version not in a submittable state).

Endpoint cookbook

GoalCall
App's ASC idGET /v1/apps?filter[bundleId]=<bundle-id>
Latest processed buildsGET /v1/builds?filter[app]=<appId>&sort=-uploadedDate&limit=5
TestFlight what's-newGET /v1/builds/<id>/betaBuildLocalizationsPATCH /v1/betaBuildLocalizations/<locId> with attributes.whatsNew
Add build to a beta groupPOST /v1/betaGroups/<groupId>/relationships/builds with {"data":[{"type":"builds","id":"<buildId>"}]}
Create next App Store versionPOST /v1/appStoreVersions — relationship app, attributes platform: "IOS", versionString, releaseType (see Release automation below — don't omit)
Set description / what's-newGET /v1/appStoreVersions/<id>/appStoreVersionLocalizationsPATCH /v1/appStoreVersionLocalizations/<locId>
Attach build to versionPATCH /v1/appStoreVersions/<id>/relationships/build
Set release behaviorPATCH /v1/appStoreVersions/<id> with attributes.releaseTypeMANUAL (release yourself), AFTER_APPROVAL (auto-release the instant Apple approves), or SCHEDULED (auto-release at attributes.earliestReleaseDate, ISO 8601)
Submit for reviewPOST /v1/reviewSubmissions (app + platform) → POST /v1/reviewSubmissionItems (reviewSubmission + appStoreVersion) → PATCH /v1/reviewSubmissions/<id> with attributes.submitted: true
Daily sales reportGET /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 analyticsPOST /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's MARKETING_VERSION (CFBundleShortVersionString) — bump that once at the Xcode-project level (→ [[xcode-cloud-single-track-ci]] build-number & version automation), then read it back for the appStoreVersions call instead of maintaining a second version counter in release tooling.
  • Changelog → whatsNew, generated once, written twice. Build the "What's New" text from git log <last-tag>..HEAD --oneline (or your conventional-commit tooling) in the release job, then PATCH it into both betaBuildLocalizations (TestFlight) and appStoreVersionLocalizations (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); skipping releaseType in 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 to MANUAL in 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 scope claim 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

  1. iss set to Team ID — ASC API wants the Issuer ID (UUID); Team ID belongs to other Apple JWTs (e.g. APNs). Symptom: 401 NOT_AUTHORIZED with a well-formed token.
  2. exp more than 20 minutes ahead — token rejected outright; also watch local clock skew on iat.
  3. openssl-signed tokens failingopenssl dgst emits a DER-encoded signature; JWT ES256 requires the raw 64-byte r‖s form. CryptoKit's rawRepresentation is already correct.
  4. Uploading the binary via REST — no such endpoint exists; route builds through Xcode Cloud / Organizer / Transporter.
  5. Ignoring pagination — the default page size silently truncates; always limit=200 + follow links.next.
  6. Parsing salesReports as JSON — it's a gzipped TSV file.
  7. Tight-polling build processing or analytics without reading X-Rate-Limit — 429 locks out every consumer of the key.
  8. Admin-role key in CI when App Manager or a scoped token suffices.
  9. .p8 committed to the repo — stop and run the rotate-first SOP in [[apple-public-repo-security]]; storage layout per [[build-time-secret-injection]].
  10. Omitting releaseType on an appStoreVersions create/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. Set releaseType: "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, kid header, iss = Issuer ID (or sub: "user" for individual keys), exp ≤ 20 min, aud appstoreconnect-v1
  • Every collection call sets limit and follows links.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
  • releaseType explicitly set on every appStoreVersions create/update (MANUAL unless auto-release is intended) — never left to ASC's undocumented default
  • grep for key IDs / issuer ID / .p8 contents 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 .p8 live (Layer 2 secrets/.env)
  • [[apple-public-repo-security]] — .p8 leak prevention and the rotate-first SOP
  • [[app-store-review-rejections]] — what to submit so review passes; this skill is how to submit

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.