Skip to content
Back to skills

Asc Api Automation

CSecurity

Use when automating App Store Connect from a script or CI via the ASC REST API (`api.appstoreconnect.apple.com`) — ASC API `.p8` key / JWT auth, TestFlight `betaGroups` / `betaBuildLocalizations`, `appStoreVersions` / `releaseType`, `reviewSubmissions`, `salesReports`, `analyticsReportRequests` — or asked "automate TestFlight / submission / release notes", "should I use fastlane", or debugging 401 NOT_AUTHORIZED / 429 RATE_LIMIT_EXCEEDED, or asked which ASC steps have no API and must be click...

  • 18 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
developmentgorubyswiftbashdebugginggitapisecurityperformance

Works with

  • cli
  • api

Security analysis

C63/100
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • criticalSends environment variables or credentials to an external URL

Pro scans all 4 files and shows the line behind each finding

Scanned September 19, 2026

npx -y skills add wei18/apple-dev-skills --skill asc-api-automation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Asc Api Automation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Asc Api Automation
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/wei18-asc-api-automation/badge)](https://www.skillsdirectory.com/skills/wei18-asc-api-automation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: asc-api-automation
description: 'Use when automating App Store Connect from a script or CI via the ASC REST API (`api.appstoreconnect.apple.com`) — ASC API `.p8` key / JWT auth, TestFlight `betaGroups` / `betaBuildLocalizations`, `appStoreVersions` / `releaseType`, `reviewSubmissions`, `salesReports`, `analyticsReportRequests` — or asked "automate TestFlight / submission / release notes", "should I use fastlane", or debugging 401 NOT_AUTHORIZED / 429 RATE_LIMIT_EXCEEDED, or asked which ASC steps have no API and must be clicked by hand. API side only: build & upload → xcode-cloud-single-track-ci / local-archive-export-upload; `.p8` storage → build-time-secret-injection.'
---

# 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** — builds arrive in ASC via Xcode Cloud (→ `xcode-cloud-single-track-ci`), a local `xcodebuild -exportArchive` / `xcrun altool` run (→ `local-archive-export-upload`), Xcode Organizer, Transporter, or the ASC API's Build Uploads resources (`POST /v1/buildUploads` + `buildUploadFiles` reserve / commit — not part of this cookbook). 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 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:

```bash
# 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 — **exception**: a token carrying `scope` and restricted to GET requests on allow-listed resources can live up to 6 months, per Apple's *Determine the Appropriate Token Lifetime* — but that allow-list is Xcode Cloud / Power-and-Performance resources only (Build Actions, Build Runs, Git References, Issues, macOS Versions, Products, Providers, Power and Performance Metrics and Logs, Pull Requests, Repositories, Test Results, Workflows, Xcode Versions); none of this cookbook's endpoints qualify, so release tooling always mints ≤ 20-minute tokens), `aud: "appstoreconnect-v1"`, optional `scope` (array of allowed requests like `"GET /v1/apps"` — pin single-purpose tokens to single endpoints).

The script is bundled at `${CLAUDE_SKILL_DIR}/scripts/mint-asc-token.swift` (the `scripts/` folder next to this SKILL.md; CryptoKit, zero dependencies) — run it by that path, or copy it into your repo's `scripts/` first, which the example below assumes.

```bash
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

| 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 `reviewSubmissions` model introduced in App Store Connect API 1.7, which replaced the deprecated one-shot `appStoreVersionSubmissions`.

## Pre-submission prerequisites the three-call flow doesn't mention

The `POST reviewSubmissions` → `POST reviewSubmissionItems` → `PATCH submitted: true` sequence above assumes the app is already submittable; in practice three preconditions block it and have no REST fix:

| Blocker | Symptom | Fix | Where |
|---|---|---|---|
| App pricing not set | `POST reviewSubmissions` fails with a pricing-state error (exact error-code string not found in Apple's published API reference — observed in practice) | Set the price once, via the API (`POST /v1/appPriceSchedules`, "Add a Scheduled Price Change to an App") or the web UI | Needs the Paid Apps Agreement accepted first — web-UI-only (see "Steps with no ASC API at all" below) |
| Empty `copyright` attribute | An `ENTITY_ERROR.ATTRIBUTE.REQUIRED`-shaped error — documented for other required fields (e.g. `companyName`); the same shape is observed in practice for a blank `copyright`, not confirmed against Apple's official attribute reference | Always send a non-empty `copyright` string | API |
| Leftover non-`COMPLETE` `reviewSubmissions` | A new `POST /v1/reviewSubmissions` is refused; the resource has no DELETE (unlike the deprecated pre-2022 `appStoreVersionSubmissions`, which did) | `PATCH /v1/reviewSubmissions/<id>` with `{"data":{"type":"reviewSubmissions","id":"<id>","attributes":{"canceled":true}}}` — `state` is read-only and transitions to `CANCELING` in response; during `WAITING_FOR_REVIEW`, canceling returns the version to an editable state (observed as `DEVELOPER_REJECTED`) without leaving an Apple rejection record | API |

## Steps with no ASC API at all — must be clicked by a human

The three prerequisites above are gaps in an otherwise-scriptable flow. These are different:
Apple publishes no REST resource for them at all, so no amount of scripting closes the gap —
budget a manual, one-time (or rarely-repeated) click in the ASC web UI. For the two verified
cases (Agreements/Tax/Banking, and whole-app Promo Codes vs Subscription Offer Codes) and their
sourcing, read `references/no-api-steps.md`.

## 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 (unless it's a `scope`d, GET-only token on allow-listed resources, which can live up to 6 months); also watch local clock skew on `iat`.
3. **openssl-signed tokens failing** — `openssl 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, `local-archive-export-upload`, Organizer, or 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 `scope`d 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 (or a `scope`d GET-only token, up to 6 months), `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
- `local-archive-export-upload` — the local `xcodebuild -exportArchive` / `altool` upload path; 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
- Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.

Files in this skill

  • SKILL.md15 KB
  • references/no-api-steps.md2.3 KB
  • references/official-docs.md1.4 KB
  • scripts/mint-asc-token.swift2.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.