# Technical Documentation: rorkai/App-Store-Connect-CLI

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/rorkai/App-Store-Connect-CLI)

### Overview (`README.md`)

# App Store Connect CLI

<p align="center">
  <a href="https://github.com/rorkai/App-Store-Connect-CLI/releases/latest"><img src="https://img.shields.io/github/v/release/rorkai/App-Store-Connect-CLI?style=for-the-badge&color=blue" alt="Latest Release"></a>
  <a href="https://github.com/rorkai/App-Store-Connect-CLI/stargazers"><img src="https://img.shields.io/github/stars/rorkai/app-store-connect-cli?style=for-the-badge" alt="GitHub Stars"></a>
  <img src="https://img.shields.io/github/go-mod/go-version/rorkai/App-Store-Connect-CLI?filename=go.mod&style=for-the-badge&logo=go" alt="Go version from go.mod">
  <img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License">
  <img src="https://img.shields.io/badge/Homebrew-compatible-blue?style=for-the-badge" alt="Homebrew">
</p>

<p align="center">
  <img src="docs/images/banner.png" alt="asc -- App Store Connect CLI" width="600">
</p>

A fast, lightweight, and scriptable CLI for the App Store Connect API.
Automate iOS, macOS, tvOS, and visionOS release workflows from your terminal, IDE, or CI/CD pipeline.

## Table of Contents

- [asc skills](#asc-skills)
- [Sponsors](#sponsors)
- [Quick Start](#quick-start)
- [Troubleshooting](#troubleshooting)
- [Privacy and telemetry](#privacy-and-telemetry)
- [Support](#support)
- [Wall of Apps](#wall-of-apps)
- [Common Workflows](#common-workflows)
- [Commands and Reference](#commands-and-reference)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)

## asc skills

Agent Skills for automating `asc` workflows including builds, TestFlight, metadata sync, submissions, and signing:
https://github.com/rorkai/app-store-connect-cli-skills

Install them globally so they are available across projects:

```bash
asc install-skills
```

`asc install-skills` checks out reviewed commit
`f52c4f04323bb2dfb21ca8be82e6494e9cd0b4d8` and copies its 25 skills directly
into the standard global agent-skills directory. It verifies the complete pack
and every installed file before succeeding, preserves unrelated skills and
unrelated lock entries, and pins the 25 ASC lock entries to the same reviewed
commit so external checks cannot update them from a mutable branch. It rolls
back the pack if any replacement fails. Only
`git` is required; the command does not execute Node.js, `npx`, an npm package,
or repository scripts.

```bash
git --version
asc install-skills
```

## Quick Start

If you want to confirm the binary works before configuring authentication:

```bash
asc version
asc --help
```

### 1. Install

```bash
# Homebrew (recommended)
brew install asc

# Install script (macOS/Linux)
curl -fsSL https://asccli.sh/install | bash
```

```powershell
# Windows (WinGet, once the package is accepted)
winget install asc

# Exact fallback when scripting
winget install --id Rorkai.ASC --exact
```

The WinGet package is tracked in
[GitHub Discussion #1552](https://github.com/rorkai/App-Store-Connect-CLI/discussions/1552).
Until it appears in `winget search asc`, Windows users can download the signed
release binaries directly from the
[GitHub releases page](https://github.com/rorkai/App-Store-Connect-CLI/releases/latest).

For source builds and contributor setup, see [CONTRIBUTING.md](CONTRIBUTING.md).
Released binaries are self-contained and do not require a Go installation;
source builds use the toolchain version declared by `go.mod`.
Releases built with Go 1.27 require macOS 13 Ventura or later. On older Macs,
retain an older compatible release or upgrade macOS before updating `asc`.

### 2. Authenticate

```bash
asc auth login \
  --name "MyApp" \
  --key-id "ABC123" \
  --issuer-id "DEF456" \
  --private-key /path/to/AuthKey.p8 \
  --network
```

Individual API keys have no issuer ID. Omit `--issuer-id` and pass `--key-type individual`:

```bash
asc auth login \
  --name "MyIndividualKey" \
  --key-id "ABC123" \
  --key-type individual \
  --private-key /path/to/AuthKey.p8
```

Generate API keys at:
https://appstoreconnect.apple.com/access/integrations/api

If you are running in CI, a headless shell, or a machine where keychain access is not available, use config-backed auth instead:

```bash
asc auth login \
  --bypass-keychain \
  --name "MyCIKey" \
  --key-id "ABC123" \
  --issuer-id "DEF456" \
  --private-key /path/to/AuthKey.p8
```

### 3. Validate auth

```bash
asc auth status --validate
asc auth doctor
```

### 4. First command

```bash
asc apps list --output table
asc apps list --output json --pretty
```

### Output defaults (TTY-aware)

`asc` chooses a default `--output` based on where stdout is connected:

- Interactive terminal (TTY): `table`
- Non-interactive output (pipes/files/CI): `json`

You can still set a global preference:

```bash
export ASC_DEFAULT_OUTPUT=markdown
```

And explicit flags always win:

```bash
asc apps list --output json
```

### Compatibility

Commands are supported as documented. Deprecation warnings identify the
available migration path and the command's long-term replacement.

## Troubleshooting

### Homebrew

- Refresh Homebrew first: `brew update && brew upgrade asc`
- Check which binary you are running: `which asc`
- Confirm the installed version: `asc version`
- If Homebrew is behind the latest GitHub release, use the install script from `https://asccli.sh/install`

### WinGet

- Refresh WinGet sources first: `winget source update`
- Prefer the short install once available: `winget install asc`
- If the short name ever becomes ambiguous, use the package identifier: `winget install --id Rorkai.ASC --exact`
- Confirm the installed command resolves: `Get-Command asc` and `asc version`

### Authentication

- Validate the active profile: `asc auth status --validate`
- Run the auth health check: `asc auth doctor`
- If keychain access is blocked, retry with `ASC_BYPASS_KEYCHAIN=1` or re-run `asc auth login --bypass-keychain`
- Use `asc auth login --local --bypass-keychain ...` when you want repo-local credentials in `./.asc/config.json`

### Apple service health

- Check Apple's developer services without credentials: `asc system-status`
- Narrow unexpected API or upload failures: `asc system-status --service "App Store Connect"`
- Poll only when requested: `asc system-status --watch --poll-interval 30s`
- Use `--issues-only` for a concise incident view; summary counts still cover all matched services

### Output

- `asc` defaults to `table` in an interactive terminal and `json` in pipes, files, and CI
- Use an explicit format when scripting or sharing repro steps: `--output json`, `--output table`, or `--output markdown`
- Use `--pretty` with JSON when you want readable output in terminals or bug reports
- Set a personal default with `ASC_DEFAULT_OUTPUT`, but remember `--output` always wins

## Privacy and telemetry

`asc` sends pseudonymous command-level usage telemetry by default to help
maintainers understand which commands are used and where reliability work is
needed. Local events include a random installation ID, which lets events from
one installation be grouped over time; it is not derived from an Apple account
or machine identifier.

Telemetry includes the CLI version, operating system and architecture,
registered command path, duration, runtime context, invocation source, a
bounded outcome class, and the HTTP status when an API request fails. It may
include a sanitized public flag name. It does **not** include raw arguments,
stderr, error messages, flag values, response bodies, credentials, private
keys, Apple account, team, or issuer IDs, app or bundle IDs, usernames,
hostnames, repository names, or file paths.

Review or change telemetry at any time:

```bash
asc telemetry status
asc telemetry disable
asc telemetry reset-id
```

`ASC_TELEMETRY_DISABLED=1` and `DO_NOT_TRACK=1` also disable telemetry. See the
[telemetry reference](commands/telemetry.mdx) for the exact event payload,
runtime handling, collector endpoint, and all controls.

## Support

- Use [GitHub Discussions](https://github.com/rorkai/App-Store-Connect-CLI/discussions) for install help, authentication setup, workflow advice, and "how do I...?" questions
- Use [GitHub Issues](https://github.com/rorkai/App-Store-Connect-CLI/issues) for reproducible bugs and concrete feature requests
- See [SUPPORT.md](SUPPORT.md) for the support policy and bug-report checklist
- Before filing an auth or API bug, retry with `ASC_BYPASS_KEYCHAIN=1`; if it is safe to do so, include redacted output from `ASC_DEBUG=api asc ...` or `asc --api-debug ...`

## Wall of Apps

[See the Wall of Apps →](https://asccli.sh/#wall-of-apps)

Want to add yours?
`asc apps wall submit --app "1234567890" --confirm`

The command uses your authenticated `gh` session to fork the repo and open a pull request that updates `docs/wall-of-apps.json`.
It resolves the public App Store name, URL, and icon from the app ID automatically. For manual entries that are not on the public App Store yet, use `--link` with `--name`.
Use `asc apps wall submit --dry-run` to preview the fork, branch, and PR plan before creating anything.

## Common Workflows

### TestFlight feedback and crashes

```bash
asc testflight feedback list --app "123456789" --paginate
asc testflight crashes list --app "123456789" --sort -createdDate --limit 10
asc testflight crashes log --submission-id "SUBMISSION_ID"
```

### Builds and distribution

```bash
asc builds upload --app "123456789" --ipa "/path/to/MyApp.ipa"
asc builds list --app "123456789" --output table
asc testflight groups list --app "123456789" --output table
```

For macOS TestFlight distribution, upload the exported `.pkg` first, then add
the processed build to a beta group:

```bash
asc builds upload --app "123456789" --pkg "./build/MyMacApp.pkg" --version "1.2.3" --build-number "42" --wait --output json
asc builds add-groups --app "123456789" --build-number "42" --version "1.2.3" --platform MAC_OS --group "Internal Testers"
```

`--app` is the App Store Connect app ID. If you use local Xcode build flags such
as `--archive-path`, also pass exactly one of `--workspace` or `--project` plus
`--scheme`; otherwise use a pre-exported `.ipa` or `.pkg` upload. Add
`--submit --confirm` to `asc builds add-groups` when distributing to an external
TestFlight group that needs beta app review submission.

### Release (high-level App Store publish flow)

```bash
# Optional: preview the staging plan before submission
asc release stage --app "123456789" --version "1.2.3" --build-id "BUILD_ID" --copy-metadata-from "1.2.2" --dry-run

# Canonical upload + attach + submit command
asc publish appstore --app "123456789" --ipa "/path/to/MyApp.ipa" --version "1.2.3" --submit --confirm

# Wait for the App Review decision (exit 0 approved, 1 rejected, 7 still pending at --timeout)
asc status --app "123456789" --until review-done --timeout 2h
```

Lower-level submission lifecycle commands (for debugging or partial workflows):

```bash
# Canonical readiness check
asc validate --app "123456789" --version "1.2.3"
asc submit status --version-id "VERSION_ID"
asc submit cancel --version-id "VERSION_ID" --confirm
```

Readiness validation also warns when localized app names, subtitles,
descriptions, keywords, promotional text, or What's New copy still contains an
unmistakable template marker such as `Lorem ipsum`, `TODO`, `TBD`, or `FIXME`.
`Lorem ipsum` matches in any letter case. `TODO`, `TBD`, and `FIXME` match only
in uppercase because their lowercase spellings can be ordinary product wording.
The warning includes the locale, field, matched text, resource ID, and a
remediation step. It is advisory by default and becomes blocking only with
`--strict`:

```bash
asc validate --app "123456789" --version "1.2.3" --output json
asc validate --app "123456789" --version "1.2.3" --strict
```

Add the `--deep` mode when a release needs checks that Apple
exposes only through its signed-in web app:

```bash
asc web auth login --apple-id "user@example.com"
asc validate --app "123456789" --version "1.2.3" --deep
```

Deep validation reuses the file-backed cache without opening a login, Keychain,
password, or 2FA prompt. It checks App Privacy publication, relevant agreements,
and the first auto-renewable subscription attachment; availability and required
App Review fields still come from the public API. Paid-agreement relevance also
uses the app's current public price so an upfront-paid app isn't mistaken for a
free one. The private Apple endpoints can change without notice, so an
unavailable or changed response is `unverified`, not a false blocker. Add
`--strict` if CI must fail when a requested deep check can't be verified.

This lint intentionally does not judge platform names, roadmap language, or
beta/demo wording because those phrases can be legitimate product copy and
cannot be classified reliably offline. It also leaves shorter Lorem Ipsum
product wording and ordinary localized `TODO` copy without marker punctuation
unflagged; only template-like residue is reported.

### Review status and blockers

```bash
asc review status --app "123456789"
asc review doctor --app "123456789"
```

### Metadata and localization

```bash
asc localizations list --app "123456789" --type app-info
asc metadata init --dir "./metadata" --version "1.2.3" --locale "en-US"
# Fill in the values to apply and delete keys to leave unchanged; empty values are rejected.
asc metadata apply --app "123456789" --version "1.2.3" --dir "./metadata" --dry-run
asc metadata keywords audit --app "123456789" --version "1.2.3" --blocked-terms-file "./blocked-terms.txt"
asc apps info view --app "123456789" --output json --pretty
```

Use `asc metadata keywords audit` before `sync` or `apply` when you want an ASO-focused
review of live keyword metadata across locales. It reports duplicate phrases, repeated
terms across locales, overlap with localized app name or subtitle, byte-budget usage,
and optional blocked terms from repeated `--blocked-term` flags or a text file.

### Screenshots and media

```bash
asc screenshots plan --app "123456789" --version "1.2.3" --review-output-dir "./screenshots/review"
asc screenshots apply --app "123456789" --version "1.2.3" --review-output-dir "./screenshots/review" --confirm
asc screenshots list --version-localization "VERSION_LOCALIZATION_ID"
asc video-previews list --version-localization "VERSION_LOCALIZATION_ID"
```

Uploading screenshots for a single locale:

```bash
asc apps list
asc versions list --app "APP_ID"
asc localizations list --version "VERSION_ID" --output json --locale "en-US" | jsonpp
asc screenshots upload --version-localization "VERSION_LOCALIZATION_ID" --path "./screenshots/en-US" --device-type "IPHONE_65" --replace --confirm --max-screenshots 10
```

`VERSION_LOCALIZATION_ID` is the App Store version localization resource ID
from `data[].id`, not the locale code from `attributes.locale`.

For local capture coverage across multiple devices, locales, appearances, and
content fixtures, use a matrix plan. Targets must already be
booted simulators; the command writes raw artifacts and an offline review
report, without uploading to App Store Connect:

```bash
asc screenshots matrix --plan .asc/screenshots-matrix.json --max-concurrency 2 --output json --pretty
```

See [docs/design/screenshots-matrix.md](docs/design/screenshots-matrix.md) for
the JSONC schema, isolated output layout, retry behavior, and review contract.

### Signing and bundle IDs

```bash
asc bundle-ids capabilities list --bundle "BUNDLE_ID"
asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --output .asc/signing
asc signing sync pull --repo git@github.com:team/signing.git --password-file ~/.config/asc/signing-sync-password --output-dir .asc/signing/pulled
```

`signing fetch` downloads public certificates and provisioning profiles. A
usable signing identity also needs the matching private key; `signing sync`
can verify that local identity and share it through an encrypted store (Git by
default). When the team has no active certificate, `signing fetch
--create-missing --create-missing-certificate` creates the key, certificate,
profile, and `.p12` in one pass. `signing keychain` installs and manages a
dedicated keychain, and `xcode signing plan --profile` maps downloaded profiles
onto Xcode targets; [docs/CI_CD.md](docs/CI_CD.md) lists the fresh-runner
sequence. For multi-target release testing, `signing reconcile` plans exact
device/profile changes. `signing run` provides a temporary macOS keychain only
for single-target archives. Multi-target exports must import the identity into
a job-scoped keychain and install every reconciled profile for the job-exclusive
macOS user. See the
[signing guide](guides/code-signing.mdx) for setup, CI, rotation, security
boundaries, and troubleshooting.

### Workflow automation

```bash
asc workflow validate --output json
asc workflow run --dry-run testflight_beta VERSION:1.2.3
```

### Raw API requests

```bash
asc api GET /v1/apps --query limit=5
asc api GET /v1/apps/APP_ID/relationships/builds --paginate
asc api PATCH /v1/apps/APP_ID --confirm --body-file update.json
```

### Verified local Xcode -> TestFlight workflow

See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) for local compile validation with
`asc xcode build` and a copyable `.asc/deployment.json`, `.asc/workflow.json`,
and `ExportOptions.plist` that use `asc builds next-build-number`,
`asc xcode inject`, `asc xcode archive`, `asc xcode export --timeout 10m`, and
`asc publish testflight --group ... --wait`.
Add `--submit --confirm` when
distributing to an external TestFlight group that needs beta app review submission.

The `asc xcode test` command provides local unit/UI test execution
with structured results and preserved `.xcresult` bundles; see the same workflow
guide for an invocation.

```bash
asc workflow validate --output json
asc xcode inject --manifest .asc/deployment.json --set version=1.2.3 --set build_number=42 --dry-run --output json
asc xcode build --project App.xcodeproj --scheme App --destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=27.0' --no-code-signing --output json
asc workflow run --dry-run testflight_beta VERSION:1.2.3
asc workflow run testflight_beta VERSION:1.2.3
```

Replace the example Xcode destination with a simulator installed on the host.

### Xcode Cloud workflows and build runs

```bash
# Trigger from a pull request
asc xcode-cloud run --workflow-id "WORKFLOW_ID" --pull-request-id "PR_ID"

# Rerun from an existing build run with a clean build
asc xcode-cloud run --source-run-id "BUILD_RUN_ID" --clean

# Fetch a single build run by ID
asc xcode-cloud build-runs view --id "BUILD_RUN_ID"
```

### Apple Ads campaign management

Apple Ads uses separate OAuth credentials from App Store Connect:

```bash
asc ads auth login --name "Marketing" --client-id "SEARCHADS_CLIENT_ID" --team-id "SEARCHADS_TEAM_ID" --key-id "KEY_ID" --private-key ./ads-key.pem --ad-account "987654"
asc ads auth discover --output json
asc ads campaigns find --ad-account "987654" --file query.json --output json
asc ads reports apps campaigns --ad-account "987654" --file report.json --output json
```

See [guides/apple-ads-playbooks.mdx](guides/apple-ads-playbooks.mdx) for
operator playbooks covering credential safety, org inspection, read-only smoke
tests, reporting, raw API usage, and guarded mutations.

### StoreKit server APIs

The App Store Server API and Retention Messaging use a dedicated In-App
Purchase API key, separate from App Store Connect API credentials:

```bash
asc storekit auth login --name Production --key-id "KEY_ID" --issuer-id "ISSUER_ID" --private-key ./SubscriptionKey.p8 --bundle-id com.example.app
asc storekit auth doctor --environment sandbox --network
asc storekit transactions history --transaction-id "2000000000000001" --environment production --paginate
asc storekit subscriptions status --transaction-id "2000000000000001" --environment production --output table
asc storekit notifications history --start 2026-09-01 --end 2026-09-08 --only-failures --environment production
asc storekit retention-messaging messages list --environment sandbox --output json
```

JSON output prints Apple's response unmodified. `--decode` adds decoded JWS
payloads without verifying Apple's signature, so don't grant entitlements from
decoded output alone.

See [docs/architecture/storekit-retention-messaging.md](docs/architecture/storekit-retention-messaging.md)
for the endpoint map, message and image requirements, environment variables,
and the full sandbox verification sequence.

## Commands and Reference

Use built-in help as the source of truth:

```bash
asc --help
asc <command> --help
asc <command> <subcommand> --help
```

Reference hierarchy:

- `asc --help`: authoritative command and flag surface
- `docs/COMMANDS.md`: generated top-level taxonomy map
- `asc docs show workflows`: curated workflow recipes
- `asc docs show reference`: repo-local quick reference template used by `asc init`

For full command families, flags, and discovery patterns, see:
- [docs/COMMANDS.md](docs/COMMANDS.md)

## Documentation

- [docs/CI_CD.md](docs/CI_CD.md) - CI/CD integration guides (GitHub Actions, GitLab, Bitrise, CircleCI)
- [commands/signing.mdx](commands/signing.mdx) - signing identities, encrypted sync, CI, and release-testing profiles
- [docs/COMMANDS.md](docs/COMMANDS.md) - Command families and reference navigation
- [docs/PARITY.md](docs/PARITY.md) - Remaining parity areas and intentional non-goals
- [docs/WORKFLOWS.md](docs/WORKFLOWS.md) - Reusable workflow patterns, including local Xcode to TestFlight
- [guides/apple-ads-playbooks.mdx](guides/apple-ads-playbooks.mdx) - Apple Ads operator playbooks
- [docs/architecture/storekit-retention-messaging.md](docs/architecture/storekit-retention-messaging.md) - Retention Messaging setup and sandbox verification
- [docs/API_NOTES.md](docs/API_NOTES.md) - API quirks and behaviors
- [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) - CLI development and testing notes
- [docs/TESTING.md](docs/TESTING.md) - Testing patterns and conventions
- [docs/openapi/README.md](docs/openapi/README.md) - Offline OpenAPI snapshot + update flow
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guide

## Acknowledgements

Local screenshot framing uses Koubou (pinned to `0.20.0`) for deterministic device-frame rendering.
GitHub: https://github.com/bitomule/koubou

Simulator UI automation for screenshot capture and interactions uses AXe CLI.
GitHub: https://github.com/cameroncooke/AXe

The keyword difficulty methodology used by `asc optimize keywords score` is adapted from semihcihan's App Store Optimization CLI (MIT licensed).
GitHub: https://github.com/semihcihan/App-Store-Optimization-CLI

## Contributing

For portable Linux iOS builds and local IPA packaging with xtool and rcodesign,
see [the Linux iOS workflow](docs/linux-ios.md). Native simulator execution still
requires a Mac host; existing Xcode archive/export helpers retain their contracts.

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

## License

MIT License - see [LICENSE](LICENSE) for details.

---

<p align="center">
  <sub>This project is an independent, unofficial tool and is not affiliated with, endorsed by, or sponsored by Apple Inc. App Store Connect, TestFlight, Xcode Cloud, and Apple are trademarks of Apple Inc., registered in the U.S. and other countries.</sub>
</p>

---

### Overview (`docs/openapi/README.md`)

# OpenAPI snapshot (offline)

This folder keeps an offline snapshot of the App Store Connect OpenAPI spec for
agents that cannot access the internet.

## Files

- `latest.json`: full OpenAPI spec snapshot (see source below)
- `paths.txt`: generated path+method index for quick existence checks

## Source

Preferred sources for the OpenAPI spec:

- Official Apple download (zip): `https://developer.apple.com/sample-code/app-store-connect/app-store-connect-openapi-specification.zip`
- Community mirror that tracks Apple's published spec: `https://github.com/EvanBacon/App-Store-Connect-OpenAPI-Spec`

Note: The published OpenAPI spec can lag reality and may omit some operations that
still work in the API (parity checks can surface these gaps).

## Update process

1. Replace `latest.json` with a newer spec file.
2. Run `make update-openapi` to regenerate `paths.txt`.
3. Run `make update-schema-index` to regenerate the runtime `asc schema` index.
4. Run `make check-docs` to verify both generated indexes are current.
5. Update the "Last synced" date below and commit the snapshot and indexes together.

Last synced: 2026-10-07

Snapshot version: `4.5.1`, from the official Apple archive listed above.

- Archive SHA-256: `7b826ae9ca3e45b0e7df2643cbb811c9930632640944db11a8af4586fab9db46`
- Extracted `openapi.oas.json` SHA-256: `7518d3a94a8bd701ac25c1c601b95b8f53aad92091affb15a2a9d331713dac1a`

The snapshot preserves the extracted JSON bytes. Both indexes are generated
from that snapshot using the update process above.

---

### Overview (`internal/cli/devices/testdata/README.md`)

device-callback.p7 is a synthetic, encapsulated CMS SignedData callback containing a plist with UDID, PRODUCT, and VERSION, signed with a self-signed RSA test certificate using OpenSSL cms -sign -binary -nodetach -outform DER. It models the signed profile-service response described by Apple. It is not a captured device response and proves neither Apple certificate trust nor live iOS acceptance. The private key was discarded.

---

### API CANARY LOG (`docs/API_CANARY_LOG.md`)

# API Canary & Capture Log

Dated live-canary transcripts, authenticated browser/source captures, disposable test fixture records, and historical verification notes extracted from [API_NOTES.md](API_NOTES.md). For active operational contracts and endpoint rules, see [API_NOTES.md](API_NOTES.md).

## Web App Privacy Data Usage Updates

- Live canary on 2026-09-04 using a disposable app and a redacted web session verified the private `PATCH /iris/v1/appDataUsages/{usageID}` contract for a same-category, same-purpose `DATA_LINKED_TO_YOU` to `DATA_NOT_LINKED_TO_YOU` identity flip. The request uses the existing `appDataUsages` JSON:API resource and its `dataProtection` relationship; Apple returned `200`, and a fresh GET showed the same usage ID with the new protection.
- A second live canary on 2026-09-05 using the same disposable app verified the reverse `DATA_NOT_LINKED_TO_YOU` to `DATA_LINKED_TO_YOU` transition. The temporary `EMAIL_ADDRESS`/`APP_FUNCTIONALITY` tuple returned `200`, and a fresh GET retained the same usage ID with the new protection. The exact pre-canary `DATA_NOT_COLLECTED` baseline was restored and matched on a fresh GET.
- The canary seeded the tuple with `POST` (`201`), confirmed it by GET, then restored the semantic baseline by deleting it (`204`) and creating the `DATA_NOT_COLLECTED` declaration (`201`). Direct restore attempts via `POST` or `PATCH` to `DATA_NOT_COLLECTED` returned `409 STATE_ERROR`.
- The direct mutations advanced the remote `lastPublished` value while `published` remained `true`, despite no publish endpoint being called.

## Analytics & Sales Reports

- Although Apple's current Sales Reports endpoint table lists `SUBSCRIPTION` `1_3`, live verification in PR #1842 proved `1_4` succeeds and is required by some accounts, so both are accepted and `1_4` remains the default.

## Tax Categories and Transaction Tax Reports

- Verified against the App Store Connect OpenAPI snapshot in `docs/openapi/` and the App Store Connect web-client source captured for issue #2299.
- For App Information tax categories (`asc web apps tax-category`), the request and response shapes are source-backed. A disposable-app canary verified explicit application tax configuration and readback. PATCH, condition changes, and account-specific errors remain unverified; selecting the legally correct classification remains the operator's responsibility.
- For In-App Purchase tax categories (`asc web iap tax-category`), a browser canary on 2026-09-06 against disposable app `6759231657` and IAP `6760268101` verified ADDON catalog discovery, create, update, explicit condition clearing, and delete/readback; the disposable IAP was restored and no resources were left behind. This is browser evidence only: final CLI live execution remains unverified.

## Sandbox Testers

- Historical public v1 create required password confirmation, a secret question/answer, and a birth date; that removed v1 contract does not establish that those fields are accepted by the current private web flow.
- The current App Store Connect web bundle's New Tester form exposes first name, last name, email, password, confirm password, and country. Its two `POST /sandbox/v2/account/validateFields` requests project the first three fields, then add `acAccountPassword`; `POST /sandbox/v2/account/create` sends exactly `firstName`, `lastName`, `acAccountName`, `acAccountPassword`, and `storeFront`. `confirmPassword` is UI-only, and the bundle contains no `secretQuestion`, `secretAnswer`, or `birthDate` fields. This is a source-backed request shape; it does not prove a successful live create or acceptance of extra portal fields. See issue #2294.
- The same bundle lists testers with `GET /sandbox/v2/provider/account/list?limit=50`; the only live list response captured was the empty `{"totalAccounts":0,"totalInactiveAccounts":0,"accounts":[]}` response. No continuation or pagination contract was captured. Account rows include `id` and `isInFamily`; the UI excludes family members from deletion.
- Delete status/body, post-delete disappearance, and account-wide mutation acceptance for `POST /sandbox/v2/account/delete` remain unverified because no sandbox tester rows were available during capture; do not infer them from the legacy public v1 surface.

## App Store Regulations & Permits declarations

- A sanitized live CLI canary on 2026-09-05 used disposable app `6759231657` and a locally built candidate CLI. The `overall-yes`, `regional-gbr-false`, and `overall-no` CLI steps all exited 0 with exact readbacks; the regional `GBR` No path was exercised while the overall declaration was Yes. One exact raw-form restore PUT returned 200 and its readback matched the saved form. The final baseline, session identity, temporary-file cleanup, and no-uncertain-retry checks all passed. This verifies the current CLI's negative regional path and restoration on that disposable app; it does not prove arbitrary affirmative legal/contact values.
- The captured Business frontend identifies `personalServiceDeclaration`, `personalServiceApps`, and `nonPersonalServiceApps` in dynamic DAC7/MRDP/ITA/SERR compliance forms. The current account has no qualifying live requirement/form fixture to verify the applicable form identity, constraints, and write postcondition.

## Web-session API keys

- The captured App Store Connect individual-keys frontend bundle confirms the actor-filtered v2 list (`GET /iris/v2/apiKeys?include=createdByActor,revokedByActor&filter[createdByActor]=USER:{actorID}`), the empty `POST /iris/v2/apiKeys` create request, browser P-256 key generation with PKCS#8 private and SPKI public export, public-key registration by v2 `PATCH`, and v2 inactive-state revocation. Before the detail/generation flow it checks `GET /iris/v1/apiAccesses` for an `APPROVED` access resource and obtains the actor ID from initialized page state. This capture contains no `/iris/v1/users/` or `AuthSession.UserEmail` request.
- No individual key was created or revoked during capture or validation, so provider acceptance of a newly created individual key and the individual revoke path remain unverified.
- A sanitized 2026-09-05 live canary created and revoked one newly created temporary team key through an authenticated web session. The pre-existing keys were unchanged, the new key was verified inactive, and temporary files were removed. This verifies the team request path for that account and session.

## App price schedules (`POST /v1/appPriceSchedules`)

- Live runs against disposable app `6759231657` on 2026-09-26 and 2026-09-27 (each one snapshot, attempts, restore, and verification in a single serialized session) returned HTTP 409 for past start dates, single future start dates, non-base-territory manual prices, and unknown price point IDs. The raw bodies are checked in under `internal/cli/cmdtest/testdata/pricing_schedule_create_409/`, byte for byte including Apple's per-response error `id`.
- At 23:22 UTC on 2026-09-26, `startDate` `2026-09-26` returned 201. At 00:03 UTC on 2026-09-27, `startDate` `2026-09-27` (the UTC date, and the CLI's default at the time) returned the future-start 409 (`Entire timeline must be covered for USA. The first interval has a start date 2026-10-26T00:00 in the future`), while `2026-09-26` (the US Pacific date) returned 201. Omitting `startDate` also returned 201 and restored the original `startDate: null` / `endDate: null` interval.
- Apple's base64 price resource IDs encode interval boundaries as Unix seconds (`sd`, `ed`); a captured in-app purchase schedule encodes `ed` of one interval and `sd` of the next as the same value, `1771228800` (`2026-02-16 00:00 PST`, `08:00 UTC`).
- No subscription-specific live capture exists for `subscriptionPrices` start-date timezone boundaries; the CLI applies the same US Pacific day as app and in-app purchase schedules.

## Developer Portal iCloud containers

- fastlane Spaceship still uses the older form endpoint `POST /services-account/QH65B2/account/cloudContainer/addCloudContainer.action` with `name`, `identifier`, and `teamId` ([portal_client.rb#L483-L492](https://github.com/fastlane/fastlane/blob/5faa062de92fd21e5674efcfde1a8359de7dd861/spaceship/lib/spaceship/portal/portal_client.rb#L483-L492)) and sends the full `iCloud.`-prefixed identifier unchanged ([cloud_container.rb#L59-L62](https://github.com/fastlane/fastlane/blob/5faa062de92fd21e5674efcfde1a8359de7dd861/spaceship/lib/spaceship/portal/cloud_container.rb#L59-L62), [commands_generator.rb#L263-L270](https://github.com/fastlane/fastlane/blob/5faa062de92fd21e5674efcfde1a8359de7dd861/produce/lib/produce/commands_generator.rb#L263-L270)). The captured list read shows the current portal reads iCloud containers from `/services-account/v1/cloudContainers`, not fastlane's `listCloudContainers.action`.
- Live capture (2026-09-29, CLI built from `414167918`, one maintainer-approved permanent container): the sequence was `listTeams.action` 200, the visible and hidden preflight reads 200, `POST https://developer.apple.com/services-account/v1/cloudContainers` **201**, then the visible and hidden read-backs 200. The receipt was `containerId` `7PU5FAD3YR`, identifier `iCloud.com.rorkai.asc.capture.t1790620906`, name `asc capture container`, prefix `YQZQG7N4WG` (the team prefix), and `hidden:false`. A separate `asc web icloud-containers list` showed the same ID, identifier, and name. This container is permanent and cannot be removed. It stays in the team as the capture artifact; do not create another one to re-verify.
- The create response body was not recorded. `internal/web/testdata/icloud_container_create_capture.json` holds the captured request body and status, plus the read-back resource. Its `canEdit`, `canDelete`, and `responseId` values are inferred. The replay test accepts both a JSON:API resource body and an empty 201 body, because the read-back is the authority either way.

## Web-session Resolution Center

- No disposable live thread/draft fixture was available during implementation of `asc web review reply` and `asc web review drafts create|update|delete`, so Apple provider acceptance of these writes remains unverified.

## Web-session app create conflicts (`POST /iris/v1/apps`)

- Captured live on 2026-09-29 against disposable app `6759231657`: re-running `asc web apps create --name "ASC Test 20260216074703" --bundle-id com.rudrank.asc.throwaway20260216074703 --sku asc-test-20260216074703 --auto-rename=false` returned HTTP 409 with three `errors[]` entries whose codes are, in order, `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`, `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`, and `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE.SAME_ACCOUNT`. The CLI exited 5 with `web apps create failed: web api error (status 409) ... codes=[...]`.
- Only the status and the codes were captured. The `detail`, `title`, and `source` of each entry were not printed, so which attribute (name, SKU, or bundle ID) each entry refers to is unknown. The captured-shape test fixture carries codes only. A second fixture adds an app-name `detail` and `source` pointers to exercise the `--auto-rename` interaction; those fields are **synthetic** (inferred, not captured) and are labeled as such in `internal/cli/web/web_apps_create_if_exists_test.go`.

## Web-session app distribution method

- The current Apple frontend's pricing action supplies an inner `apps` resource to its request action. The shared Iris transport serializer (`module 60285`, helper `Oe`) converts that resource to `JSON.stringify({data: resource})` before `fetch` (`it`), so the wire request is JSON:API.
- `asc web apps distribution set` writes are based on the captured frontend constructor and local HTTP tests; no live Apple mutation or live eligibility/success proof was performed.

## Web-session custom-distribution Apple Account recipients

- A redacted authenticated browser canary on 2026-09-06 exercised the workflow in CUSTOM mode, including `POST /iris/v1/customAppUsers` with HTTP 201, an app-scoped list with HTTP 200, and `DELETE /iris/v1/customAppUsers/{id}` with HTTP 204. The disposable app was restored to its original public distribution and education settings; fresh app, user, and organization reads showed the baseline and zero remaining rows. This is browser contract evidence, not final CLI live verification, and no organization write was performed.
- The live canary does not establish role behavior, duplicate rejection, organization recipient rows, or a second collection page.

## App transfer status (web session)

- On 2026-09-06, an authenticated read of disposable app `6759231657` (`GET /iris/v1/apps/{appId}?include=appTransferRequest`) returned HTTP 200, the expected `apps` resource ID, `relationships.appTransferRequest.data: null`, and an empty `included` array. The related URL `/iris/v1/apps/{appId}/appTransferRequest` returned HTTP 409 with `ENTITY_ERROR.RELATIONSHIP.INVALID` for that fixture. No pending-transfer response was observed live.

## Last-compatible version settings (`downloadable`)

- A web-session read (`asc web apps last-compatible-version view`) briefly existed for this screen and was retired before it reached a release. It mirrored App Store Connect's own iris request (`GET /iris/v1/apps/{id}?include=appStoreVersions&fields[appStoreVersions]=...,downloadable,...&limit[appStoreVersions]=2000`), which required a web session and offered no write. The public API path supersedes it in both directions.

## App Store version creation conflicts (`POST /v1/appStoreVersions`)

- Captured live on 2026-09-15: Apple answers HTTP 409 with `ENTITY_ERROR.RELATIONSHIP.INVALID` ("You cannot create a new version of the App in the current state.") both when the platform already has an unreleased version and, followed by `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`, when the version string is already used.

## Web-session review submissions (iris)

- Verified live on 2026-09-03: `GET /iris/v1/reviewSubmissions/{id}/items` rejects `include=appStoreVersionExperimentV2` with HTTP 400 `PARAMETER_ERROR.INVALID` even though the public OpenAPI snapshot lists that relationship.

## TestFlight text validation (What to Test)

- A live probe on 2026-09-25 against `betaAppLocalizations` `description` on a disposable app (the app had no builds, so `betaBuildLocalizations` `whatsNew` could not be written directly) rejected the write with `An attribute value has invalid text.: Text contains invalid characters/formats.` for: any `<` (including `a<b` and `x < y`); decomposed `Cafe` + U+0301 that NFC would compose; U+0301 after `q` or at the start of the text; U+1AB0; U+20D7; and every tested emoji (`❤`, `❤️`, U+FE0E, keycap, ZWJ family, skin tone, `😀`).
- The same probe accepted `>`, `&`, the literal text `&lt;`, newlines, precomposed Vietnamese, and script-specific combining marks: Devanagari (`हिंदी में परीक्षण करें`, including a leading U+0902), Thai tone marks, Hebrew niqqud, Arabic harakat, and the Cyrillic titlo U+0483.
- A live probe on 2026-09-26 against `betaBuildLocalizations` `whatsNew` (PATCH on an existing localization, disposable app) answered 409 `An attribute value has invalid text.: Text for whatsNew contains invalid characters:'[…]'` and listed each offending character.
  - Rejected: every supplementary-plane character (U+1F600, ZWJ family, skin tone U+1F3FD, regional indicators U+1F1FA/U+1F1F8, U+1F004, U+1F130, U+1D400, U+1D11E, U+10330, U+20000); Variation Selectors U+FE00, U+FE0E, U+FE0F (even after a plain `a`); enclosing marks U+20DD and keycap U+20E3; Control Pictures U+2400 and OCR U+2440; Box Drawing U+2500/U+257F and Block Elements U+2588/U+2591; Miscellaneous Symbols U+2600–U+26FF (`☀☂☺♠♥⚠★☆`, U+26FF); Dingbats U+2701–U+27BF (`✓✔✖✗✂✨✅❤`, U+2794, U+27BF); Miscellaneous Mathematical Symbols-A U+27C2 and -B U+2980; Braille U+2801; Supplemental Mathematical Operators U+2A00; Miscellaneous Symbols and Arrows U+2B00–U+2BFF (`⬆⭐⭕⬛`); Private Use U+E000 and U+F8FF; U+FFFD.
  - Accepted: Arrows (U+2192, U+2194, U+21FF), Supplemental Arrows-A U+27F6 and -B U+2934, Letterlike (`™`, `ℹ`, `№`), `©`, `®`, Miscellaneous Technical (U+2300, `⌘⌚⌛`, U+23E9, U+23F0, U+23FF), Enclosed Alphanumerics (U+2460, `Ⓜ`, U+24FF), Geometric Shapes (U+25A0, U+25AA, `▶`, U+25CF, U+25FB, U+25FF), Mathematical Operators, General Punctuation (`•…—‼⁉`), `€`, U+3030, U+303D, U+3297, U+3299, newlines, CRLF, tabs, NBSP, soft hyphen, ZWSP, ZWJ/ZWNJ inside Devanagari and Persian, LRM/RLO, U+FEFF, and Latin, Greek, Cyrillic, IPA, CJK, Hangul, Thai, Hebrew, Arabic, Devanagari, and Tamil text.
  - Whole scripts and presentation forms rejected in the same sample (left to App Store Connect rather than refused locally because neighboring characters such as IPA and U+F900 were accepted): Ethiopic, Cherokee, Khmer, Georgian, Tibetan, Mongolian, Yi, Vai, Glagolitic, U+02C6 (Spacing Modifier Letters), Alphabetic Presentation Forms U+FB01, Arabic Presentation Forms U+FDFD, Vertical Forms U+FE10, Small Form Variants U+FE50, and CJK Compatibility U+3300.
- A live check on 2026-09-25 showed App Store Connect matches `filter[locale]` on `betaAppLocalizations` case-insensitively (`fr-fr` and `FR-FR` both returned `fr-FR`).

## App Store review details (`appStoreReviewDetails`)

- Field length, contact requirement, and phone validation rules were derived from App Store Connect responses observed in agent runs between 2026-09-21 and 2026-09-30:
  - `notes`: Rejected over 4000 characters with `An attribute value is too long.: Review Notes cannot be longer than 4000 characters.` (agents trimmed and retried up to 7 times, often measuring bytes with `wc -c`).
  - Contact fields on create: `POST /v1/appStoreReviewDetails` sending only `contactEmail` failed with `You must provide a value for the attribute 'contactFirstName' with this request`; sending first name, last name, and email failed on `'contactPhone'`. An update sending only `contactEmail` to a detail without a first name also failed on `contactFirstName`.
  - `contactPhone`: Rejected 7 digits after `+503`, and 7- and 11-digit national numbers after `+1`, with `The phone number must be in a valid format. Preface the phone number with '+' followed by the country code (for example, +44 844 209 0611)` (`betaAppReviewDetails` answered `The format of the phone number is invalid`). Numbers without a plus sign (10- and 11-digit North American numbers) were accepted by the API.

## Authentication & Rate Limiting

- The public storefront retry path is validated with deterministic `httptest` coverage for status boundaries, Retry-After parsing/capping, response-body draining, request replay, concurrency, and cancellation; it does not perform live mutations.
- The bulk-mutating concurrency ceiling of 16 (`asc.WithBulkMutatingRequestLimit`) is covered by request-gating tests with a fake transport; its behavior against App Store Connect rate limits has not been measured live yet (#2786).

## Builds

- Read-only checks on 2026-09-30 found 62–74 iPad `deviceModel` rows in `GET /v1/buildBundles/{id}/buildBundleFileSizes` on every one of five builds of a live iPhone-only app, matching universal apps.
- fastlane encountered the same intermittent `GET /v1/apps/{id}/buildUploads` 404 flake (`There is no resource of type 'apps' with id '…'`) after moving build-number lookups to that endpoint (https://github.com/fastlane/fastlane/issues/29908).

## Xcode Cloud workflows

- On 2026-09-06, an authenticated browser session returned HTTP 200 for `GET /ci/api/teams/{teamID}/scm-providers-v2` (six providers) and `GET /ci/api/teams/{teamID}/scm-providers/{providerID}/connection-v2` (one `{"status":"success"}` response) with zero mutations. This establishes the provider contract, not final CLI live execution.
- Custom Xcode Cloud version aliases (`version-aliases-v3`) request and response details are source-captured from the current frontend; no alias mutation was performed against Apple's service during implementation.
- A live read on 2026-09-04 confirmed that private `workflows-v15` payloads expose TestFlight post-actions under `post_actions[].deployment_config.testflight_deployment_ids`. Two distinct `beta_group_ids` observed across two post-actions matched the same app's authenticated IRIS `betaGroups` IDs and the public command output from `asc testflight groups list --app APP_ID --paginate --output json` (three groups returned). The scan covered 14 products/workflows; all observed `beta_tester_ids` arrays were empty, so tester-ID equivalence remains unverified.

## Developer Portal session (web session)

- The public schema supports `fields[bundleIds]=bundleIdCapabilities` and `limit[bundleIdCapabilities]=50`; their acceptance by the private `/services-account/v1` proxy remains live-unverified.

## Developer Portal Bundle ID reads (web session)

- Portal-only attributes observed in the captured `POST /services-account/v1/bundleIds` (`X-HTTP-Method-Override: GET`) collection include `identifier`, `dateModified`, `entitlementGroupName`, `bundleType`, `platform`, `wildcard`, `dateCreated`, `bundleIdCapabilitiesSettingOption`, `seedId`, `name`, `platformName`, `deploymentDataNotice`, and `responseId`.

## Developer Portal Services IDs (web session)

- A 2026-09-26 browser capture accepted `PATCH /services-account/v1/bundleIds/{id}` for `asc web service-ids domains set` with an ID-less `bundleIdCapabilities` child linked to capability `APPLE_ID_AUTH` and its existing `appConsentBundleId`. The captured detail response reported `paging.total=0` with `limit=2147483647` despite populated capability linkage.
- Live CLI verification on 2026-09-27 confirmed a domain update, unchanged repeat, and two-domain/two-return-URL replacement with the existing primary App ID preserved. The disposable Services ID and parent App ID were deleted afterward.
- A 2026-09-28 live capture on the disposable team (`asc web service-ids create --identifier com.rorkai.asc.capture.services.t1790609684 --name "asc capture temp" --confirm`, then `asc web service-ids view --service-id ID --output json`, then `asc web service-ids delete --service-id ID --confirm`; a follow-up view returned 404 and the list had no match) showed that a Services ID with zero capabilities returns `{"meta":{"paging":{"total":0,"limit":2147483647}},"data":[],"links":{"self":...,"related":...}}` with no `next` link. The sanitized relationship is checked in as `internal/web/testdata/service_id_empty_capabilities_relationship.json`, with the captured resource ID in its links replaced by `service-1`.

## Developer Portal Website Push IDs (web session)

- The live `listWebsitePushIds.action` read returned HTTP 200 with `resultCode=0`, `pageNumber=1`, `pageSize=1000`, and an empty root-level `websitePushIdList` in the captured account state.
- The captured frontend (`captures/apple-developer-main.js`) maps each legacy item to `id=item.websitePushId` for this resource (offset 849,700), while its generic identifier table reads `name` and `identifier` (offset 866,718); the topic picker also reads `websitePushId`/`id` and `identifier` (offsets 1,806,200–1,808,480).
- A final built-CLI canary using binary SHA-256 `fd940938b7477aa27b0f5c793618af0c5c1e30d050f85b6bacf1738378ce31f9` exercised the complete happy path for one uniquely generated owned disposable identifier: baseline `list`, `create`, `view`, `delete`, and final `list` all exited successfully. The create and delete receipts and detail readback matched the canary identity, the capability relationship and included graph were empty, the baseline list remained unchanged, and final absence plus temporary-evidence cleanup were verified. The CLI view exposed no certificate fields; `certificateAbsenceIndependentlyVerified` is false because this command slice has no certificate endpoint, so the canary does not claim independent certificate absence.

## Sparse Fieldsets Combined with Includes

- Observed on 2026-09-02 against a live App Store Connect team:
  - `GET /v1/profiles` with `fields[profiles]=name&include=devices` returned HTTP 200 and still put related devices in `included`, while omitting `relationships` on each profile unless `devices` was also in `fields[profiles]`. `fields[devices]=name,udid` sparse-filtered the included devices.
  - `GET /v1/certificates` with `fields[certificates]=displayName&include=passTypeId` returned HTTP 200. The team had no `PASS_TYPE_ID` certificates, so `included` was absent both with that sparse fieldset and with `include=passTypeId` alone. Non-pass certificates exposed `relationships.passTypeId.data=null` only when the relationship was in the fieldset (or when no certificate fieldset was sent).

## Developer Portal App Groups (web session)

- `deleteApplicationGroup.action` was inferred from sibling `listApplicationGroups.action` / `addApplicationGroup.action` actions and third-party portal client contracts; it was not live-verified when added because no Developer Portal web session was available.
- Both single-resource `PATCH /services-account/v1/bundleIds/{id}` capability updates and the pre-delete collection read (`POST /services-account/v1/bundleIds` with `X-HTTP-Method-Override: GET`, `fields[bundleIds]=bundleIdCapabilities`, and `limit[bundleIdCapabilities]=50`) remain unverified against a live private proxy.

## Offline signature verification: Apple leaf certificate types

- Built on 2026-09-28 from public certificates on a macOS 27 host:
  - Login keychain certificates (`security find-certificate -a -p`), `DeveloperCertificates` in local provisioning profiles (22 Apple Development, 11 Apple Distribution, 8 legacy iPhone Developer), leaves extracted with `codesign -d --extract-certificates` from signed apps and IPAs, and leaves read from `X509Certificate` elements of flat-package tables of contents (`xar --dump-toc`), including Xcode's bundled `CoreTypes`, `MobileDevice`, `MobileDeviceDevelopment`, and `XcodeSystemResources` pkgs.
  - Apple's device-management article "Allowing and denying apps and binaries" confirmed `6.1.9` (App Store), `6.1.13` (Developer ID), `6.1.4`, and `6.1.12` (development). Apple's WWDR and Developer ID CPS pages were not machine-readable at the time.

## Offline signature verification: xar member payload checksums

- Checked on 2026-09-29 against `pkgutil --check-signature` on macOS 27:
  - Flipping one payload byte of Xcode's `MobileDeviceDevelopment.pkg` made `pkgutil` report "package is invalid (checksum did not verify)", and `pkg-info` reported `invalid` naming `Payload`.
  - All 12 packages bundled in three installed Xcode releases and a notarized Developer ID Installer pkg verified as `valid`.
  - An older Developer ID Installer pkg whose certificate expired in 2024 recomputed all 13 of its file checksums and still reported `expired`.
  - `pkgutil --check-signature` still accepted the older Developer ID Installer pkg after one of its extended-attribute (`<ea>`) heap bytes was flipped, confirming that `<ea>` checksums should not be enforced.

---

### API NOTES (`docs/API_NOTES.md`)

# API Notes

Operational contracts, endpoint quirks, filter/include rules, and live-verification status for App Store Connect, Apple Ads, and Developer Portal surfaces.

Dated maintainer canary transcripts, authenticated browser/source captures, and disposable test fixture records live in the repository at [API_CANARY_LOG.md](API_CANARY_LOG.md) and are intentionally excluded from the embedded `asc docs show api-notes` guide to keep CLI output concise.

## Web App Privacy Data Usage Updates

- `PATCH /iris/v1/appDataUsages/{usageID}` supports only a same-category, same-purpose identity flip between `DATA_LINKED_TO_YOU` and `DATA_NOT_LINKED_TO_YOU` using the `appDataUsages` JSON:API resource and its `dataProtection` relationship (live-verified).
- Tracking, `DATA_NOT_COLLECTED`, and scope changes must use `DELETE` (`204`) + `POST` (`201`); direct `POST` or `PATCH` to `DATA_NOT_COLLECTED` fails with `409 STATE_ERROR`.
- Direct mutations advance `lastPublished` while `published` remains `true` without calling a publish endpoint. Treat `asc web privacy apply` as a published-state mutation.

## Apple Ads Profile Context Isolation

- Named Apple Ads profiles use only the context stored on that profile and never inherit `ads.org_id` or `ads.ad_account_id` from root config or another profile. Profile-less access-token and environment authentication can still use matching root context.

## Public App Store Ranking

- `asc apps public rank` is an unauthenticated storefront read: iOS inspects up to 200 results from the public iTunes `/search` endpoint, while Apple TV uses the undocumented MZStore search endpoint with `X-Apple-Store-Front: <numeric-storefront-id>,33` (available only for countries with a known numeric storefront ID).
- `found: false` means only that the app was absent from the returned result window.

## Analytics & Sales Reports

- Sales Reports require `YYYY-MM` for monthly reports and `YYYY` for yearly reports on the wire; the CLI accepts `YYYY-MM-DD` and reduces it to the live period identifier before sending.
- Vendor number comes from Sales and Trends → Reports URL (`vendorNumber=...`).
- Sales Reports validates the report type/subtype/frequency/version tuple against Apple's endpoint table. Both `SUBSCRIPTION` `1_3` and `1_4` are accepted (`1_4` is the default and live-verified).
- Use `--paginate` with `asc analytics view --processing-date` (`filter[processingDate]`) to search every report page, or `--next <links.next> --paginate` to resume. Use `--granularity "DAILY,WEEKLY,MONTHLY"` to filter by granularity. Raise `ASC_TIMEOUT` for long runs.

## Finance Reports

Finance reports use Apple fiscal months (`YYYY-MM`), not calendar months.

| API `--report-type` | UI Option | `--region` Code(s) |
|---|---|---|
| `FINANCIAL` | All Countries or Regions (Single File) | `ZZ` (consolidated) |
| `FINANCIAL` | All Countries or Regions (Multiple Files) | `US`, `EU`, `JP`, etc. |
| `FINANCE_DETAIL` | All Countries or Regions (Detailed) | `Z1` (required) |
| Not available | Transaction Tax (Single File) | N/A |

- `FINANCE_DETAIL` requires region code `Z1`.
- Transaction Tax reports are not exposed in `/v1/financeReports` or `/v1/salesReports`; use `asc web finance transaction-tax download` or App Store Connect.
- Region codes: `asc finance regions` or https://developer.apple.com/help/app-store-connect/reference/financial-report-regions-and-currencies/.

## Tax Categories and Transaction Tax Reports

- The public API has no tax-category resource or attribute on `apps`, `appInfos`, or `inAppPurchases`. `asc capabilities --area monetization` reports App Information and IAP tax categories as web-session coverage.
- **App Information (`asc web apps tax-category list|view|set`)**:
  - Catalog read: `GET /iris/v1/taxCategories?filter[productType]=APPLICATION&include=subcategories,conditions&limit[subcategories]=100&limit[conditions]=100`.
  - App read: `GET /iris/v1/appTaxCategories/{appId}?include=category,enabledConditions&limit[enabledConditions]=100`. A missing resource means unconfigured (UI default: App Store Software).
  - `set` requires `--confirm`, validates category and condition IDs against the catalog, sends the complete condition set (`enabledConditions.data=[]` when `--condition` is omitted), and verifies via read-back without retrying ambiguous writes. Explicit creation/readback is live-verified; PATCH, condition changes, and account-specific errors remain unverified.
- **In-App Purchases (`asc web iap tax-category list|view|set|reset`)**:
  - Catalog read uses `filter[productType]=ADDON` and preserves the raw JSON:API envelope.
  - `view` reads `GET /iris/v2/inAppPurchases/{iapId}?include=inAppPurchaseTaxCategoryInfo`. Explicit `inAppPurchaseTaxCategoryInfo.data: null` means inherited from the parent app (raw discovery envelope preserved). When configured, the CLI reads `GET /iris/v1/inAppPurchaseTaxCategoryInfos/{taxInfoId}?include=category,enabledConditions,inAppPurchaseV2&limit[enabledConditions]=100` and verifies the owner IAP ID.
  - `set` (`--confirm`) validates against the `ADDON` catalog, creates when inherited or patches when configured, sends at most one write plus one verification read, and skips matching state as a no-op.
  - `reset` (`--confirm`) deletes only the `inAppPurchaseTaxCategoryInfos` override and verifies an explicit-null relationship (no-op when already inherited). Browser contract is verified; final CLI live execution remains unverified.

## Sandbox Testers

- List, view, update, and clear-history use the public v2 API through `asc sandbox`.
- `asc web sandbox create` requires `--first-name`, `--last-name`, `--email`, `--password` (8+ chars with uppercase, lowercase, and digit), and `--territory`. Territory inputs accept alpha-2, alpha-3, or exact English country names and normalize to 3-letter ASC territory codes (limited to verified ASC alpha-3 surfaces).
- Web create runs two `POST /sandbox/v2/account/validateFields` requests followed by `POST /sandbox/v2/account/create` with `firstName`, `lastName`, `acAccountName`, `acAccountPassword`, and `storeFront` (source-backed; live create acceptance is unverified).
- Web list uses `GET /sandbox/v2/provider/account/list?limit=50` (no pagination contract captured; rows include `id` and `isInFamily`).
- `asc web sandbox delete` (`--confirm`) sends `POST /sandbox/v2/account/delete` with `{"ids":[...]}`, refuses family members and incomplete list snapshots, sends one request, and verifies a fresh list. Delete status/body and post-delete disappearance remain unverified against live data.

## App Store Regulations & Permits declarations

- The public OpenAPI snapshot has no medical-device, personal-service, trader, or DSA declaration endpoints (`TerritoryAvailability.contentStatuses` exposes only read-only trader status consequence codes).
- Declarations use the web-session `ppm/complianceform/v1` service with UI headers (`X-Csrf-Itc: itc`, `Origin`, and `/apps/{id}/distribution/info` `Referer`):
  - `asc web apps declarations list`: `GET /ppm/complianceform/v1/accounts/{accountId}/requirements?contentId={appId}`. Prefers `requirementData` matching `contentId`, falling back to empty `contentId`.
  - `asc web apps medical-device view`: `GET /ppm/complianceform/v1/accounts/{accountId}/requirements/{requirementId}/forms?contentId={appId}`, reading `medicalDeviceData.declaration` (`no`, `yes`, or absent) alongside `$[*]`-rooted `constraints`.
  - `asc web apps medical-device set`: uses `POST .../contents/{appId}/requirements/{requirementId}/forms` when `medicalDeviceData.declaration` is absent and `PUT` when present. `--declared false` derives regions from form constraints, preserves existing regional rows, and skips writing when already `no` + `COLLECTED`. `--declared true [--countries-or-regions EEA,GBR,USA] --confirm` limits regions to `EEA`, `GBR`, `USA` (defaulting to all three), updates `registrationInfo` rows, and verifies readback.
  - `asc web apps medical-device region set --app APP_ID --region REGION --input PATH --confirm`: sends `PUT .../forms` with rootfs-anchored `declaration`, localized `supportInfo`, and `registrationNumber` (for `USA`/`EEA`). Requires app-level declaration `yes` and region availability in `$[*].countriesOrRegions`. Performs contact preflight (legal entity, region coverage, non-empty phone/email/address, no missing constraint legal-entity candidates), sends one PUT, and verifies readback. Negative regional and restore paths are live-verified; arbitrary affirmative legal/contact values remain provider-dependent.
- Personal-service declarations (`personalServiceDeclaration`) are not implemented (no live fixture). EU DSA trader status (`GET /ppm/v1/accounts/{id}/sellerInfo`, `POST /ppm/v1/legalEntities/{id}/sellerInfo`) requires interactive `id.apple.com` identity verification and is out of scope for unattended CLI writes.

## Web-session API keys

- `asc web api-keys list` combines iris v1 team keys (`GET /iris/v1/apiKeys?include=createdBy,revokedBy,provider&sort=-isActive,-revokingDate&limit=2000`) and iris v2 individual keys (`GET /iris/v2/apiKeys?include=visibleApps,createdByActor,revokedByActor&limit[visibleApps]=3&limit=2000`), following `links.next` internally (no `--paginate`). Use `asc web auth capabilities --key-id` to resolve actor-backed roles when an individual key's `roles` array is empty.
- `asc web api-keys view --key-id` reads `GET /iris/v1/apiKeys/{id}?include=provider` (team keys only). Creation dates are absent from the payload and private key material is never printed.
- `asc web api-keys create-individual --user-id USER_UUID --output-dir DIR --confirm` checks `GET /iris/v1/apiAccesses` (`APPROVED`) plus fail-closed `GET /iris/v1/users/{userUUID}` and session-email preflight, stages a local `0600` PKCS#8 P-256 private key under `--output-dir`, sends `POST /iris/v2/apiKeys` (`{data:{type:"apiKeys"}}`), resolves the single new active key from the actor-filtered v2 list (`filter[createdByActor]=USER:{actorID}`), atomically renames the key to `ApiKey_<KEY_ID>.p8` without replacing existing files, registers the SPKI public key via `PATCH /iris/v2/apiKeys/{KEY_ID}`, and verifies active readback. Uncertain steps retain the local artifact and never retry automatically. Source-backed; live individual key creation remains unverified.
- `asc web api-keys revoke --key-id KEY_ID --type team|individual --confirm` sends `PATCH /iris/{v1|v2}/apiKeys/{id}` with `{"data":{"id":"<id>","type":"apiKeys","attributes":{"isActive":false}}}` after pre-listing the requested family, and verifies inactive post-state without retrying 5xx/transport failures. Team create/revoke is live-verified; individual revoke remains unverified.

## Web app availability (iris)

- `GET /iris/v1/apps/{id}/appAvailabilityV2` returns `availableInNewTerritories` and links-only `relationships.territoryAvailabilities`; `?include=availableTerritories&limit[availableTerritories]=200` fails with 400 `PARAMETER_ERROR.INVALID`. When unconfigured, it can return `data: null` (treated as absent by `asc web apps availability create`).
- Territory availability reads use `GET /iris/v2/appAvailabilities/{id}/territoryAvailabilities?include=territory&limit=200` (follow `links.next`; `filter[available]=true` returns 400 `PARAMETER_ERROR.ILLEGAL`, so filter client-side). `asc web apps delete` uses this collection for its removed-from-sale preflight.
- On the public API, `GET /v1/apps/{id}/appAvailabilityV2` and `GET /v2/inAppPurchases/{id}/inAppPurchaseAvailability` return 404 `NOT_FOUND` naming `type 'appAvailabilities'` or `'inAppPurchaseAvailabilities'` when unconfigured (vs `'apps'` / `'inAppPurchases'` when the parent is missing); `asc pricing availability view` treats the unconfigured shape as an expected negative (exit 1, `state_not_ready`).
- When an app has never had a price schedule, `GET /v1/apps/{id}/appPriceSchedule` returns a synthetic schedule resource, and `GET /v1/appPriceSchedules/{id}/manualPrices` returns 404 `There is no resource of type 'null' with id '{appId}'`. `asc pricing current` treats that (and a direct schedule 404 or missing current base price) as an expected negative (`configured: false` or `reason: no_current_base_price`).
- `asc web removed-apps restore` patches `/iris/v1/apps/{id}` with `removed:false`, verifies restoration, and only then POSTs `/iris/v1/userAppPermissions` (`GRANT` or `REVOKE` for `ALL_SILOABLE_USERS`).

## App price schedules (`POST /v1/appPriceSchedules`)

- `POST /v1/appPriceSchedules` replaces the whole schedule. Known HTTP 409 conflict shapes (fixtures in `internal/cli/cmdtest/testdata/pricing_schedule_create_409/`):
  - Past `startDate`: `ENTITY_ERROR.INVALID_START_DATE` (`Interval can not have a start date in the past`).
  - Single future `startDate` without covering today: `ENTITY_ERROR.INVALID_START_DATE` (`Entire timeline must be covered for USA...`).
  - Manual price territory mismatching `baseTerritory`: `ENTITY_ERROR.BASE_TERRITORY_INTERVAL_REQUIRED`.
  - Unknown or cross-app price point ID: `ENTITY_ERROR.NOT_FOUND`.
- App Store Connect evaluates "today" and schedule interval boundaries at **US Pacific midnight** (`America/Los_Angeles`) with exclusive end dates, and stores an accepted today `startDate` as `null`. `asc pricing`, `asc app-setup pricing`, `asc iap pricing`, `asc subscriptions pricing`, and `asc validate` all evaluate current/resolved prices and default start dates using the US Pacific date.

## Developer Portal iCloud containers

- `asc web icloud-containers list` sends `POST /services-account/v1/cloudContainers?filter[AND][hidden]=false` (or `true` with `--hidden`) with `X-HTTP-Method-Override: GET`, `teamId`, and `urlEncodedQueryParams=limit=1000&offset=0&sort=name` (bounded first page, no `--paginate`; warns on stderr if continuation/total indicates truncation).
- `asc web icloud-containers create --identifier iCloud.ID --name NAME --confirm` requires the exact `iCloud.` prefix, preflights both visible and hidden collections (failing closed on truncation or existing matches), sends `POST /services-account/v1/cloudContainers` with `{"data":{"type":"cloudContainers","attributes":{"identifier":...,"name":...,"teamId":...}}}` (no root `teamId`), and verifies the new container across both collections without ever retrying (iCloud containers cannot be deleted; live-verified).

## Web-session Resolution Center

- Resolution Center has no public OpenAPI surface; all calls use `/iris/v1` with Apple ID web-session auth and follow `links.next` internally (no `--paginate`).
- Submission scope (`GET /iris/v1/resolutionCenterThreads?filter[reviewSubmission]={id}&include=reviewSubmission`, used by `asc web review show`) and app scope (`GET /iris/v1/apps/{appId}/resolutionCenterThreads?include=appStoreVersions,app,appMessageThreadDetail,build,betaBackgroundAssetReviewSubmission&limit[appStoreVersions]=2000&filter[threadType]=REJECTION_BINARY,REJECTION_METADATA,REJECTION_REVIEW_SUBMISSION,APP_MESSAGE_ARC,APP_MESSAGE_ARB,APP_MESSAGE_COMM,APP_MESSAGE_INFORMATIONAL`, used by `asc web review threads --app`) are distinct; send the exact known-good query shapes because narrowed/modified includes return 400.
- Unsent draft replies live at `GET /iris/v1/resolutionCenterThreads/{threadId}/resolutionCenterDraftMessage?include=resolutionCenterMessageAttachments,fromActor&limit[resolutionCenterMessageAttachments]=1000`. Explicit `data: null` or HTTP 404 means no draft; a response missing `data` is malformed and fails closed. `asc web review threads --drafts` omits signed attachment download URLs.
- Mutations (`POST /iris/v1/resolutionCenterDraftMessages`, `PATCH`/`DELETE` on the draft resource, and `POST /iris/v1/resolutionCenterMessages` with `createFromDraftMessage` for `asc web review reply` and `asc web review drafts create|update|delete`) require `--confirm`, never upload attachments, never retry ambiguous writes, and remain unverified against a live fixture.

## Web-session app create conflicts (`POST /iris/v1/apps`)

- Re-creating an existing app returns HTTP 409 with three `errors[]` entries whose codes are, in order, `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`, `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`, and `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE.SAME_ACCOUNT` (captured live on 2026-09-29; `detail`, `title`, and `source` were not captured), so `APIError.AllCodes()` must inspect every entry in `errors[]` rather than only the first. `asc web apps create --if-exists skip` keys on either code and resolves the existing app via the public API (`GET /v1/apps?filter[bundleId]=`, then `filter[sku]=`).

## Web-session app distribution method

- `distributionType` (`APP_STORE` or `CUSTOM`) and `educationDiscountType` (`DISCOUNTED`, `NOT_DISCOUNTED`, `NOT_APPLICABLE`) exist only on `/iris/v1/apps/{id}` (no sparse fieldset). Missing `distributionType` is rendered as `unknown` in tables and omitted in JSON.
- `asc web apps distribution set --app APP_ID --method public|private --confirm` preflights `GET /iris/v1/apps/{id}`, rejects `DIRECT_URL` or unknown methods, sends one `PATCH /iris/v1/apps/{id}` with `educationDiscountType` and conditional `distributionType`, and verifies via read-back without retries (uncertain outcomes return `status: "uncertain"`). Source-backed; live mutation remains unverified. Unlisted App Store distribution has no captured endpoint.

## Web-session custom-distribution Apple Account recipients

- Private JSON:API under `/iris/v1` (`customAppUsers` only; `customAppOrganizations` is unsupported):
  - `asc web apps distribution users list --app APP_ID [--paginate]`: reads `GET /iris/v1/apps/{appId}/customAppUsers?limit=100` and validates pagination invariants strictly.
  - `create --app APP_ID --recipient-apple-id APPLE_ACCOUNT --confirm` (`POST /iris/v1/customAppUsers` -> `201`) and `delete --app APP_ID --id RECIPIENT_ID --confirm` (`DELETE /iris/v1/customAppUsers/{recipientID}` -> `204`) require the app to be in `distributionType: CUSTOM` (never switching distribution implicitly), send at most one write without retry, and verify against a fresh complete collection read. Browser contract is live-verified; final CLI live execution remains unverified.

## App transfer status (web session)

- `asc web apps transfer status --app APP_ID` reads `GET /iris/v1/apps/{appId}?include=appTransferRequest` (live-verified for `data: null`). Do not call `/iris/v1/apps/{appId}/appTransferRequest` directly (returns HTTP 409 `ENTITY_ERROR.RELATIONSHIP.INVALID` when no transfer exists). Transfer initiation/acceptance actions have no captured API contract.

## Last-compatible version settings (`downloadable`)

- Last-compatible version settings use the nullable boolean `downloadable` attribute on public `appStoreVersions` (`asc versions list/view --output json` and `asc versions update --downloadable true|false`; `--downloadable false` requires `--confirm`). Absent `downloadable` is never defaulted to `true`.
- Live-version lookups query `filter[appStoreState]` and `filter[appVersionState]` in separate requests (Apple combines them with AND) and prefer `appVersionState` (`READY_FOR_DISTRIBUTION`) over `appStoreState` (`READY_FOR_SALE`), while checking `appStoreState` for `PREORDER_READY_FOR_SALE`, `DEVELOPER_REMOVED_FROM_SALE`, and `REMOVED_FROM_SALE`.

## App Store version creation conflicts (`POST /v1/appStoreVersions`)

- Apple returns HTTP 409 `ENTITY_ERROR.RELATIONSHIP.INVALID` ("You cannot create a new version of the App in the current state.") both when an unreleased version already exists for the platform and (with `ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE`) when the version string is already used.
- On an unhandled 409, `asc versions create` makes a best-effort 5s paginated read of `GET /v1/apps/{id}/appStoreVersions?filter[platform]=...&limit=200` (which has no `sort` parameter, so pages are sorted client-side by `createdDate`) to name existing/unreleased versions in the diagnostic.

## Web-session app status history

- `asc web apps history --app APP_ID [--version-id VERSION_ID]` lists versions via `GET /iris/v1/apps/{id}/appStoreVersions` and serially reads `GET /iris/v1/appStoreVersions/{appStoreVersionId}/appStoreVersionStateChanges` (`appStoreState`, `date`, `initiator`), following `links.next` internally. Requires the `AppStatusHistory` role capability; use `--version-id` or raise `ASC_TIMEOUT` for apps with long histories.

## Web-session review submissions (iris)

- `GET /iris/v1/reviewSubmissions/{id}/items` rejects `include=appStoreVersionExperimentV2` with HTTP 400 `PARAMETER_ERROR.INVALID` (live-verified); `asc web review show` omits it and keeps iris-accepted includes (`inAppPurchaseVersion`, `subscriptionVersion`, `subscriptionGroupVersion`).

## TestFlight Distribution

- `externalBuildState` cannot be patched on build beta details (`--external-testing` was removed in 5.0.0); manage external distribution via `asc builds add-groups ... --submit --confirm` and `asc builds remove-groups ... --confirm`.
- `POST /v1/builds/{id}/relationships/betaGroups` can briefly return a build-specific 404 right after upload; `asc publish testflight` confirms `GET /v1/builds/{id}` (stopping on `FAILED`/`INVALID`) and retries only that propagation 404 with bounded backoff.
- `asc builds add-groups` preflights build state (and external build beta detail when assigning external groups), failing closed on blockers. On HTTP 422, it performs best-effort diagnostic reads of the build and beta detail. `--dry-run` performs the read-only resolution/preflight without mutating or submitting (`--submit` conflicts with `--dry-run`).

## TestFlight text validation (What to Test)

- `betaAppLocalizations.description` and `betaBuildLocalizations.whatsNew` reject `<`, uncomposed generic combining marks, supplementary-plane characters (`U+10000`–`U+10FFFF`, including all emoji), Variation Selectors (`U+FE00`–`U+FE0F`), enclosing/keycap marks (`U+20D0`–`U+20FF`), and symbol/dingbat/box/braille/private-use ranges (`U+2400`–`U+245F`, `U+2500`–`U+259F`, `U+2600`–`U+27EF`, `U+2800`–`U+28FF`, `U+2980`–`U+2BFF`, `U+E000`–`U+F8FF`, `U+FFFD`) with 409 invalid-character errors (`Text for whatsNew contains invalid characters` / `Text contains invalid characters/formats`). Representative live probes across these blocks (including `U+1F600`, `U+10330`, `U+20000`, `U+FE00`/`U+FE0E`/`U+FE0F`, `U+20DD`/`U+20E3`, `U+2400`/`U+2440`, `U+2500`/`U+257F`/`U+2588`/`U+2591`, `U+2600`–`U+26FF`, `U+2701`–`U+27BF`, `U+27C2`, `U+2801`, `U+2980`, `U+2A00`, `U+2B00`–`U+2BFF`, `U+E000`/`U+F8FF`, and `U+FFFD`) were rejected, while sampled code points in neighboring blocks (`Arrows`, `Letterlike Symbols`, `Miscellaneous Technical`, `Enclosed Alphanumerics`, `Geometric Shapes`) and script-specific marks (Devanagari, Thai, Hebrew, Arabic, Cyrillic) were accepted (full probe log in [API_CANARY_LOG.md](API_CANARY_LOG.md)).
- `asc builds test-notes create|update`, `asc builds upload --test-notes`, and `asc publish testflight --test-notes` validate before any HTTP call: they NFC-normalize, strip `<` and uncomposed generic combining marks (`U+0300`–`036F`, `U+1AB0`–`1AFF`, `U+1DC0`–`1DFF`, `U+20D0`–`20FF`, `U+FE20`–`FE2F`) with a stderr notice, and exit 2 on rejected symbol/emoji code points without echoing the text.
- Before writing What to Test, the CLI ensures `betaAppLocalizations` exists for the locale (`filter[app]` + case-insensitive `filter[locale]`), creating it if absent and re-checking on `POST /v1/betaAppLocalizations` HTTP 409 (which can mean either duplicate or invalid locale).

## App Store review details (`appStoreReviewDetails`)

- `notes`: rejected over 4000 characters; `asc review details-create` and `details-update` count Unicode characters of trimmed text with CRLF as 1 (`validation.ReviewNotesLength`) and exit 2 before sending when over 4000.
- Contact fields: `details-create` with default `--if-exists fail` requires non-blank `--contact-first-name` and `--contact-phone` before `POST /v1/appStoreReviewDetails` (`--if-exists skip|update` and `details-update` are exempt).
- `contactPhone`: `+1` numbers written with digits and standard separators must have exactly 10 digits after `+1`; other country codes and numbers without `+` are left to App Store Connect.

## Game Center

- Endpoints resolve Game Center detail via `/v1/apps/{id}/gameCenterDetail` (404 when disabled; `asc game-center details list` has no pagination flags).
- Achievements, leaderboards, and leaderboard sets require creating a release resource to go live; image uploads use reserve → upload → commit.
- `challengesMinimumPlatformVersions` on `gameCenterDetails` is a replace-only `PATCH` relationship (GET is rejected) that requires live `appStoreVersions` linkages (rejects `gameCenterAppVersions` and non-live versions with `ENTITY_ERROR.RELATIONSHIP.INVALID.MIN_CHALLENGES_VERSION_MUST_BE_LIVE`).
- Leaderboard-set member localizations have no direct GET; `view --id` resolves the parent leaderboard and set, then scans the filtered collection. Group challenge relationships are read-only; use `asc game-center challenges create --group-id`.

## Apple Ads Platform API v1

- Platform API v1 (`https://api.ads.apple.com/v1/`) is the primary `asc ads` surface (since 4.4.0). Legacy Campaign Management API v5 (retiring January 26, 2027) lives under `asc ads v5` with deprecation warnings (`asc ads v5 api request` stays v5; `asc ads api request` is v1).
- Account-scoped v1 calls send `X-AP-Context: adAccountId=<AD_ACCOUNT_ID>;` resolved from `--ad-account`, `ASC_ADS_AD_ACCOUNT_ID`, profile `ad_account_id`, or root `ads.ad_account_id` when no profile is selected (`--org` / `ASC_ADS_ORG_ID` are never fallbacks).
- `POST /v1/ad-accounts` omits `X-AP-Context`, whereas `GET`/`PUT /v1/ad-accounts/{id}` require `X-AP-Context: adAccountId=<id>;` matching `{id}`.
- `asc ads auth login --network`, `asc ads auth status --validate`, and `asc ads auth discover` call `GET /v1/me` (and `GET /v1/acls` for `discover`) without `X-AP-Context` (`ASC_ADS_ACCESS_TOKEN` skips OAuth token exchange).
- V1 report pagination belongs in the JSON request body (no `--paginate` flag). Negative keywords use a single v1 resource without bulk-delete; product-page countries/devices and custom impression-share report list/view have no one-command v1 replacement. The deprecated `asc ads v5 reports preset` warning follows `--level`: campaigns, ad groups, ads, keywords, and search terms point to their matching `asc ads reports apps` command; the two ad-group-specific keyword levels point to v1's consolidated `keywords` or `search-terms` report.

## Authentication & Rate Limiting

- App Store Connect JWTs are valid for 10 minutes. `--api-debug` / `ASC_DEBUG=api` logs `X-Rate-Limit` to stderr.
- Retries (`ASC_MAX_RETRIES`, `ASC_BASE_DELAY`, `ASC_MAX_DELAY`, `ASC_RETRY_LOG`; `ASC_MAX_RETRIES=0` disables):
  - GET/HEAD retry transient 408/429/5xx and transport errors.
  - Ordinary POST/PATCH/PUT/DELETE replay only on 429 (ambiguous 408/5xx and transport errors are not replayed); explicitly idempotent mutations retry transient failures.
  - Presigned `uploadOperations` PUT parts retry per part on 408/429/500/502/503/504 and transient transport errors within `ASC_UPLOAD_TIMEOUT`.
  - Unauthenticated public storefront GET commands retry 429/5xx and honor seconds or HTTP-date `Retry-After` up to `ASC_MAX_DELAY`.
- Concurrency: clients cap ordinary mutating requests at 8, with an opt-in bulk ceiling of 16 (`asc.WithBulkMutatingRequestLimit`) used by per-territory availability PATCH fan-out (`PATCH /v1/territoryAvailabilities/{id}`; bulk `POST /v2/appAvailabilities` on already-configured apps is unverified).

## Profiles and certificates list filters

- `GET /v1/profiles` and `GET /v1/certificates` reject `filter[platform]`; only Bundle ID list accepts `filter[platform]` (including `UNIVERSAL`).
- Provisioning profile names generated by `signing fetch` / `signing sync` and explicit `profiles create --name` values are capped at 64 Unicode code points.

## Builds

- `GET /v1/apps/{id}/builds` has no defined order and rejects `sort` with 400 `PARAMETER_ERROR.ILLEGAL`; always query latest builds via `GET /v1/builds?filter[app]={id}&sort=-uploadedDate&limit=1`. Relationship endpoints and top-level collections frequently accept different `sort`/`filter` parameters.
- Build resources and `GET /v1/buildBundles/{id}/buildBundleFileSizes` do not identify `UIDeviceFamily` (iPhone-only builds still list iPad `deviceModel` rows); `asc validate` reads `UIDeviceFamily` from `--ipa` when checking required iPad screenshots.
- Build Upload API reads (`/v1/buildUploads*`, `/v1/buildUploadFiles*`, `/v1/apps/{id}/buildUploads`, `/v1/apps/{id}/relationships/buildUploads`) intermittently return a false 404 `There is no resource of type 'apps' with id '…'`. Top-level reads retry that `type 'apps'` 404 directly; app-scoped reads first verify `GET /v1/apps/{id}` once and retry only when the app exists.

## Xcode Cloud workflows

- **Public `/v1/ciWorkflows` quirks**:
  - `GET /v1/ciWorkflows/{id}` omits `data` linkages on relationships unless `?include=product,repository,xcodeVersion,macOsVersion` is requested (all four are required by `POST /v1/ciWorkflows`), and emits `null` for optional non-nullable create properties (`destination`, `testConfiguration`, `filesAndFoldersRule`) that `workflows duplicate` must strip.
  - TestFlight post-actions (`post_actions[].deployment_config.testflight_deployment_ids`, whose `beta_group_ids` match public `betaGroups` IDs; `beta_tester_ids` equivalence is unverified) and workflow-scoped env vars/secrets exist only on private `/ci/api/` payloads (`asc web xcode-cloud env-vars`).
- **Private `/ci/api` web-session surfaces**:
  - `asc web xcode-cloud scm providers list` (`GET /ci/api/teams/{teamID}/scm-providers-v2`) and `connection-status` (`GET .../scm-providers/{providerID}/connection-v2`): read-only, no pagination; provider contract is browser-verified, CLI live execution unverified.
  - `asc web xcode-cloud settings next-build-number show|set`: `GET` and bodyless `PUT .../products/{productID}/next-build-number?next_build_number=N` (`--confirm`; requires `N` > current, verifies read-back, and redacts any returned TestFlight URL).
  - `asc web xcode-cloud settings version-aliases list|view|create|update|delete`: uses `/ci/api/teams/{publicProviderID}/products/{productID}/configuration-options/version-aliases-v3` (`?limit=100` on list, no `--paginate`; detail `GET`/`PUT`/`DELETE` on `.../{versionAliasID}`). Saves `{name,type,build,locked}` with trimmed `name` (1–40 UTF-16 code units) and `type` in `macos_version|xcode_version`. Mutations require `--confirm`, never retry 408/5xx, and reconcile via read-back (or 404 on delete). Source-captured; live alias mutations remain unverified.

## Devices

- Devices have no DELETE endpoint; enable or disable via `PATCH`. Registration requires a UDID (iOS) or Hardware UUID (macOS). Resetting the device list is portal-only once per membership year, and disabling does not free slots.

## Subscription Offer Codes

- `POST /v1/subscriptionOfferCodes` requires the `prices` relationship for all modes: `FREE_TRIAL` must omit `subscriptionPricePoint` (`--prices "DEU,FRA"`, otherwise 409 `ENTITY_ERROR.RELATIONSHIP.INVALID`), while paid modes require `--prices "DEU:PRICE_POINT_ID"`.

## Monthly Subscriptions with a 12-Month Commitment

- Exposed via `subscriptionPlanAvailabilities` (`/v1/subscriptions/{id}/planAvailabilities`) with `planType=MONTHLY` (upfront availability stays on `subscriptionAvailability`).
- `/v1/subscriptionPricePoints/{id}/adjustedEqualizations` accepts only `filter[planType]=MONTHLY` (rejects `UPFRONT`).
- Unavailable in `USA` and `SGP`; the CLI strips both territories before writing monthly-commitment plan availability.

## Subscription Plan Availability

- `GET /v1/subscriptions/{id}/planAvailabilities?include=availableTerritories` caps `limit[availableTerritories]` at 50; complete territory lists require `GET /v1/subscriptionPlanAvailabilities/{id}/relationships/availableTerritories` (`limit=200`, cursor-paginated).
- `PATCH /v1/subscriptionPlanAvailabilities/{id}` replaces `availableTerritories` wholesale and allows only `availableInNewTerritories` as a mutable attribute (`planType` is create-only, and `availableInNewTerritories` is unsupported for `MONTHLY`). `set` verifies territories via the paginated relationship endpoint and `availableInNewTerritories` via a fresh resource GET.
- Removing an approved subscription from sale (emptying `availableTerritories`) requires the Account Holder web session (`asc web subscriptions availability remove-from-sale` on `/iris/v1`).

## Developer Portal session (web session)

- Bundle IDs, Services IDs, Website Push IDs, App Groups, iCloud containers, and agreements bootstrap CSRF and team context via `POST /services-account/QH65B2/account/listTeams.action`. Same-origin HTTPS redirects are enforced, and cookies/CSRF tokens are never logged.
- `--developer-team` (ID or exact name) is accepted only on Developer Portal commands (`web bundle-ids`, `web service-ids`, `web website-push-ids`, `web app-groups`, `web icloud-containers`, `web agreements`). An explicit selector overrides the cached team; without one, the cached team is reused if available, otherwise the selected App Store Connect provider is matched to a team. A single remaining team is used; multiple unmatched teams require `--developer-team`. There is no team-selection environment-variable fallback.
- `asc web bundle-ids capabilities disable --bundle-id ID --capability PRIVATE_CLOUD_COMPUTE --confirm` requires confirmation and currently supports only `PRIVATE_CLOUD_COMPUTE`. It resolves the Bundle ID through `POST /services-account/v1/bundleIds` (`X-HTTP-Method-Override: GET`), reads `fields[bundleIds]=bundleIdCapabilities` + `include=bundleIdCapabilities,bundleIdCapabilities.capability` (`limit[bundleIdCapabilities]=50`), fails closed on pagination/linkage ambiguity, sends a single `PATCH /services-account/v1/bundleIds/{id}` with `enabled:false` on the target capability, and verifies disabled/removed state via read-back without retry (private proxy acceptance of the field/limit query remains live-unverified).

## Developer Portal Bundle ID reads (web session)

- `asc web bundle-ids list` sends `POST /services-account/v1/bundleIds` (`X-HTTP-Method-Override: GET`) with `teamId` and `urlEncodedQueryParams` (`limit=1000`, `sort=name`, `filter[platform]=IOS,MACOS`). Preserves the JSON:API envelope and warns on stderr if more pages are indicated (no `--paginate`).
- `asc web bundle-ids view --bundle-id ID` sends `POST /services-account/v1/bundleIds/{id}` (`X-HTTP-Method-Override: GET`, body `{teamId}`) with `fields[bundleIds]=name,identifier,platform,seedId,wildcard,~permissions.delete,~permissions.edit` and the full capability/app-group/merchant/cloud-container/certificate/consent include graph.

## Developer Portal Services IDs (web session)

- Public OpenAPI `BundleIdPlatform` omits `SERVICES`; Services IDs use the private `/services-account/v1/bundleIds` proxy (`X-HTTP-Method-Override` for `GET`/`DELETE`, direct `POST`/`PATCH` for writes) and verify `attributes.platform=SERVICES`:
  - `list`: `POST /services-account/v1/bundleIds` (`X-HTTP-Method-Override: GET`, `limit=1000`, `sort=name`, `filter[platform]=SERVICES`, body `{urlEncodedQueryParams, teamId}`).
  - `view --service-id ID`: `POST /services-account/v1/bundleIds/{id}` (`X-HTTP-Method-Override: GET`, body `{teamId}`) with `include=bundleIdCapabilities,bundleIdCapabilities.capability,bundleIdCapabilities.appConsentBundleId`.
  - `create --identifier ID --name NAME --confirm`: `POST /services-account/v1/bundleIds` with `data.type=bundleIds`, `attributes.{identifier,name,platform:"SERVICES",seedId,teamId}`, and empty `bundleIdCapabilities.data` (**must omit root-level `teamId`**, which Apple rejects on non-override `POST`). Verifies via detail read-back.
  - `rename --service-id ID --name NAME --confirm` and `domains set`: preflight and post-write reads enforce `bundleIdCapabilities` completeness (no `next` link, and either exact `paging.total` or the captured `paging.total=0, limit=2147483647` placeholder with resolved references; known navigation-only links like `appGroups`/`bundleId` without `data` are omitted from `PATCH`). `domains set` updates `APPLE_ID_AUTH` (`APPLE_ID_AUTH_WEB_DOMAIN`, `APPLE_ID_AUTH_WEB_RETURN_URL`) on an already enabled Services ID and is live-verified.
  - `delete --service-id ID --confirm`: sends `POST /services-account/v1/bundleIds/{id}` with `X-HTTP-Method-Override: DELETE` and **root-level `teamId`** (omitting it returns HTTP 403 `Please select a team.`), expects 204, and verifies a follow-up detail 404. No Services ID mutation retries automatically.

## Developer Portal Website Push IDs (web session)

- `asc web website-push-ids list` calls legacy `POST /services-account/QH65B2/account/ios/identifiers/listWebsitePushIds.action` (`onlyCountLists=true`, `pageSize=1000`, `pageNumber=1`, `sort=name=asc`, `teamId`, no `sidx`; no `--paginate`) and preserves the root `websitePushIdList` response.
- `view`, `create`, and `delete` use modern `/services-account/v1/websitepushIds` (live-verified):
  - `view`: `POST /services-account/v1/websitepushIds/{id}?include=websitepushIdCapabilities` (`X-HTTP-Method-Override: GET`, body `{teamId}`).
  - `create` (`--confirm`): requires legacy list preflight to be non-full (`pageNumber=1`, rows < `pageSize`) with no matching identifier and an empty capability catalog, sends `POST /services-account/v1/websitepushIds` (`data.type=websitepushIds`, `name`, `identifier`, `teamId`, empty `websitepushIdCapabilities`), and settles via detail/legacy-list read-back without retry.
  - `delete` (`--confirm`): preflights modern detail (`canDelete=true` and proven empty capability relationship), sends `POST /services-account/v1/websitepushIds/{id}` (`X-HTTP-Method-Override: DELETE`, body `{teamId}`), expects 204, and verifies absence in both detail and legacy list without retry. Rename and non-empty capability configuration are unsupported.

## Developer Portal Agreements (web session)

- `asc web agreements status|accept|download` uses `POST /services-account/QH65B2/account/getAgreementHistory` and `POST /services-account/QH65B2/account/acceptAgreements` (HTTP 200 with `resultCode=0` on success). `accept` re-reads `getAgreementHistory` to verify `dateAccepted >= dateEffective`. `download` resolves root-relative `agreementDownloadUrl` paths against the Developer Portal origin (same-origin HTTPS only, non-empty/non-HTML) and redacts the URL from download receipts and errors.

## Transaction Tax reports (web session)

- `asc web finance transaction-tax download` reads `/WebObjects/iTunesConnect.woa/ra/paymentConsolidation/providers/{providerId}/sapVendorNumbers/{sapVendorNumber}?year={YYYY}&month={M}` (requiring `hasVendorTaxReport=true`), derives `regionCurrencyIds` from `reportSummaries[].proceedsByRegion[]`, triggers generation via `GET .../reports?...&isVendorTaxReportReq=true`, polls `/reports/{uuid}/status` for `readyForDownload`, and streams the same-origin HTTPS ZIP under a bounded workflow context into a new `0600` file without retries or leaking sensitive identifiers/URLs.

## Pass Type IDs

- `/v1/passTypeIds/{id}/certificates` rejects `include=passTypeId` and `fields[passTypeIds]` on the live API despite the OpenAPI spec; `asc pass-type-ids certificates list` omits both.

## Sparse Fieldsets Combined with Includes

- `GET /v1/profiles` with `fields[profiles]=name&include=devices` populates `included` even though `devices` is omitted from the primary fieldset; `fields[devices]=...` still sparse-filters included devices. Apple omits per-profile `relationships` unless `devices` is also in `fields[profiles]` (live-verified).
- `GET /v1/certificates` accepts `fields[certificates]=displayName&include=passTypeId`. The probed team had no `PASS_TYPE_ID` certificates, so `included` was absent with and without the sparse fieldset; population of related pass types remains unverified. Non-pass certificates expose `relationships.passTypeId.data=null` only when `passTypeId` is in the fieldset or no certificate fieldset is sent (live-verified; see [the canary record](API_CANARY_LOG.md#sparse-fieldsets-combined-with-includes)).

## App Store Connect API 4.4.1

- Apple added discrete versions for in-app purchases, subscriptions, and subscription groups. Their v2 localizations and images are version-scoped; pass a version ID rather than the legacy product, subscription, or group ID.
- Review submissions accept `inAppPurchaseVersions`, `subscriptionVersions`, and `subscriptionGroupVersions` through `reviewSubmissionItems`. The CLI preserves both relationship data and `included` resources in JSON output.
- API 4.4.1 has no item-detail GET operation (`asc review items view` and `asc review items-get` were removed in 4.0.0); list a parent submission's items with `asc review items list --submission "SUBMISSION_ID"`.
- API 4.4.1 has no marketplace-webhook instance GET operation; `asc marketplace webhooks view` selects the exact ID across all pages of the collection GET.
- Review-item updates accept only nullable `resolved` and `removed` attributes (`--state` was removed in 4.0.0; use `--resolved`, `--removed`, or their matching `--clear-*` flags, with `--confirm` for `removed=true`).
- Review-submission updates expose nullable `platform`, `submitted`, and `canceled` values plus matching `--clear-*` flags (`--confirm` required for `submitted=true` or `canceled=true`; false, null, and platform-only updates do not).
- Canceling a review submission in a non-cancellable state returns HTTP 409 `Resource state is invalid.: Resource is not in cancellable state`. On that error path, `asc review submissions-cancel`, `asc review submissions-update --canceled=true`, and `asc submit cancel` read `GET /v1/reviewSubmissions/{id}` once to explain its state and next step while preserving the conflict exit code.
- The create schema names its second experiment relationship `appStoreVersionExperimentV2`, while its linked resource type remains `appStoreVersionExperiments`; the CLI selector is `appStoreVersionExperimentsV2` (the 3.x `--item-type appStoreVersionExperimentV2` alias was removed in 4.0.0, and experiment treatments are not valid review-item create relationships). Review items require `appCustomProductPageVersions` (`appCustomProductPages` is rejected).
- The product-scoped v1 localization, image, submission, and `sync` command families (`asc iap localizations|images|submit`, `asc subscriptions localizations|images|submit`, `asc subscriptions groups localizations|submit`, `asc subscriptions review submit|submit-group`) were removed in 5.0.0. `asc iap setup` and `asc subscriptions setup` remain supported and warn only when localization flags request legacy v1 localization steps.
- Migration mapping:
  - IAP localizations/images → create or resolve an IAP version, then use `asc iap versions localizations ...` / `asc iap versions images ...`.
  - Subscription localizations/images → create or resolve a subscription version, then use `asc subscriptions versions localizations ...` / `asc subscriptions versions images ...`.
  - Subscription group localizations → create or resolve a group version, then use `asc subscriptions groups versions localizations ...`.
  - IAP / subscription / subscription-group submissions → `asc review items add --submission "SUBMISSION_ID" --item-type <inAppPurchaseVersions|subscriptionVersions|subscriptionGroupVersions> --item-id "<VERSION_ID>"`.
- There is no one-command version-scoped replacement for the removed legacy localization `sync` leaves; reconcile entries through the matching version-localization `list`/`create`/`update`/`delete` commands.
- A legacy IAP image file re-upload has no one-to-one v2 update: create the replacement version image, then delete the old version image if needed. Subscription v2 image updates do not expose the legacy checksum flag; use the version image upload workflow for a new file.
- Exported `internal/asc.Client` methods targeting these legacy resources remain callable during the transition window, with `Deprecated:` comments on legacy submission and IAP localization/image entrypoints naming their version-scoped or review-item replacements.
- Nullable v2 localization updates distinguish omitted, value, and JSON `null`; use the corresponding `--clear-*` flag for explicit clears.

## Developer Portal App Groups (web session)

- `asc web app-groups` combines legacy form endpoints under `/services-account/QH65B2/account/ios/identifiers/` (`listApplicationGroups.action`, `addApplicationGroup.action`, `deleteApplicationGroup.action`; `application/x-www-form-urlencoded` with `teamId` and scoped `csrf`/`csrf_ts` headers, returning `resultCode=0`) with `/services-account/v1/bundleIds`:
  - `delete` (`deleteApplicationGroup.action`, inferred and live-unverified) first scans all team Bundle IDs via proxied `POST /services-account/v1/bundleIds` (`X-HTTP-Method-Override: GET`, `fields[bundleIds]=bundleIdCapabilities`, `limit=200`, `limit[bundleIdCapabilities]=50`, following `links.next`) and refuses if any `APP_GROUPS` capability (even disabled) references the group. After delete, it verifies absence across the strictly paginated App Group list.
  - `assign`, `unassign`, and `set` read the Bundle ID with `fields[bundleIds]=bundleIdCapabilities` and `limit[bundleIdCapabilities]=50`, fail closed on incomplete/ambiguous capability or nested `appGroups` pagination, rewrite only `APP_GROUPS`, and `PATCH /services-account/v1/bundleIds/{id}` (live proxy acceptance remains unverified). `assign` rejects a `group.*` identifier with exit 2 when the team listing proves an opaque resource ID is required. Ambiguous writes settle via read-back without retry and return `DeveloperAppGroupUnverifiedError` when unverified.

## Offline signature verification: Apple leaf certificate types

The offline verifier uses Go's certificate-chain policy, not macOS's platform trust policy. SHA-1-signed leaf or intermediate certificates report `untrusted-chain`, even when `codesign --verify --strict` accepts the binary. The receipt still reports successful CMS and code-page integrity checks when those checks pass. Recognizing an Apple leaf type does not override certificate-chain policy, expiry, or trust anchors.

`asc ipa-info --verify-signature` accepts only code-signing leaves and `asc pkg-info --verify-signature` accepts only installer leaves (`internal/artifacts/verify_leaf.go`). Leaves are matched most-specific-first by Apple OID markers (`1.2.840.113635.100.…`, treated as handled critical extensions) and required EKU; unrecognized or dual-use leaves report `untrusted-chain`:

| Type | Apple markers (`1.2.840.113635.100.…`) | EKU | Artifact | Observed on |
|---|---|---|---|---|
| Apple Development | `6.1.2` + `6.1.12` | codeSigning | IPA / Mach-O | keychain; profiles; Apple docs |
| Apple Distribution | `6.1.4` + `6.1.7` | codeSigning | IPA / Mach-O | keychain; profiles; Apple docs |
| iPhone Developer (legacy) | `6.1.2` | codeSigning | IPA / Mach-O | keychain; profiles |
| iPhone Distribution (legacy) | `6.1.4` | codeSigning | IPA / Mach-O | keychain; profiles; App Store export IPA executable |
| Mac App Distribution | `6.1.7` | codeSigning | IPA / Mach-O | Mac half of Apple Distribution |
| Mac Development | `6.1.12` | codeSigning | IPA / Mach-O | Mac half of Apple Development; Apple docs |
| Developer ID Application | `6.1.13` | codeSigning | IPA / Mach-O | keychain; third-party `/Applications` apps; Apple docs |
| Mac App Store application signing | `6.1.9` | codeSigning | IPA / Mach-O | Mac App Store app in `/Applications`; Apple docs |
| Apple platform signing | `6.22` | codeSigning | IPA / Mach-O | `/bin/ls`, Safari |
| Developer ID Installer | `6.1.14` | `1.2.840.113635.100.4.13` | pkg | notarized third-party pkg |
| Mac Installer Distribution | `6.1.8` | `1.2.840.113635.100.4.9` | pkg | Xcode-exported Mac App Store pkgs |
| Apple package signing (Software Update) | `6.1.29.2` | `1.2.840.113635.100.4.1` | pkg | Xcode's bundled `CoreTypes`, `MobileDevice`, `MobileDeviceDevelopment`, and `XcodeSystemResources` pkgs |

- iOS App Store re-signing and TestFlight (`6.1.25.x`) leaves lack local certificate fixtures and report `untrusted-chain`. CMS-only `x-signature` Mac App Store pkgs report `unsupported`.

## Offline signature verification: xar member payload checksums

- `asc pkg-info --verify-signature` (`internal/artifacts/verify_xar.go`) validates all `<file><data>` heap ranges in the xar table of contents (including component packages) before hashing and verifies each distinct range's `archived-checksum` (`sha1`, `sha256`, `sha384`, `sha512`, `md5`; other algorithms report `unsupported` unless another member is `invalid`).
- Missing checksums on heap data, malformed hex, out-of-heap ranges, or partially overlapping ranges report `invalid`. Extended-attribute (`<ea>`) checksums are intentionally not enforced because `pkgutil --check-signature` accepts modified `<ea>` heap bytes.

---

### CI CD (`docs/CI_CD.md`)

# CI/CD Integrations

## GitHub Actions

Install `asc` using the official setup action:

```yaml
- uses: rudrankriyam/setup-asc@v1
  with:
    version: latest

- run: asc --help
```

For end-to-end examples, see:
https://github.com/rudrankriyam/setup-asc

## GitLab CI/CD Components

Use the official `asc-ci-components` repository:

```yaml
include:
  - component: gitlab.com/rudrankriyam/asc-ci-components/run@main
    inputs:
      stage: deploy
      job_prefix: release
      asc_version: latest
      command: asc --help
```

For install/run templates and self-managed examples:
https://github.com/rudrankriyam/asc-ci-components

## Bitrise

Use the official `setup-asc` Bitrise step repository:

```yaml
workflows:
  primary:
    steps:
    - git::https://github.com/rudrankriyam/steps-setup-asc.git@main:
        inputs:
        - mode: run
        - version: latest
        - command: asc --help
```

## CircleCI

Use the official CircleCI orb repository:
https://github.com/rudrankriyam/asc-orb

## Code signing on a fresh runner

A fresh macOS runner can go from an App Store Connect API key to a signed
archive without hand-written `security` calls. Provide `ASC_KEY_ID`,
`ASC_ISSUER_ID`, and `ASC_PRIVATE_KEY_B64` from the CI secret store, then run
these commands in one shell step:

1. First run only, when the team has no active certificate of the required
   type: `asc signing fetch --create-missing --create-missing-certificate`
   creates the private key, CSR, certificate, profile, and a
   password-protected `.p12`. Push that identity to the encrypted store
   (`asc signing sync push` with `--identity`) before building, because the
   private key exists only on this runner.
2. Every later run: `asc signing sync pull` decrypts the stored identity and
   profiles.
3. `asc signing keychain install --confirm` imports the identity into a new
   dedicated keychain, and `asc profiles local install` installs each profile.
4. `asc xcode signing plan` with `--profile` infers each target's signing
   settings and an `ExportOptions.plist` from the profiles, and
   `asc xcode signing apply --confirm` writes those settings into the project.
5. `asc xcode archive`, then `asc xcode export` with `--export-options`,
   produce the signed archive and IPA.
6. `asc signing keychain delete --confirm` removes the job keychain.

The first-run commands look like this. The full script, including the
password files, cleanup trap, and later-run variant, is in the
[code signing guide](../guides/code-signing.mdx) and the
[signing command reference](../commands/signing.mdx).

```bash
asc signing fetch --bundle-id com.example.app --profile-type IOS_APP_STORE --create-missing --create-missing-certificate --identity-password-file "$p12_password_file" --output .asc/signing/bootstrap --format json > .asc/signing/fetch-result.json
asc signing sync push --bundle-id com.example.app --profile-type IOS_APP_STORE --repo git@github.com:team/signing.git --password-file "$sync_password_file" --identity "$p12_path" --identity-password-file "$p12_password_file" --output json
asc signing keychain install --identity "$p12_path" --identity-password-file "$p12_password_file" --keychain "$keychain_path" --keychain-password-file "$keychain_password_file" --add-to-search-list --confirm --output json
asc profiles local install --path "$profile_path" --output json
asc xcode signing plan --project Example.xcodeproj --profile "$profile_path" --configuration Release --export-options-out .asc/artifacts/ExportOptions.plist --output json
asc xcode signing apply --plan .asc/xcode/signing/plan.json --confirm --output json
asc xcode archive --project Example.xcodeproj --scheme Example --configuration Release --archive-path .asc/artifacts/Example.xcarchive
asc xcode export --archive-path .asc/artifacts/Example.xcarchive --export-options .asc/artifacts/ExportOptions.plist --ipa-path .asc/artifacts/Example.ipa
asc signing keychain delete --keychain "$keychain_path" --confirm
```

`p12_path` and `profile_path` come from the `p12Path` and `profileFile` fields
of the fetch receipt. `xcode signing plan` exits 0 with `ready: false` when a
target has no matching profile; check `ready` before applying.

Expired or invalid profiles can already be removed with
`asc signing fetch --delete-stale-profiles`, and certificates can be
deactivated or revoked with `asc certificates`. With Git storage,
`asc signing sync push --renew-expired` replaces an expired synced profile,
`--force-for-new-devices` (optionally with `--include-mac-in-profiles`)
recreates a development or ad hoc profile when its devices change, and
`asc signing sync nuke` removes one profile type with its certificates and
encrypted files. These lifecycle options require Git storage.

### Signing assets in object storage with workload identity

`asc signing sync push`, `pull`, and `rotate-password` can keep the encrypted
artifacts in an S3 bucket instead of a Git repository with `--storage object`
and `--object-bucket`, plus optional `--object-prefix`, `--object-region`, and
`--object-endpoint`. Objects use the Git tree's layout, so a bucket prefix and
a signing repository can be mirrored byte for byte. No flag accepts a cloud
credential: credentials and, unless `--object-region` is set, the region come
from the standard AWS configuration chain (environment variables, shared
config or SSO profiles, web identity, or container and instance metadata).

In GitHub Actions, OpenID Connect lets the job assume an AWS role and receive
short-lived credentials, so no AWS access key is stored in the repository or
passed to `asc`:

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: rudrankriyam/setup-asc@v1
        with:
          version: latest

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/asc-signing-read
          aws-region: us-east-1

      - name: Pull signing assets
        env:
          ASC_SIGNING_SYNC_PASSWORD: ${{ secrets.ASC_SIGNING_SYNC_PASSWORD }}
        run: |
          asc signing sync pull --storage object --object-bucket team-certs --object-prefix asc/ --output-dir .asc/signing/pulled --output json
```

`configure-aws-credentials` exchanges the job's OIDC token for temporary role
credentials and exports them, with `AWS_REGION`, to later steps. The role's
trust policy should accept only this repository (and, ideally, a protected
branch or environment), and a pull-only role needs just `s3:ListBucket` and
`s3:GetObject` on the prefix. A job that runs `sync push` or
`sync rotate-password` also needs `s3:PutObject`, and rotation needs
`s3:DeleteObject` to remove its staged objects. The repository encryption
password still comes from the CI secret store, never from a command argument:
push and pull read `ASC_SIGNING_SYNC_PASSWORD` or a protected
`--password-file`, and `rotate-password` requires protected
`--password-file` and `--new-password-file` files and ignores the environment
variable.

Push and rotation use conditional writes, so a push that races another writer
fails with exit code 1 instead of overwriting newer data; pull and retry. For an
S3-compatible service, add `--object-endpoint` with its HTTPS origin; requests
then use path-style addressing, and a service that ignores `If-Match` and
`If-None-Match` loses the conflict protection. Receipts include
`storage.kind` (`object`) and `storage.location` (`s3://<bucket>/<prefix>`).

---

### COMMANDS (`docs/COMMANDS.md`)

# Command Reference Guide

This file is generated from live CLI help output.
For authoritative command behavior, also use:

```bash
asc --help
asc <command> --help
asc <command> <subcommand> --help
```

To regenerate:

```bash
make generate-command-docs
```

## Usage Pattern

```bash
asc <subcommand> [flags]
```

## Global Flags

- `--api-debug` - Enable HTTP debug logging to stderr (redacts sensitive values)
- `--debug` - Enable debug logging to stderr
- `--profile` - Use named authentication profile (accepted before or after the command name)
- `--read-only` - Refuse every mutating request (POST/PATCH/PUT/DELETE) before it is sent; ASC_READ_ONLY=1 has the same effect (default: false)
- `--report` - Report format for CI output (e.g., junit)
- `--report-file` - Path to write CI report file
- `--retry-log` - Enable retry logging to stderr (overrides ASC_RETRY_LOG/config when set)
- `--strict-auth` - Fail when credentials are resolved from multiple sources (default: false)
- `--version` - Print version and exit (default: false)

## Command Families

### Getting Started

- `auth` - Manage authentication for the App Store Connect API.
- `doctor` - Diagnose authentication configuration issues.
- `install-skills` - Install the asc skill pack globally for App Store Connect workflows.
- `init` - Initialize asc helper docs in the current repo.
- `docs` - Access embedded documentation guides and reference helpers.

### Web Session Commands

- `web` - Apple web-session workflows, including finance report downloads.

### Analytics and Finance

- `analytics` - Request and download analytics and sales reports.
- `ads` - Manage Apple Ads API resources.
- `optimize` - Build cross-API optimization plans.
- `insights` - Generate weekly and daily insights from App Store data sources.
- `finance` - Download payments and financial reports.
- `performance` - Access performance metrics and diagnostic logs.

### App Management

- `apps` - List and manage apps in App Store Connect.
- `app-setup` - Post-create app setup automation.
- `app-tags` - Inspect Apple-generated App Store discoverability tags.
- `versions` - Manage App Store versions.
- `localizations` - Manage App Store localization metadata.
- `metadata` - Manage app metadata with deterministic workflows and keyword tooling.
- `screenshots` - Upload and manage App Store screenshots, including local capture, framing, and matrices.
- `video-previews` - Manage App Store app preview videos.
- `asset-library` - Inspect Asset Library media and specifications, and upload images and videos.
- `background-assets` - Manage background assets.
- `product-pages` - Manage custom product pages and product page experiments.
- `routing-coverage` - Manage routing app coverage files.
- `pricing` - Manage app pricing and availability.
- `pre-orders` - Manage app pre-orders.
- `categories` - Manage App Store categories.
- `age-rating` - Manage App Store age rating declarations.
- `accessibility` - Manage accessibility declarations.
- `encryption` - Manage app encryption declarations and documents.
- `eula` - Manage End User License Agreements (EULA).
- `agreements` - Manage agreements in App Store Connect.
- `app-clips` - Manage App Clip experiences and invocations.
- `android-ios-mapping` - Manage Android-to-iOS app mapping details.
- `marketplace` - Manage marketplace resources.
- `alternative-distribution` - Manage alternative distribution resources.
- `nominations` - Manage featuring nominations.
- `game-center` - Manage Game Center resources in App Store Connect.

### TestFlight and Builds

- `testflight` - Manage TestFlight workflows.
- `builds` - Manage builds in App Store Connect.
- `build-bundles` - Manage build bundles and App Clip data.
- `build-localizations` - Manage build release notes localizations.
- `xcode` - Local Xcode build/archive/export and signing-settings helpers.
- `distribute` - Plan, execute, inspect, and publish iOS distribution artifacts.
- `ipa-info` - Inspect a local IPA without contacting App Store Connect.
- `pkg-info` - Inspect a local flat package or product archive without contacting Apple.
- `sandbox` - Manage sandbox testers in App Store Connect.

### Review and Release

- `release` - Run high-level App Store release workflows.
- `status` - Show a release pipeline dashboard for an app.
- `release-notes` - Generate and manage App Store release notes.
- `review` - Manage App Store review details, attachments, and submissions.
- `reviews` - List and manage App Store customer reviews.
- `submit` - Submission lifecycle tools; use `publish appstore --submit` to ship.
- `validate` - Canonical App Store submission readiness report.
- `publish` - High-level publish workflows for TestFlight and App Store.

### Monetization

- `iap` - Manage in-app purchases in App Store Connect.
- `storekit` - Manage StoreKit server APIs with In-App Purchase API keys.
- `app-events` - Manage App Store in-app events.
- `subscriptions` - Manage subscription groups and subscriptions.

### Signing

- `signing` - Manage signing certificates and profiles.
- `bundle-ids` - Manage bundle IDs and capabilities.
- `certificates` - Manage signing certificates.
- `profiles` - Manage provisioning profiles.
- `merchant-ids` - Manage merchant IDs and certificates.
- `pass-type-ids` - Manage pass type IDs.
- `notarization` - Manage macOS notarization submissions.

### Team and Access

- `account` - Inspect account-level health and access signals.
- `users` - Manage users and invitations in App Store Connect.
- `actors` - Lookup actors (users, API keys) by ID.
- `devices` - Manage devices in App Store Connect.

### Automation

- `workflow` - Run multi-step automation workflows.
- `webhooks` - Manage webhooks in App Store Connect.
- `xcode-cloud` - Trigger and monitor Xcode Cloud workflows.
- `notify` - Send notifications to external services.
- `migrate` - Migrate metadata from/to fastlane format.

### Utility

- `system-status` - Check Apple Developer service health.
- `diff` - Generate deterministic non-mutating diff plans.
- `capabilities` - Show CLI, API, web-only, and public-API-limited capability coverage.
- `search` - Search asc commands and examples for agent-oriented command discovery.
- `snitch` - Report CLI friction as a GitHub issue.
- `version` - Print version information and exit.
- `completion` - Print shell completion scripts.
- `schema` - Inspect App Store Connect API endpoint schemas at runtime.
- `api` - Send an authenticated raw request to the App Store Connect API.
- `telemetry` - Manage CLI telemetry settings.

## Scripting Tips

- Output defaults are TTY-aware: interactive terminals default to `table`, while piped/non-interactive output defaults to minified `json`.
- Use `--output table` or `--output markdown` for explicit human-readable output.
- Use `--output json` for explicit machine-readable output.
- Use `--paginate` on list commands to fetch all pages automatically.
- Use `--limit` and `--next` for manual pagination control.
- Any flag that takes a value accepts `@env:NAME` or `@file:PATH` to read it from the environment or a file; escape a literal leading `@` as `@@`. `--output` and the root `--profile`, `--report`, and `--report-file` are always read literally.
- Prefer explicit flags and deterministic outputs in CI scripts.

## High-Signal Examples

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Related Documentation

- [../README.md](../README.md) - onboarding and common workflows
- [API_NOTES.md](API_NOTES.md) - API-specific behavior and caveats
- [TESTING.md](TESTING.md) - test strategy and patterns
- [CONTRIBUTING.md](CONTRIBUTING.md) - contribution and dev workflow

---

### CONTRIBUTING (`docs/CONTRIBUTING.md`)

# Contributing

For development setup, integration tests, and PR guidelines, see the root `CONTRIBUTING.md`.

This file covers patterns for AI agents working on the codebase.

## Git Workflow

- Branch from `main` and keep one logical change per branch
- Do not commit directly to `main` unless explicitly instructed; prefer PRs
- Prefer `git worktree add` for parallel tasks; remove with `git worktree remove` when done
- Compare the branch with current `main` before merging. Do not rebase or merge `main` into a clean branch merely because `main` advanced; update only when GitHub reports an actual merge conflict or refuses an explicitly authorized merge attempt made after every readiness gate passes under strict up-to-date branch protection, and never bypass protection with an admin merge
- Preserve additive PR commits. Do not force-push, rebase, squash, or otherwise rewrite shared history unless the user explicitly requests it. When merging, use a regular merge commit by default
- Commit small, coherent changes; no WIP commits on shared branches
- Use concise, present-tense commit messages that match repo style
- Never commit secrets or local config files (keys, `.env`, `.asc/config.json`)

## Before Committing

For substantive behavior changes, run:

```bash
make build      # Build the CLI
make format     # Format code
make lint       # Check for issues
make check-docs  # Verify repo docs, website docs, and command docs stay in sync
ASC_BYPASS_KEYCHAIN=1 make test  # Run all tests without keychain prompts
git diff        # Review changes before staging
```

For a narrowly scoped documentation or skill change, run `make check-docs`; it includes the repository and skill validators. Use broader checks when the changed surface or repository policy requires them.

If `docs/wall-of-apps.json` is the only staged change, the local hook skips the full Go pipeline and only runs:

```bash
make check-wall-of-apps
```

## Repository Agent Skills

Keep always-on repository invariants in `AGENTS.md` and task-specific maintainer workflows in `.agents/skills/<skill-name>/`.

Each skill must contain a `SKILL.md` with only `name` and `description` frontmatter plus matching `agents/openai.yaml` UI metadata. Keep trigger descriptions specific, link detailed references through progressive disclosure, and run:

```bash
make check-agent-skills
```

`make check-docs` includes this validation.

## CLI Structure

- Command implementations live in `internal/cli/<domain>` packages
- Each domain exposes a top-level `XCommand() *ffcli.Command`
- `cmd/` only contains the root entry point; do not add wrapper files
- Register top-level commands in `internal/cli/registry/registry.go` (order matters)
- Shared CLI helpers go in `internal/cli/shared` (use `shared_wrappers.go` as needed)

## Adding a New Command

1. Add or extend a domain package in `internal/cli/<domain>`
2. Implement a command factory (e.g., `XCommand() *ffcli.Command`)
3. Register it in `internal/cli/registry/registry.go`
4. Write tests in the domain package (or `internal/cli/cmdtest` for root-level tests)
5. Update README.md with usage examples

## Adding a New API Endpoint

1. Add method to `internal/asc/client.go`
2. Add types for request/response structs
3. Add helper functions for table/markdown output
4. Create command in `internal/cli/<domain>` to expose the endpoint
5. Write HTTP client tests with mocked responses
6. If endpoint tests are repetitive, group the request-wiring cases, but keep at least one representative non-empty decode assertion and one representative output-structure assertion where formatting is user-facing

## Releases

Tag releases with plain semver like `0.1.0` (no `v` prefix).

### Pre-Release Checklist

Before tagging a release, verify:

```bash
# 1. Update release-facing docs
#    - update any website pages affected by the release
#    - release notes are auto-generated from merged PRs on tag push

# 2. Verify documentation
make check-docs

# 3. All tests pass
ASC_BYPASS_KEYCHAIN=1 make test

# 4. Audit help output for all parent commands
for cmd in auth analytics finance apps app-tags testflight builds versions \
           feedback crashes localizations \
           build-localizations sandbox submit xcode-cloud reviews; do
  echo "=== $cmd ===" && ./asc $cmd --help 2>&1
done

# 5. Check for duplicate sections (should see SUBCOMMANDS only once per command)
# 6. Verify bold formatting renders correctly
```

**Common issues to check:**
- No duplicate "Subcommands:" sections (don't list subcommands in LongHelp; DefaultUsageFunc handles it)
- All flags have descriptions
- Examples are up to date

## Parity map

Remaining fastlane/ASC parity areas and the intentional non-goal list live in [PARITY.md](PARITY.md).

---

### GO STANDARDS (`docs/GO_STANDARDS.md`)

# Go Standards

Follow idiomatic Go so the code is predictable to anyone who reads Go.

## Formatting

- Always run `gofmt` (and `gofumpt` via `make format`)
- No manual formatting

## Naming

- Use mixedCaps (not snake_case)
- Keep common initialisms uppercase: `ID`, `URL`, `API`, `JSON`

## Error Handling

- Return errors, don't panic for expected failures
- Wrap with context using `%w`: `fmt.Errorf("operation failed: %w", err)`

## Context

- Pass `context.Context` into network operations
- Respect timeouts and cancellations

## Types

- Model request/response types with JSON tags
- Use pointers for optional fields, values for required fields
- Prefer typed `const` values (not raw strings) for API enums and resource types

## CLI Behavior

- If a flag is accepted, it must be implemented or error
- Never silently ignore flags
- Data goes to stdout, errors to stderr
- Keep JSON minified by default
- If a command already prints structured output and must exit non-zero,
  return `shared.NewReportedError(err)`; the root prints `err` once to stderr
  as an `Error:` line
- If the command already wrote its own error line to stderr, return
  `shared.NewStderrReportedError(err)` instead so nothing is printed twice

## Dependencies

- Standard library first
- Avoid new deps unless necessary and justified

## CLI Help Output

- Use `UsageFunc` on ffcli commands for consistent help formatting
- When returning `flag.ErrHelp`, do **not** call `fs.Usage()` manually (avoids duplicate output)
- Help output is written to stderr by default

## Testing

### Error Checking

- **Do**: Use `errors.Is()` or typed extraction via `errors.AsType()` (Go 1.26+)
- **Do**: Use `errors.As()` when you need compatibility with older Go versions
- **Do**: Simply check `err != nil` when only verifying an error occurred
- **Don't**: Match error strings with `strings.Contains(err.Error(), "...")` - this is fragile

```go
// Good: Check for specific error type (Go 1.26+)
if notFoundErr, ok := errors.AsType[*NotFoundError](err); ok {
    // handle not found
    _ = notFoundErr
}

// Also valid: classic errors.As pattern
var legacyErr *NotFoundError
if errors.As(err, &legacyErr) {
    // handle not found
}

// Good: Just verify error occurred
if err == nil {
    t.Fatal("expected error, got nil")
}
t.Logf("got expected error: %v", err)

// Bad: Fragile string matching
if !strings.Contains(err.Error(), "not found") {  // DON'T DO THIS
    t.Fatal("wrong error")
}
```

### Empty Results

- Prefer empty arrays/objects over `null` when no data is returned
- CLI commands should exit `0` for empty results and render empty output cleanly
- Add tests for empty responses (including empty pagination)

### Test Isolation

- Use `t.TempDir()` for temporary files
- Use `t.ArtifactDir()` for test artifacts you want to inspect with `go test -artifacts`
- Use `t.Setenv()` to set environment variables (auto-cleaned up)
- Isolate from user config by setting `ASC_CONFIG_PATH` to a temp path; a temporary `HOME` alone is not enough, because the upward `.asc/config.json` search from a checkout inside the developer's home reaches `~/.asc/config.json`
- `make test`, `make test-short` (used by the pre-commit hook), `make test-parallel`, and `make test-coverage` unset inherited `ASC_*` variables (except the opt-in test switches `ASC_UPDATE_GOLDEN`, `ASC_SIGNING_RUN_LIVE_TEST`, and `ASC_SIGNING_KEYCHAIN_INSTALL_LIVE_TEST`) and `DO_NOT_TRACK` and point `ASC_CONFIG_PATH` at a missing file in a fresh, read-only per-run directory; tests that need any of those inputs set them with `t.Setenv`

---

### PARITY (`docs/PARITY.md`)

# Parity map

Lookup table for remaining fastlane/ASC parity after the 2026-08-30 backlog
(#2227–#2236) and the 2026-09-02 web-session issues. Command claims were
checked against a built `asc` binary's `--help`. Live inventory:
`asc capabilities` (use `--status web-session` for web-only rows). Do not
duplicate that catalog here.

Each area is exactly one of: covered by a command, tracked by issue(s), or an
intentional non-goal. Do not copy this file into `AGENTS.md`.

## Remaining areas

| Area | Resolution |
| --- | --- |
| App creation plus Developer Portal service associations as one end-to-end flow | **Intentional non-goal.** No single end-to-end command. Compose `asc web apps create` with `asc web bundle-ids`. Remaining identifier/services/iCloud work is #2290. |
| Metadata, screenshots, preview videos, binary, and submission as one operation | **Intentional non-goal.** Separate commands by design. Closest composed paths: `asc publish appstore` (IPA or local build, optional metadata, optional submit) and `asc release stage` (metadata, attach, validate). Screenshots and preview videos stay `asc screenshots` and `asc video-previews`. Chain with `asc workflow`. |
| Simulator creation, booting, app installation, and screenshot capture beyond the bounded matrix in #2230 | **Intentional non-goal for mutation.** `asc xcode test-destinations` lists simulators and the local Mac with a read-only `simctl list`. `asc screenshots run` and default iOS `asc screenshots capture` require a booted simulator and an installed app. `asc screenshots capture --provider macos` captures a running macOS app. `asc xcode install` targets a connected physical device, not a simulator. Bounded matrix capture is #2230. Create/boot/clone/delete stay on host Simulator/`simctl`. |
| Test configuration and reporting beyond the first `asc xcode test` slice (#2227) | **Intentional non-goal for extra test knobs.** First slice shipped as `asc xcode test` (`test`, `build-for-testing`, `test-without-building`, destinations, test plans, filters, result bundle, JUnit via `--report junit --report-file PATH`). An existing `.xcresult` converts with `asc xcode test junit`. Further scan-style knobs stay `--xcodebuild-flag` unless demand appears. |
| Code signing on a fresh CI runner: certificate and private-key creation, profiles, shared encrypted identity, keychain lifecycle, and Xcode signing settings | **Covered.** `asc signing fetch --create-missing --create-missing-certificate` creates the key, CSR, certificate, profile, and `.p12`; `--match-extensions` adds extension bundle IDs; `--delete-stale-profiles` removes expired or invalid profiles (`asc profiles list --stale-only` lists them). `asc signing sync push` and `pull` share the encrypted identity (Git, S3-compatible object storage, or experimental GitLab Secure Files and AWS Secrets Manager). `asc signing keychain install`, `list`, `unlock`, `lock`, `set-timeout`, `set-partition-list`, and `delete` own the dedicated keychain. `asc xcode signing plan` (with `--profile`) and `asc xcode signing apply` set per-target signing settings and `ExportOptions.plist`; `asc xcode archive` and `asc xcode export` produce the signed artifact. Sequence: [CI_CD.md](CI_CD.md#code-signing-on-a-fresh-runner). |
| Signing profile renewal, bulk cleanup, device refresh, and Mac profile inclusion | **Covered.** With Git storage, `asc signing sync push --renew-expired` replaces an expired synced profile with a same-name successor; `--force-for-new-devices` recreates a development or ad hoc profile when its devices change, and `--include-mac-in-profiles` adds enabled Apple silicon Macs to iOS development and ad hoc device sets; `asc signing sync nuke` deletes every profile of one type, revokes its certificates, and removes their encrypted files. Stale cleanup outside sync is `asc signing fetch --delete-stale-profiles`; certificates are deactivated or revoked with `asc certificates`. |
| Signing object storage | **Covered.** `asc signing sync push`, `pull`, and `rotate-password` with `--storage object --object-bucket NAME` (optional `--object-prefix`, `--object-region`, `--object-endpoint` for S3-compatible services) store the same ciphertext in the Git tree's layout, with conditional writes that never overwrite a concurrent push. Credentials come from the standard AWS chain, including workload identity; no flag accepts one. Profile renewal, device refresh, and `sync nuke` remain Git-only. CI example: [CI_CD.md](CI_CD.md#signing-assets-in-object-storage-with-workload-identity). |
| Web-session-only capabilities with no public Apple API | **Tracked by** open 2026-09-02 web-session issues: #2275, #2278, #2283, #2284, #2286, #2287, #2288, #2290, #2291, #2292, #2293, #2294, #2295, #2297, #2298, #2299, #2300, #2301. Shipped the same day (do not reopen): #2268, #2269, #2271, #2272, #2274, #2277, #2280, #2281, #2289, #2296. Shipped inventory: `asc capabilities --status web-session`. |
| Android tooling, plugin/lane runtime, chat connectors, documentation generators, obsolete actions | **Intentional non-goal.** See list below. |

## Intentional non-goals

Unless user demand appears, do not build:

- Android tooling (Play/Gradle/supply). `asc android-ios-mapping` is Apple's Android-to-iOS mapping API, not Android CI.
- Plugin/lane runtime (Fastfile plugins). `asc workflow` runs repo-local steps; it is not a plugin host.
- Chat notification connectors beyond existing `asc notify slack` (incoming webhook): Mailgun, Chatwork, IFTTT, Flock, desktop notifications.
- Documentation generators (API/doc-site generators, fastlane docs plugins). `asc docs` / `asc init` are CLI reference helpers.
- Obsolete actions Apple or fastlane have already retired.

---

### TESTING (`docs/TESTING.md`)

# Testing Guidelines

The rules for which tests to write, and which not to, are in the `Tests` section of `AGENTS.md`. This file covers patterns.

## General Principles

- Mock external API calls (`httptest` or a stub transport); only the opt-in `make test-integration` suite calls App Store Connect.
- Write the failing test first, then implement.
- Prefer a few high-signal tests over broad repetitive matrices.

## Client Endpoint Coverage

Shared request, validation, and error-decoding code is tested once. Do not add a success, validation, and API-error test to every endpoint; test only what an endpoint does differently, such as its path, query, body, or response shape.

When consolidating repetitive client tests:
- Keep grouped/table-driven coverage for repeated request wiring
- Preserve at least one representative non-empty response assertion per response family
- Do not replace all list tests with `{"data":[]}` smoke checks; assert decoded fields for at least one realistic payload
- For user-facing renderers, assert output structure or headers in addition to value presence

## Test Patterns

### Table-Driven Tests

Table-driven tests are preferred for repetitive endpoint coverage, but they should not weaken the regression signal. Group repeated limit/next-url/request-wiring cases together, then keep one focused representative assertion that proves the endpoint still decodes the correct response shape.

```go
func TestSomething(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    string
        wantErr bool
    }{
        {"valid input", "foo", "bar", false},
        {"empty input", "", "", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Something(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
            }
            if got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}
```

### Helper Functions

- Use `t.Helper()` in test helper functions
- For JSON assertions, unmarshal and assert fields (not `strings.Contains`)
- For renderer/output assertions, avoid token-only checks when structure matters; include header or formatting assertions for representative cases

### Assertions Outside the Test Goroutine

`t.Fatal`, `t.Fatalf`, and `t.FailNow` may only be called from the goroutine running the test. Inside an `httptest` handler they skip the response write and leave the client under test waiting; inside a worker goroutine or an injected dependency stub they terminate that goroutine before it can send its result, so the test deadlocks instead of failing and the real failure is buried in a timeout dump.

Use `internal/handlertest` instead. It records the failure with `Errorf`, which is safe from any goroutine, and returns a value the fixture hands back so the code under test unwinds normally:

```go
fixture := handlertest.New(t)

// RoundTrippers, dependency stubs, and worker goroutines return the error.
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
    return nil, fixture.Errorf("unexpected request: %s %s", req.Method, req.URL)
})

// httptest handlers cannot return, so answer with a 500 and stop.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    fixture.Respond(w, "unexpected request: %s %s", req.Method, req.URL.Path)
}))

// Fixture builders that must return a response answer with a 500 body.
func territoryResponse(fixture *handlertest.Asserter, payload any) *http.Response {
    body, err := json.Marshal(payload)
    if err != nil {
        return fixture.Response("marshal territory response: %v", err)
    }
    return jsonHTTPResponse(http.StatusOK, string(body))
}
```

`Errorf` renders with `fmt.Errorf`, so `%w` keeps wrapping the operand and still prints readably in the test log. Keep using `t.Fatal` for setup that runs on the test goroutine.

### CLI Tests

- Use `internal/cli/cmdtest` for command output and parsing tests
- Capture stderr for usage text (help output goes to stderr)

## Running Tests

```bash
ASC_BYPASS_KEYCHAIN=1 make test  # Run all tests
ASC_BYPASS_KEYCHAIN=1 go test -v ./...  # Verbose output
ASC_BYPASS_KEYCHAIN=1 go test -run TestName ./pkg  # Run specific test
```

Always set `ASC_BYPASS_KEYCHAIN=1` for manual test commands so host keychain profiles cannot affect results. The `make test` target also sets it internally.

---

### WINGET (`docs/WINGET.md`)

# WinGet Packaging

This document captures the Windows Package Manager packaging flow from
[GitHub Discussion #1552](https://github.com/rorkai/App-Store-Connect-CLI/discussions/1552).

The goal is to make the common install command work:

```powershell
winget install asc
```

Keep the exact package identifier documented as the scripting fallback:

```powershell
winget install --id Rorkai.ASC --exact
```

## Package Identity

Use this identity for `microsoft/winget-pkgs` submissions:

```yaml
PackageIdentifier: Rorkai.ASC
PackageName: asc
Moniker: asc
Commands:
  - asc
```

`Moniker: asc` is what lets `winget install asc` resolve to this package when
there is no ambiguity. `PackageIdentifier: Rorkai.ASC` remains the stable,
non-ambiguous form for automation.

## First Submission

WinGet packages live in the public
[`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) repository,
not in this repository. For the initial submission, create a portable package
from the Windows release asset:

```yaml
PackageIdentifier: Rorkai.ASC
PackageVersion: 1.5.1
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.6.0
```

```yaml
PackageIdentifier: Rorkai.ASC
PackageVersion: 1.5.1
PackageLocale: en-US
Publisher: Rorkai
PackageName: asc
PackageUrl: https://github.com/rorkai/App-Store-Connect-CLI
License: MIT
LicenseUrl: https://github.com/rorkai/App-Store-Connect-CLI/blob/main/LICENSE
ShortDescription: Fast, scriptable CLI for the App Store Connect API.
Moniker: asc
Tags:
  - app-store-connect
  - app-store-connect-api
  - cli
  - ios
  - testflight
ReleaseNotesUrl: https://github.com/rorkai/App-Store-Connect-CLI/releases/tag/1.5.1
ManifestType: defaultLocale
ManifestVersion: 1.6.0
```

```yaml
PackageIdentifier: Rorkai.ASC
PackageVersion: 1.5.1
InstallerType: portable
Commands:
  - asc
Installers:
  - Architecture: x64
    InstallerUrl: https://github.com/rorkai/App-Store-Connect-CLI/releases/download/1.5.1/asc_1.5.1_windows_amd64.exe
    InstallerSha256: REPLACE_WITH_RELEASE_SHA256
ManifestType: installer
ManifestVersion: 1.6.0
```

The release workflow generates these files with
`scripts/generate_winget_manifests.py` using the SHA256 from
`asc_<version>_checksums.txt`.

## Release Automation

`.github/workflows/release.yml` generates the WinGet manifests after the GitHub
release is published, in parallel with the Homebrew tap update. It then opens or
updates a PR against `microsoft/winget-pkgs`.

Required release configuration:

- `secrets.WINGET_GITHUB_TOKEN`: token that can create/use a fork of
  `microsoft/winget-pkgs`, push branches to that fork, and open PRs against
  `microsoft/winget-pkgs`.
- `vars.WINGET_FORK_OWNER`: optional organization/user that owns the fork. When
  unset, the workflow uses the authenticated token owner.

The submission step logs the token's `gh api rate_limit` quota first, then runs
every `gh` and `git` network call through a bounded retry helper (five attempts,
exponential backoff from 10s capped at 120s, with jitter). Only GitHub rate
limits and transient transport failures are retried; authentication,
permission, validation, and scope-check failures fail immediately. Clone and PR
creation are idempotent under retry, so a lost response cannot open a duplicate
`microsoft/winget-pkgs` PR.

If the `winget` job still fails after the retries, re-run that job (or dispatch
the release workflow for the same version). If the preflight showed a primary
quota (`core`, `graphql`, or `search`) at zero, wait for that bucket's logged
reset time first; for secondary throttles, 5xx responses, or transport
failures the reset times are unrelated, so re-run after a few minutes. The job
is safe to re-run: it reuses the published release artifact,
recognizes an existing `rorkai-asc-<version>` branch, and skips PR creation
when one is already open. Fall back to a manual submission only when a re-run
fails for a non-transient reason.

The generated branch name is:

```text
rorkai-asc-<version>
```

The manifest path in `winget-pkgs` is:

```text
manifests/r/Rorkai/ASC/<version>/
```

To generate manifests locally for a release asset directory:

```bash
python3 scripts/generate_winget_manifests.py \
  --version 1.5.1 \
  --release-dir release \
  --output-dir /tmp/asc-winget
```

## Acceptance Check

After the manifest PR is merged and WinGet sources update, run the manual
`WinGet smoke` workflow or use a clean Windows machine:

```powershell
winget source update
winget search asc --source winget
winget install asc --source winget --accept-source-agreements --accept-package-agreements
asc --help
asc version
winget uninstall --id Rorkai.ASC --exact
```

If `winget install asc` prompts for disambiguation, keep the README fallback as
the documented automation-safe command:

```powershell
winget install --id Rorkai.ASC --exact
```

---

### WORKFLOWS (`docs/WORKFLOWS.md`)

# Workflow Patterns

Use the high-level workflow surfaces deliberately:

- `asc publish appstore`: canonical App Store shipping path
- `asc publish testflight`: canonical high-level TestFlight publish path
- `asc workflow`: user-defined orchestration for repo-specific pipelines

To include store assets in a canonical metadata round-trip, select the asset scopes explicitly:

```bash
asc metadata pull --app "APP_ID" --version "1.2.3" --dir ./metadata --include localizations,app-clip,previews
asc metadata push --app "APP_ID" --version "1.2.3" --dir ./metadata --include localizations,app-clip,previews --dry-run
asc metadata push --app "APP_ID" --version "1.2.3" --dir ./metadata --include localizations,app-clip,previews --confirm
```

App Clip files are stored at `app_clip/action.txt` and `<locale>/app_clip/` beneath
`--dir`; previews use `app_previews/<locale>/<device_type>/`. `asc migrate` uses the
same App Clip layout beneath its resolved `metadata/` directory and previews
beneath the selected Fastlane directory. Existing JSON and Fastlane text metadata
formats are unchanged. Preview validation needs `ffprobe` on `PATH`. Exported media
is Apple's delivered rendition, which may differ from the original upload bytes;
previews are exported as `.m3u8` playlists that only reference existing remote
previews; importing them where that preview does not exist requires the original videos.

`asc workflow` lets you compose existing `asc` commands and shell commands into
repeatable release pipelines once you know which top-level path you want.

## Verified local Xcode -> TestFlight workflow

This pattern was validated against a real app using:

- `asc builds next-build-number` to choose the next build number for a version
- `asc xcode inject` to materialize deployment metadata into generated Xcode
  plist/config files and asset paths before archiving
- `asc xcode build` to compile a scheme for an explicit simulator or device
  destination before the archive/export path
- `asc xcode archive` to create a deterministic `.xcarchive`
- `asc xcode export` to create a deterministic `.ipa`
- `asc publish testflight --group ... --wait` to upload, wait for processing,
  and add the build to a TestFlight group
- `--submit --confirm` on `asc publish testflight` when the target is an
  external group that should trigger beta app review submission

For a simulator compile check without modifying project files, use the typed
destination and explicit no-signing flag. For that invocation,
`--no-code-signing` overrides signing with `CODE_SIGNING_ALLOWED=NO`. Derived
data defaults to a stable asc cache path outside the checkout; use
`--derived-data-path` when the workflow needs a specific artifact directory.
Use `--result-bundle-path` to produce a new `.xcresult` bundle at an explicit
path; the destination must not already exist and asc does not overwrite it.

Replace the example destination below with a simulator installed on the host.

```bash
asc xcode build \
  --project App.xcodeproj \
  --scheme App \
  --configuration Debug \
  --destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=27.0' \
  --no-code-signing \
  --output json
```

For local unit or UI test execution, use the typed test command.
It keeps Xcode diagnostics on stderr, writes a structured result to stdout, and
retains the `.xcresult` bundle for inspection:

```bash
asc xcode test \
  --project App.xcodeproj \
  --scheme App \
  --destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=27.0' \
  --result-bundle-path .asc/artifacts/App-tests.xcresult \
  --output json
```

Use `--action build-for-testing` to produce test products, then pass the safely
discovered `.xctestrun` path to `--action test-without-building`. Each action
requires an explicit `--destination`; Xcode may boot or launch the selected
simulator or device. The command does not change project files, upload
artifacts, or call App Store Connect.

Device builds retain Xcode's signing behavior unless `--no-code-signing` is
provided explicitly.

`asc xcode export` generates archive-specific App Store Connect export options
automatically when `--export-options` is omitted. It chooses a unique
archive-adjacent path and never overwrites an existing file. The command result
reports the exact path used. To persist a plist at a deterministic path for
inspection or reuse, generate it after the archive:

```bash
asc xcode export-options generate \
  --archive-path .asc/artifacts/App.xcarchive \
  --output-path .asc/export-options-app-store.plist \
  --overwrite
```

The standalone command defaults to `.asc/export-options-app-store.plist` and
requires `--overwrite` if that file already exists. Automatic generation is
cross-platform and only reads archive metadata. Manual signing resolution is
Darwin-only because it inspects local Xcode signing identities and provisioning
profiles.

To export an IPA for registered devices, use the modern Xcode
method name `release-testing` (the older `ad-hoc` spelling is deprecated):

```bash
asc xcode export \
  --archive-path .asc/artifacts/App.xcarchive \
  --ipa-path .asc/artifacts/App.ipa \
  --method release-testing \
  --signing-style manual \
  --team-id TEAM_ID
```

The default remains `app-store-connect`; release-testing always produces a
local export and cannot be combined with `--wait`.

For a PCC-capable app, or another multi-target app that needs manual signing,
pass the signing policy directly to export. ASC reads the archive and matches
installed profiles for the app and its embedded targets, so the command does
not need profile UUID flags or a checked-in plist:

```bash
asc xcode export \
  --archive-path .asc/artifacts/App.xcarchive \
  --ipa-path .asc/artifacts/App.ipa \
  --signing-style manual \
  --team-id TEAM_ID
```

The same flags work when the archive and export are owned by a local publish
flow:

```bash
asc publish testflight \
  --app APP_ID \
  --workspace App.xcworkspace \
  --scheme App \
  --version 1.2.3 \
  --group GROUP_ID \
  --signing-style manual \
  --team-id TEAM_ID
```

An explicit `--export-options` plist cannot be combined with `--method`,
`--signing-style`, or `--team-id`. When supplied without those flags, the
plist is authoritative.

Create `.asc/deployment.json`:

```json
{
  "values": {
    "bundle_id": "com.example.app",
    "app_name": "Example",
    "version": "",
    "build_number": ""
  },
  "outputs": [
    {
      "type": "plist",
      "path": "../Generated/Info.generated.plist",
      "values": {
        "CFBundleIdentifier": "${bundle_id}",
        "CFBundleDisplayName": "${app_name}",
        "CFBundleShortVersionString": "${version}",
        "CFBundleVersion": "${build_number}"
      }
    },
    {
      "type": "text",
      "path": "../Generated/Deployment.xcconfig",
      "contents": "PRODUCT_BUNDLE_IDENTIFIER = ${bundle_id}\nMARKETING_VERSION = ${version}\nCURRENT_PROJECT_VERSION = ${build_number}\n"
    },
    {
      "type": "copy",
      "source": "../Assets/AppIcon.appiconset/Contents.json",
      "path": "../Generated/Assets.xcassets/AppIcon.appiconset/Contents.json"
    }
  ]
}
```

Point your Xcode project at generated files such as `Generated/Info.generated.plist`
or include `Generated/Deployment.xcconfig` from the build configuration. Then run
`asc xcode inject` before archive time to fill in the release-specific values
that previously came from Fastlane scripts.

Create `.asc/workflow.json`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run it:

```bash
asc workflow validate --output json
asc workflow run --dry-run testflight_beta VERSION:1.2.3
asc workflow run testflight_beta VERSION:1.2.3
```

### Resumable upload and distribution steps

When upload and external distribution need separate retry boundaries, the
upload-only flag can make the upload its own output-producing
step. The successful upload step is persisted with `BUILD_ID`; if the later
processing wait or distribution step fails, `--resume` skips the upload and
reuses that exact build ID.

```json
{
  "env": {
    "APP_ID": "1234567890",
    "IPA_PATH": ".asc/artifacts/App.ipa",
    "TESTFLIGHT_GROUP": "External Testers"
  },
  "workflows": {
    "testflight_external": {
      "steps": [
        {
          "name": "upload",
          "run": "asc publish testflight --app \"$APP_ID\" --ipa \"$IPA_PATH\" --upload-only --output json",
          "outputs": {
            "BUILD_ID": "$.buildId",
            "BUILD_VERSION": "$.buildVersion",
            "BUILD_NUMBER": "$.buildNumber"
          }
        },
        {
          "name": "wait",
          "run": "asc builds wait --build-id ${steps.upload.BUILD_ID} --fail-on-invalid --output json"
        },
        {
          "name": "distribute",
          "run": "asc builds add-groups --build-id ${steps.upload.BUILD_ID} --group \"$TESTFLIGHT_GROUP\" --submit --confirm --output json"
        }
      ]
    }
  }
}
```

After a failed wait or distribution step, use the run ID printed by
`asc workflow`:

```bash
asc workflow run testflight_external --resume RUN_ID
```

The upload step is not executed again because its declared outputs were already
persisted in the run-state file.

Notes:

- `VERSION` must be a valid next marketing version for your app. If the latest
  App Store version is already `READY_FOR_DISTRIBUTION`, reusing that same
  version can cause App Store Connect to reject the upload.
- `TESTFLIGHT_GROUP` accepts either a beta group name or group ID.
- Add `"ASC_BYPASS_KEYCHAIN": "1"` to the top-level `env` block if you want the
  workflow to resolve credentials from environment variables or config instead
  of the macOS keychain.
- Output-producing step names only need to stay unique within workflows that
  can execute together in the same run graph. Independent workflows can reuse
  names like `archive` or `publish`.
- Declared outputs keep the exact JSON value the command printed. A numeric
  `$.nextBuildNumber` of `42` is stored and interpolated as `42`, so it can be
  passed straight to `CURRENT_PROJECT_VERSION` or another build-number flag.

## Bounded retry and timeout

Long-form `run` steps can opt into a fixed-delay retry policy and a per-attempt
timeout. This replaces shell retry loops for operations such as Apple's
eventually consistent build-to-beta-group relationship:

```jsonc
{
  "name": "add_build_to_group",
  "run": "asc builds add-groups --build-id $BUILD_ID --group $GROUP_ID",
  "retry": {
    "max_attempts": 6,
    "delay": "10s"
  },
  "timeout": "2m"
}
```

`max_attempts` includes the initial execution and must be between 2 and 100.
`delay` and `timeout` use positive Go duration strings up to `24h`; examples
include `250ms`, `10s`, and `2m`. The delay is fixed and has no jitter. Timeout
applies separately to each attempt. With the example above, the total policy is
bounded by six two-minute attempts plus five ten-second delays.

Retry and timeout are supported only on `run` steps. Workflow-call steps and the
`before_all`, `after_all`, and `error` string hooks remain single-execution.
Caller cancellation stops a running process tree or retry delay immediately.
The error hook runs only after the final attempt fails; `after_all` still runs
only after success.

Use retry only when you have explicitly decided the command is safe to repeat.
The runner does not infer whether a shell command is read-only, idempotent, or a
mutation. A successful command with invalid declared output is not retried,
because its side effect may already have happened. That failure is terminal:
the structured result sets `terminal: true`, the run state records the terminal
reason, and `--resume` rejects the run instead of executing the command again.
A timeout-only failure is terminal for the same replay-safety reason: local
termination cannot prove that a remote mutation was not accepted. A step that
configures both `retry` and `timeout` remains resumable because `retry` is the
explicit repeat-safety opt-in.
Failed-attempt diagnostics from `timeout` alone do not create a resume checkpoint.
Recovery requires a previously successful step or hook, or a retry-enabled
failed step. Omit `retry` or `timeout` to disable it; explicit `null` is invalid.

Attempt numbers and retry delays are written to stderr. stdout remains the one
machine-readable workflow result. Each attempt gets a fresh output buffer, and
declared outputs are extracted and persisted only from the successful attempt.
Persisted attempt diagnostics remain available across `--resume`; already
successful steps are never rerun.

Example:

```json
{
  "workflows": {
    "testflight_beta": {
      "steps": [
        {
          "name": "archive",
          "run": "printf '{\"buildId\":\"beta\"}'",
          "outputs": {
            "BUILD_ID": "$.buildId"
          }
        }
      ]
    },
    "appstore_release": {
      "steps": [
        {
          "name": "archive",
          "run": "printf '{\"buildId\":\"release\"}'",
          "outputs": {
            "BUILD_ID": "$.buildId"
          }
        }
      ]
    }
  }
}
```

This is valid because those workflows are independent. If a third workflow can
call both of them in the same run, the duplicate `archive` producers still need
to be renamed.

---

### Game Center Blocked Players (`docs/game-center-blocked-players.md`)

# Game Center blocked players

API 4.5 adds a blocked-player collection for a Game Center detail and an endpoint
for updating a player's blocked status.

```bash
asc game-center details blocked-players list --detail-id "DETAIL_ID" --paginate
asc game-center details blocked-players update --id "PLAYER_ID" --blocked true --confirm
asc game-center details blocked-players update --id "PLAYER_ID" --blocked false --confirm
```

The list contains only blocked players. It is not a directory of every player.
Resolve the Game Center detail ID with `asc game-center details list --app
"APP_ID"`. Player IDs for a new block can come from a submitted score's `player`
relationship. Apple does not expose a standalone player lookup endpoint in this
API version.

Lists support `--fields nickname,blocked,bundleId`, `--limit` (1–200), `--next`,
and `--paginate`. A continuation URL cannot be combined with a detail ID, fields,
or limit because it already specifies the complete query. Each page gets a fresh
request timeout.

Updates require an explicit `--blocked true|false` and `--confirm`. An optional
`--bundle-id` sends Apple's `bundleId` update attribute; omitting it leaves that
attribute unchanged. An explicitly empty bundle ID is rejected. JSON retains
the resource envelope, including explicit false and empty nickname values.
Table and Markdown output show the ID, nickname, blocked status, and bundle ID.

The ID-only blocked-player relationship endpoint remains accessible with
`asc api`.

---

### Game Center Score Moderation (`docs/game-center-score-moderation.md`)

# Game Center score moderation

API 4.5 adds score moderation for v2 leaderboards. List the submitted scores and
include their player relationships:

```bash
asc game-center leaderboards v2 score-moderations list --leaderboard-id "LEADERBOARD_ID" --include player --paginate
```

The list supports `--limit` (1–200), `--next`, `--fields`, and `--player-fields`.
`--player-fields` requires `--include player`. Including a player automatically
retains the `player` relationship in an explicit score field selection.
`--exists-blocked true|false` forwards Apple's `exists[blocked]` filter.
A `--next` URL supplies its complete query, so it cannot be combined with the
leaderboard ID, filters, includes, fields, or limit.

Block or unblock a score by its moderation ID:

```bash
asc game-center leaderboards v2 score-moderations update --id "SCORE_ID" --blocked true --confirm
asc game-center leaderboards v2 score-moderations update --id "SCORE_ID" --blocked false --confirm
```

Both updates require `--confirm`. This changes an individual score's blocked
status. It does not block the player. JSON retains Apple's resource envelope,
player relationships, included resources, and numeric score strings without
converting them to floating-point numbers. Table and Markdown output summarize
ID, rank, score, blocked status, and submission date.

---

### Linux Ios (`docs/linux-ios.md`)

# Build and package iOS apps on Linux

ASC's portable workflow uses an installed Swift/Apple SDK toolchain, `xtool`,
and `rcodesign`. The tested xtool backend is
[joshuaswarren/xtool at f0a1f90](https://github.com/joshuaswarren/xtool/tree/f0a1f90efdbb0dc023e276ff529da92618da7a87);
it must replace its app output directory on a successful build so ASC can reject
stale output. It does not install these tools or provide an iOS simulator
runtime. The initial backend supports one thin arm64 iOS app plus resources;
nested executable code, extensions, embedded frameworks, watch apps, App Clips,
and app symlinks are rejected.

The installed xtool backend owns SDK maintenance. Before compiling, it can
automatically rebuild and replace an outdated normal SDK using its existing
Xcode copy. Pin a compatible xtool/SDK pair to avoid that update; a prepared
Swift package does not isolate or freeze the installed SDK.

## User-supplied tooling and SDK licensing

ASC provides a local integration with tools installed by the user. It does not
bundle, download, or redistribute Xcode, Apple SDKs, or Swift toolchains, and it
does not operate a hosted build service. Users must obtain and configure their
own toolchain and signing materials and are responsible for reviewing and
complying with the agreements applicable to their account and build environment.

This follows the tooling boundary described by xtool's author,
[Kabir Oberai](https://forums.swift.org/t/xtool-cross-platform-xcode-replacement-build-ios-apps-on-linux-and-more/79803/3):
xtool does not distribute Apple's SDKs or toolchains, asks users to supply Xcode,
and leaves license compliance to users. He also suggests using macOS CI for the
final App Store build when concerned about a Linux build environment. Operators
can use a macOS/Xcode workflow for their final distribution build.

Providing an integration does not establish permission to use an SDK in every
environment. Apple's standard
[Developer Program agreement](https://developer.apple.com/support/terms/apple-developer-program-license-agreement/)
§2.6 restricts Apple SDK use on non-Apple computers and enabling others to do so.
The [Xcode/SDK agreement](https://www.apple.com/legal/sla/docs/xcode.pdf) authorizes
execution on Apple hardware running macOS. No applicable exception or written
Apple permission has been verified. User-supplied tooling does not waive these
terms or establish Apple approval of this workflow. Technical verification does
not establish licensing compliance or App Store acceptance.

For an Xcode project, first prepare a Swift package using the converter from
[omarchy-apple-dev](https://github.com/joshuaswarren/omarchy-apple-dev):

```sh
python3 /opt/omarchy-apple-dev/tools/xcodeproj2xtool.py App.xcodeproj --target App
```

The converter creates `omarchy-xtool/`. Inspect its generated Package.swift,
xtool.yml, Info.plist, dependencies, and resource handling. Xcode schemes,
workspaces, build settings, and arbitrary scripts are not interchangeable with
Swift package targets. Keep the Apple SDK/toolchain version pinned.

Build a release device app:

```sh
asc builds compile --package-path ./omarchy-xtool --product App \
  --platform device --configuration release --app-path ./artifacts/App.app
```

Package it with an existing local identity and provisioning profile:

```sh
asc builds package --app-path ./artifacts/App.app --ipa-path ./artifacts/App.ipa \
  --identity ./signing/App.p12 --identity-password-file ./secrets/password \
  --provisioning-profile ./signing/App.mobileprovision --entitlements ./signing/entitlements.plist
```

The profile must be signed by Apple's trusted provisioning signer, unexpired,
and match the exact app bundle ID and supplied signing certificate. Requested
entitlements must be authorized by that profile. Optional capabilities are not
automatically copied from the profile or an existing code signature. ASC passes
the validated identity and password to rcodesign through pipes and never writes
them or the decrypted private key to disk; the password file must contain a
single line. Signing with an identity requires Linux or macOS. This
command packages fresh build output; use `asc signing resign` on macOS for its
existing complete IPA re-signing contract.

For a local pipeline test without an Apple identity:

```sh
asc builds package --app-path ./artifacts/App.app --ipa-path ./artifacts/Test.ipa --ad-hoc
```

Ad hoc test output is not accepted by Apple and cannot be installed on an
ordinary physical iPhone. A simulator app is a separate build target:

```sh
asc builds compile --package-path ./omarchy-xtool --product App \
  --platform simulator --configuration debug --app-path ./artifacts/Simulator.app
```

Simulator output gets corrected platform metadata and a Linux-created ad hoc
signature. A Mac iOS simulator host is still needed to install and run it. A
simulator executable cannot be packaged as a device IPA.

Compiler and signer diagnostics go to stderr. JSON (or explicit table/Markdown)
receipts go to stdout. Outputs are create-only; source apps are copied into
private staging before signing. Inspect a reported incomplete app publication
before retrying. Existing Xcode archive/export/resign behavior is unchanged.

Only one `asc builds compile` may use a prepared package at a time, including
different products or configurations. A competing invocation fails before
starting xtool. The package's `.asc-ios-build.lock` file remains after completion;
its presence alone does not mean a build is running. Avoid running xtool directly
against the same package while ASC is compiling it.

On macOS and Linux, signing inputs must belong to the current user and have one
hard link. Identity and password files must not grant group or other access;
profiles and entitlements must not be group- or other-writable.

Device compilation reports `signingType: unknown`: xtool may apply an ad hoc
signature for entitlements in its product configuration, and ASC does not
determine the device output's signing type during compilation. Package with
explicit signing inputs before distribution. Simulator compilation reports
`adHoc` after its explicit rcodesign step.

`signatureVerified: false` is intentional: rcodesign completion is not Apple's
complete bundle signature verification. `profileValidated: true` reports the
specific profile/identity/entitlement authorization checks, not App Store
acceptance. Apple metadata requirements, assets, capabilities, SDK stamping,
debug symbols, and project compatibility still need validation before shipping.

Upload a genuinely signed, prepared IPA as a separate authorized step:

```sh
asc builds upload --app APP_ID --ipa ./artifacts/App.ipa --wait
```

Neither portable command discovers Apple credentials, creates certificates or
profiles, invokes upstream ship.sh, or uploads anything. rcodesign configuration
is disabled and timestamp requests are disabled for local signing.

Child tools get an allowlisted environment. xtool receives process, locale,
proxy, and toolchain settings (`PATH`, `HOME`, `TMPDIR`, `LANG`, `LC_*`, `XDG_*`,
`XTL_*`, `XTOOL_*`, `SWIFT_*`, `SWIFTPM_*`, `SDKROOT`, `DEVELOPER_DIR`,
`TOOLCHAINS`, proxy and TLS certificate variables, `SSH_AUTH_SOCK`); rcodesign
receives only `PATH`, `HOME`, temporary-directory, and locale settings. Other
variables, including `ASC_*` credentials and cloud or GitHub tokens, are not
passed.

---

### Release Notes App Info Kids Age Band (`docs/release-notes-app-info-kids-age-band.md`)

# AppInfo kidsAgeBand deprecation

API 4.5 removed `kidsAgeBand` from the `AppInfo` response schema. AppInfo sparse
selectors now emit one deprecation warning on stderr per invocation. Existing
flags and HTTP query forwarding remain available for compatibility, and JSON
stdout is unchanged. Apple errors are still reported normally.

The warning applies to `apps`/`apps list`, `apps view`, `apps info list`,
`apps info view`, `localizations list --type app-info`, and
`xcode-cloud products app` when they select AppInfo `kidsAgeBand`. Supported
continuation URLs carrying that selector also warn.

Replace reads such as:

```bash
asc apps info view --info-id "APP_INFO_ID" --fields kidsAgeBand
```

with:

```bash
asc age-rating view --app-info-id "APP_INFO_ID"
```

`AgeRatingDeclaration.kidsAgeBand` remains in API 4.5, and the age-rating
`--kids-age-band` edit flag is unchanged. This release does not remove any
stable flag or announce a removal date.

---

### Release Notes App Tag Territories (`docs/release-notes-app-tag-territories.md`)

# App-tag territory deprecation

Apple's [API 4.5 release notes](https://developer.apple.com/documentation/appstoreconnectapi/app-store-connect-api-4-5-release-notes)
deprecate the app-tag `territories` relationship and the full-resource and
ID-only territory endpoints. They also retire territory-related list parameters,
although the published OpenAPI snapshot still exposes them.

The CLI retains existing flags, HTTP requests, and pagination for compatibility.
Territory selections on `app-tags list`/`view`, the `territories` and
`territories-links` commands, and territory parameters in continuation URLs now
emit one stderr warning per invocation. JSON stdout and API errors are unchanged.
No stable command is removed and no removal date is announced.

For core tag reads, omit territory selections:

```bash
asc app-tags list --app "APP_ID" --fields name,visibleInAppStore
asc app-tags view --app "APP_ID" --id "TAG_ID"
```

Remove `territories` from `--fields` and omit `--include territories`,
`--territory-fields`, and `--territory-limit`. Apple does not name an equivalent
replacement for territory lookups. `app-tags links` and tag visibility updates
remain warning-free.

---

### Release Notes Apple Ads Platform V1 (`docs/release-notes-apple-ads-platform-v1.md`)

# Apple Ads Platform API v1 and v5 deprecation

Release 4.4.0 makes Apple Ads Platform API v1 the direct `asc ads` resource
surface. Campaign Management API v5 remains runnable under `asc ads v5`, but
Apple retires that API on January 26, 2027 and every v5 leaf now prints a dated
deprecation warning on stderr.

Existing scripts that must keep their v5 endpoint, payload, IDs, response, and
organization context need to add `v5` to the command path:

| Before 4.4.0 | 4.4.0 compatibility path |
| --- | --- |
| `asc ads campaigns list --org ORG_ID` | `asc ads v5 campaigns list --org ORG_ID` |
| `asc ads reports campaigns --org ORG_ID --file report.json` | `asc ads v5 reports campaigns --org ORG_ID --file report.json` |
| `asc ads reports preset --level ads --org ORG_ID` | `asc ads v5 reports preset --level ads --org ORG_ID` |
| `asc ads api request --path v5/me` | `asc ads v5 api request --path v5/me` |

New automation should use the direct v1 commands. V1 account-scoped requests
take `--ad-account` or `ASC_ADS_AD_ACCOUNT_ID`; they don't substitute the v5
`--org` value. Query and report bodies also use v1 schemas, so the CLI doesn't
translate old selectors or reporting requests:

```bash
asc ads campaigns find \
  --ad-account "AD_ACCOUNT_ID" \
  --file query.json \
  --output json

asc ads reports apps campaigns \
  --ad-account "AD_ACCOUNT_ID" \
  --file report.json \
  --output json
```

Named Ads profiles keep their organization and ad-account contexts separate.
After upgrading, a selected profile won't inherit either value from the root
Ads config. If an older named profile depended on the root `ads.org_id`, store
its organization again with `asc ads auth login --name NAME --org ORG_ID`, or
pass `--org` on its v5 command. Profile-less access-token and environment auth
can still use the root context.

`asc ads auth discover` now calls Platform API v1 `GET /v1/me` and
`GET /v1/acls`. Use `asc ads me view` and `asc ads acls list` when a script
needs one response at a time.

Seven v5 leaves have no one-command v1 replacement: product-page countries,
product-page devices, three keyword bulk-delete operations, and custom
impression-share report list and view. Their warnings explain the manual
migration path. Run the matching command with `--help` before removing the v5
namespace from an existing script.

---

### Release Notes Auth Login Config Path (`docs/release-notes-auth-login-config-path.md`)

# Keychain-bypassing login writes the ASC_CONFIG_PATH file

Changed in 5.9.1. When `ASC_CONFIG_PATH` is set, `asc auth login` with
`--bypass-keychain` or a truthy `ASC_BYPASS_KEYCHAIN` now stores credentials in
that file. Earlier releases wrote `~/.asc/config.json` instead, even though every
command reads only the `ASC_CONFIG_PATH` file when it is set. The login reported
success, but later commands in the same environment could not find the profile,
and the credentials landed in the global config the variable was meant to
isolate from.

`asc auth init` follows the same rule: without `--local`, it writes its template
to the `ASC_CONFIG_PATH` file when set.

Behavior without `ASC_CONFIG_PATH` is unchanged: a keychain-bypassing login and
`asc auth init` write `~/.asc/config.json`, and `--local` writes
`./.asc/config.json`. Keychain logins are unchanged.

An explicit `--local` still writes `./.asc/config.json` when `ASC_CONFIG_PATH`
is set. Because later commands read only the `ASC_CONFIG_PATH` file, the command
now prints a warning to stderr:

```text
Warning: ASC_CONFIG_PATH is set, so other commands read /path/to/config.json instead of /repo/.asc/config.json; unset ASC_CONFIG_PATH or point it at /repo/.asc/config.json to use this file.
```

## Migration

If a script set `ASC_CONFIG_PATH`, logged in with the keychain bypassed, and then
read the credentials from `~/.asc/config.json` with `ASC_CONFIG_PATH` unset, run
that login with `ASC_CONFIG_PATH` unset. Credentials an earlier release wrote to
`~/.asc/config.json` stay there; check that file and remove any profile you did
not mean to keep.

---

### Release Notes Auth Logout Config Scope (`docs/release-notes-auth-logout-config-scope.md`)

# Auth logout respects ASC_CONFIG_PATH

Changed in 5.9.1. When `ASC_CONFIG_PATH` is set, `asc auth logout` (with
`--name`, `--all`, or neither) removes credentials only from that file and the
keychain. Earlier releases also removed matching credentials from
`~/.asc/config.json`, even though no command reads that file while
`ASC_CONFIG_PATH` is set. As a result, `asc auth logout --all --confirm` in an
isolated CI job, sandbox, agent, or test run also deleted the developer's own
global credentials.

A keychain login that replaces a profile follows the same rule: while
`ASC_CONFIG_PATH` is set, it removes the replaced profile's config entry only
from that file.

Keychain cleanup is unchanged, because the keychain is not scoped by
`ASC_CONFIG_PATH`. Behavior without `ASC_CONFIG_PATH` is unchanged: logout
removes matching credentials from the active config file and
`~/.asc/config.json`.

When logout leaves matching credentials in `~/.asc/config.json`, it says so on
stderr instead of removing them silently:

```text
Warning: ASC_CONFIG_PATH is set, so auth logout did not change /Users/you/.asc/config.json, which still holds credentials named 'MyKey'; to remove them, run auth logout with ASC_CONFIG_PATH unset or pass --include-global.
```

The command still exits 0 and prints the same stdout as before. If
`--name` matches a profile only in `~/.asc/config.json`, nothing in scope
matches, so logout prints the warning and then fails with the existing
not-found error (exit 1), where earlier releases removed the global profile.
The same not-found error now applies when the active config file does not
exist; earlier releases reported success there without removing anything.

## Migration

To keep the earlier cross-file cleanup, pass the new `--include-global` flag:

```bash
asc auth logout --all --include-global --confirm
asc auth logout --name "MyKey" --include-global --confirm
```

Or run logout with `ASC_CONFIG_PATH` unset, which cleans the active config file
and `~/.asc/config.json` as before.

Releases before 5.9.1 could write a keychain-bypassing login to
`~/.asc/config.json` even when `ASC_CONFIG_PATH` was set (see
`docs/release-notes-auth-login-config-path.md`). The earlier logout behavior
also removed those misplaced profiles. If you relied on that, check the warning
output and use `--include-global` once to remove them.

---

### Release Notes Auth Logout Confirmation (`docs/release-notes-auth-logout-confirmation.md`)

# Auth logout confirmation migration

> Status: the compatibility window described below closed in 5.0.0. `asc auth
> logout` without `--confirm` is now a usage error before any credential is
> removed. See `migrate-to-5-0.mdx`.

`asc auth logout` now accepts `--confirm` so scripts can migrate before
confirmation becomes mandatory in 5.0.0. New invocations should name the target
explicitly:

```bash
asc auth logout --name "PROFILE" --confirm
asc auth logout --all --confirm
```

During the compatibility window, existing logout calls still remove credentials
but print this warning to stderr:

```text
Warning: auth logout without --confirm is deprecated and will be rejected in 5.0.0; pass --confirm to acknowledge credential removal.
```

Bare `asc auth logout` continues to target all stored App Store Connect
credentials. Stray positional arguments and an explicit `--confirm=false` are
now rejected before any credential store is changed.

---

### Release Notes Auth Logout Keychain Bypass (`docs/release-notes-auth-logout-keychain-bypass.md`)

# Auth logout respects ASC_BYPASS_KEYCHAIN

Changed in 5.10.1. When `ASC_BYPASS_KEYCHAIN` is set to 1/true/yes/on,
`asc auth logout --all` (and `--all --include-global`, or logout with no
`--name`) removes credentials only from config files. Earlier releases also
deleted every `asc` credential from the system keychain, even though every
other auth command skips the keychain while the variable is set. As a result,
`asc auth logout --all --confirm` in a CI job, sandbox, agent, or test run that
set `ASC_BYPASS_KEYCHAIN=1` deleted the developer's own keychain credentials.

`asc auth logout --name` already skipped the keychain under the bypass and is
unchanged. Behavior without `ASC_BYPASS_KEYCHAIN` is unchanged.

While the bypass is set, logout says so on stderr:

```text
Note: ASC_BYPASS_KEYCHAIN is set, so auth logout changes config files only and leaves keychain entries untouched.
```

The command still exits 0 and prints the same stdout as before. To remove
keychain credentials, run `asc auth logout` with `ASC_BYPASS_KEYCHAIN` unset.

`asc ads auth logout` and `asc storekit auth logout` keep their own variables,
`ASC_ADS_BYPASS_KEYCHAIN` and `ASC_STOREKIT_BYPASS_KEYCHAIN`, which already
applied to every keychain operation in those stores.

---

### Release Notes Beta Groups Server Side Filtering (`docs/release-notes-beta-groups-server-side-filtering.md`)

# TestFlight beta groups server-side filtering release note

`asc testflight groups list --app APP_ID --internal` and `--external` are now
filtered by App Store Connect instead of in the CLI. `GET /v1/apps/{id}/betaGroups`
accepts only `limit` and `fields[betaGroups]`, so the command previously fetched every page and
filtered the aggregate in Go. Those requests now go to `GET /v1/betaGroups`
with `filter[app]` and `filter[isInternalGroup]`, while retaining the complete
multi-page result.

Two new flags cover query parameters that endpoint already
documents: `--name` filters on the exact group name (`filter[name]`) and
`--sort` accepts `name`, `-name`, `createdDate`, `-createdDate`, `publicLinkEnabled`,
`-publicLinkEnabled`, `publicLinkLimit`, and `-publicLinkLimit`. An unsupported
`--sort` value is a usage error listing the valid values.

## Pagination compatibility

App-scoped `--internal` and `--external` listings continue to collect every
matching page automatically when `--name` and `--sort` are absent, so existing
invocations retain complete output:

```bash
asc testflight groups list --app APP_ID --internal
```

Combined app-scoped filters, including `--internal --name NAME`, follow the
standard one-page default; add `--paginate` to collect all matching pages.
Listings that use only the new `--name` or `--sort` flags behave the same way.
Global listings are unchanged and also require `--paginate` for aggregation.

For the stable app-scoped `--internal` and `--external` paths, `--limit` remains
a cap on the final aggregate after every page is fetched. Without explicit
`--paginate`, the aggregate fetch uses the maximum page size of 200 regardless
of `--limit`, then applies the same final cap. Explicit `--paginate` uses the
same page size of 200. Other filtered and global listings retain their standard
page-size semantics for `--limit`.

`--next` now rejects `--internal`, `--external`, `--name`, and `--sort`. A
`links.next` URL is followed verbatim and already carries the query it came
from, so those flags were previously accepted and discarded. Drop them from the
follow-up call; a bare `--next`, with or without `--paginate`, is unchanged.

Unfiltered app-scoped listing (`--app APP_ID` with no filter or sort) still uses
`GET /v1/apps/{id}/betaGroups` and is unchanged.

---

### Release Notes Build Id Flag (`docs/release-notes-build-id-flag.md`)

# Canonical `--build-id` selector

> Status: the hidden `--build` compatibility alias described below was removed
> in 5.0.0 on every command in the table (and on `asc versions attach-build`).
> `--build` is now an unknown flag; use `--build-id`. See `migrate-to-5-0.mdx`.

Release 4.7.0 standardizes the build-ID selector across the remaining command
paths that previously used `--build`. New automation should use
`--build-id`; it is now the documented flag and appears in command help.

The affected commands are:

| Command | Build-ID use |
| --- | --- |
| `asc review submit` | Build to attach to the review submission |
| `asc publish testflight` | Existing build to distribute |
| `asc validate testflight` | Build to validate |
| `asc release stage` | Build to attach during staging |
| `asc build-localizations list` | Build whose localizations are listed |
| `asc build-localizations create` | Build whose localization is created |
| `asc build-bundles list` | Build whose bundles are listed |
| `asc testflight crashes list` | Build-ID filter |
| `asc testflight feedback list` | Build-ID filter |
| `asc performance metrics view` | Build whose metrics are viewed |
| `asc performance diagnostics list` | Build-ID filter |
| `asc performance download` | Build whose diagnostics are downloaded |
| `asc encryption declarations list` | Build-ID filter |
| `asc encryption declarations assign-builds` | Build IDs to assign |
| `asc apps app-encryption-declarations list` | Build-ID filter |

## Compatibility window

Existing scripts may continue to pass `--build` during the 4.x deprecation
window. The flag is hidden from help, retains its existing behavior, and emits
one migration warning on stderr pointing to `--build-id`; stdout and API
semantics remain unchanged. Supplying both spellings with different values is
a usage error before any API side effect, while identical values continue with
the deprecation warning.

Migrate automation to `--build-id` before the next major release, 5.0.0,
where deprecated compatibility flags may be removed under the CLI stability
ladder.

---

### Release Notes Game Center Replacement Confirmation (`docs/release-notes-game-center-replacement-confirmation.md`)

# Game Center relationship replacement confirmation

> Status: the compatibility window described below closed in 5.0.0. The
> relationship setters now reject invocations without `--confirm` before any
> request. See `migrate-to-5-0.mdx`.

Game Center relationship setters now accept an `--confirm` flag.
Invocations that omit it continue to work in the 4.x compatibility window and
emit a deprecation warning. Starting in 5.0.0, `--confirm` will be required to
acknowledge that these setters replace the complete relationship collection and
may remove existing members.

---

### Release Notes Iap Review Screenshots Update (`docs/release-notes-iap-review-screenshots-update.md`)

# IAP review screenshot update

`asc iap review-screenshots update` supports upload finalization and resuming
an in-progress file upload.

Use `--checksum` and/or `--uploaded` to finalize an existing upload reservation
with Apple's supported PATCH attributes. These attributes do not replace a
completed asset.

Use `--file` to resume an existing screenshot reservation when the API returns
`uploadOperations`; the file size must match the reservation, and the command
uploads, commits, and verifies the same screenshot ID. App Store Connect
rejects creating a second IAP review screenshot while a completed one exists,
and its update schema has no asset-replacement field. Therefore update fails
before any POST, PATCH, or DELETE for a completed screenshot and prints the
manual delete-then-create workflow. The delete command requires `--confirm`,
so the existing screenshot is never removed implicitly.

---

### Release Notes Keychain Signing Identifier (`docs/release-notes-keychain-signing-identifier.md`)

# Stable code-signing identifier for keychain access

Release binaries for macOS are now signed with the fixed identifier
`com.rorkai.asc`. Earlier releases let `codesign` derive the identifier from
the versioned file name (for example `asc_5.3.0_macOS_arm64`), so every
release carried a different designated requirement. macOS keychain access
lists trust applications by that requirement, which is why "Always Allow" on
the stored `ASC API Key` item did not carry over to the next upgrade.

Resolving one profile also reads only that profile's keychain secret now.
Previously every stored profile was read to select one, so each stored
profile raised its own authorization prompt on every command.

The first run of a release with the new identifier prompts once more per
stored profile because the identifier changed. Choose "Always Allow" and
later upgrades reuse that decision.

Fixes #2512.

---

### Release Notes Koubou 0 20 0 (`docs/release-notes-koubou-0-20-0.md`)

# Koubou 0.20.0 for screenshot framing

`asc screenshots frame` now pins Koubou 0.20.0 instead of 0.18.1. Other
Koubou versions are rejected as unsupported; an older install gets an upgrade
hint. Upgrade and refresh the downloaded frames:

```bash
pip install -U koubou==0.20.0   # or: brew upgrade bitomule/tap/koubou
kou setup-frames
```

Frames generated by asc look the same after the upgrade. The frame artwork is
unchanged, and asc now emits `alignment: center` on every generated image item
as well as on text, so Koubou 0.19.0's alignment change keeps them centered.

Custom `--config` YAML files may need updates:

- Koubou 0.19.4 removed `"` from 18 frame names, for example
  `iPad Air 13" - M2 - Space Gray - Portrait` is now
  `iPad Air 13 - M2 - Space Gray - Portrait`.
- Koubou 0.19.0 made content `alignment` the true horizontal anchor for text
  and images. Items with `alignment: left` or `right` now start or end at their
  `position` x value.

---

### Release Notes Last Compatible Version Public Api (`docs/release-notes-last-compatible-version-public-api.md`)

# Last-compatible version settings move to the public API

App Store Connect's Last-Compatible Version Settings screen is the nullable
`downloadable` attribute on `appStoreVersions`. Both directions are now on the
public API path.

Read it with the versions JSON output, which preserves the attribute whenever
Apple returns it:

```bash
asc versions list --app "APP_ID" --paginate --output json
asc versions view --version-id "VERSION_ID" --output json
```

Write it with a new tri-state `--downloadable` flag on `asc versions update`:

```bash
asc versions update --version-id "VERSION_ID" --downloadable true
asc versions update --version-id "VERSION_ID" --downloadable false --confirm
```

Leaving `--downloadable` unset sends no `downloadable` attribute, so an
unrelated `asc versions update` never changes download availability. Setting it
to `false` makes a previously released version unavailable for download on
older operating systems and devices and is not reversible from every state, so
that direction requires `--confirm`:

```text
Error: --confirm is required with --downloadable false because making a released version unavailable for download is not reversible from every state. No request was sent.
```

`--confirm` is rejected in any other combination rather than silently ignored,
including an explicit `--confirm=false`:

```text
Error: --confirm applies only to --downloadable false; remove it or pass --downloadable false
```

`--downloadable false --confirm=false` is not confirmation either, so the write
is still refused.

`asc versions update --output json` now also echoes `downloadable` in its
receipt when Apple returns the attribute, matching `asc versions view`.

Use `--paginate` on the list read. The versions this setting usually targets are
the oldest ones, which fall past the first page for apps with a long release
history.

The short-lived web-session read `asc web apps last-compatible-version view` is
removed. It shipped after `4.11.0` and was never part of a tagged release, so it
never reached the `stable` rung of the command lifecycle and is removed directly
rather than deprecated. It was read-only, required an interactive web session,
and returned the same `downloadable` attribute the public API now exposes.

Migration:

| Retired | Replacement |
| --- | --- |
| `asc web apps last-compatible-version view --app APP_ID` | `asc versions list --app APP_ID --paginate --output json` |
| (no write existed) | `asc versions update --version-id VERSION_ID --downloadable true` |
| (no write existed) | `asc versions update --version-id VERSION_ID --downloadable false --confirm` |

The `capabilities` inventory row for this feature moves from `partial` to
`cli-supported` and no longer lists a web-session command.

---

### Release Notes Official Search Optimization (`docs/release-notes-official-search-optimization.md`)

# Official search optimization plan

Release 4.4.4 adds a read-only workflow that turns official
Apple Ads Platform API v1 evidence and App Store Connect metadata into one
reviewable search plan:

```bash
asc optimize search plan \
  --app "APP_ID" \
  --version "APP_VERSION" \
  --ad-account "AD_ACCOUNT_ID" \
  --country "US" \
  --genre "PRODUCTIVITY_UTILITIES" \
  --locale "en-US" \
  --out-dir ".asc/optimization/APP_VERSION" \
  --output markdown
```

The command keeps Apple's metrics semantically separate: Apple's 1–5 and
1–100 popularity scores are market search demand, impression share is
app-specific paid reach, search-term report rows are actual paid outcomes, and
metadata coverage comes from the selected App Store localization. Deterministic
actions identify converting broad terms,
costly zero-install terms, metadata candidates, low-share opportunities, and
already-saturated terms without inventing organic rank or proprietary
difficulty scores.

When `--out-dir` is set, the command writes a canonical report plus review-only
metadata, exact-keyword, and negative-keyword import files. It never applies
those files or mutates campaigns or App Store metadata. Partial Apple Ads
source failures remain visible in the report, while successful official data
is preserved. Actions that require proving a keyword is absent are suppressed
when their dependency is unavailable, and table or Markdown output shows the
source matrix and notices directly.

For apps with multiple App Info records, the command matches App Info to the
selected version state. If that match is ambiguous, pass the explicit record
with `--app-info`. The JSON report records both the resolved version ID and App
Info ID so the evidence can be reproduced.

The report also preserves Apple's app-and-country target-CPA suggestion for a
new Maximize Conversions campaign separately from recommendations for existing
campaigns.

App Store Connect and Apple Ads credentials are independent. Run `asc auth`
for App Store Connect and `asc ads auth login` for Apple Ads before using the
workflow. Credentialed live verification is still recommended before relying
on generated candidates in production.

---

### Release Notes Publish Macos Pkg (`docs/release-notes-publish-macos-pkg.md`)

# Prebuilt macOS PKG publishing

`asc publish testflight` and `asc publish appstore` now accept a prebuilt macOS
installer through `--pkg`. PKG uploads select `MAC_OS` automatically and
require explicit `--version` and `--build-number` values.

The existing `--ipa --platform MAC_OS` combination remains available during a
compatibility window, but now prints a deprecation warning. Migrate those
invocations to:

```bash
asc publish appstore \
  --app "APP_ID" \
  --pkg "MacApp.pkg" \
  --version "1.2.3" \
  --build-number "42"
```

The deprecated macOS IPA publishing combination will be removed in a future
major release. IPA publishing for iOS, tvOS, and visionOS is unchanged.

---

### Release Notes Signing Identity Sync (`docs/release-notes-signing-identity-sync.md`)

# Signing identity sync release note

> Status: the legacy password sources described below were removed in 5.0.0.
> `--password` is no longer registered and `ASC_MATCH_PASSWORD` is no longer
> read; use `--password-file` or `ASC_SIGNING_SYNC_PASSWORD`. See
> `migrate-to-5-0.mdx`.

`asc signing sync push` can now pair a local PKCS#12 or RSA/EC private key with
the certificate embedded in the selected provisioning profile, then store one
canonical encrypted identity for other agents and CI workers. New identity
envelopes use scrypt plus authenticated metadata, while existing
certificate/profile repository behavior stays compatible.

Use `--password-file` or `ASC_SIGNING_SYNC_PASSWORD` for the signing repository
password. The older `--password` flag and `ASC_MATCH_PASSWORD` environment
variable remain available through 4.x with a deprecation warning and will be
rejected in 5.0.0.

## Profile lifecycle

`asc signing sync push --renew-expired` replaces an expired profile with a
same-name successor. `--force-for-new-devices` recreates a development or ad
hoc profile when its devices differ from the enabled device list, and
`--include-mac-in-profiles` adds enabled Apple silicon Macs to iOS development
and ad hoc device sets. `asc signing sync nuke --profile-type TYPE --confirm`
deletes every profile of one type, revokes its certificates, and removes their
encrypted artifacts; `--dry-run` previews the plan without changes. See
[Renew, refresh, and remove synced profiles](../commands/signing.mdx#renew-refresh-and-remove-synced-profiles).

---

### Release Notes Web Bundle Ids Sync App Clip Confirm (`docs/release-notes-web-bundle-ids-sync-app-clip-confirm.md`)

# Web bundle-ids sync-app-clip confirmation and idempotency

`asc web bundle-ids capabilities sync-app-clip` now requires `--confirm`, in
line with every other `asc web` mutation. Syncing rewrites the App Clip Bundle
ID capability graph, and Apple invalidates existing provisioning profiles that
contain the changed App ID.

```bash
asc web bundle-ids capabilities sync-app-clip \
  --bundle-id "CLIP_BUNDLE_ID" \
  --parent-bundle-id "PARENT_BUNDLE_ID" \
  --capability "PUSH_NOTIFICATIONS" \
  --confirm
```

The previous invocation shape still parses, but it fails with usage exit code
`2` before a web session is resolved or any request is sent, and prints this
migration guidance to stderr:

```text
Warning: web bundle-ids capabilities sync-app-clip now requires --confirm because syncing rewrites the App Clip Bundle ID capability graph and invalidates existing provisioning profiles; re-run with --confirm to acknowledge. No request was sent.
Error: --confirm is required
```

Because a no-op write still invalidates profiles, the command is now
idempotent. It reads the current capability graph first and, when the
capability is already enabled with the requested `parentBundleId` (and the
requested settings, when `--settings-json` is passed), it sends no PATCH and
reports `changed: false` with status `already-synced`. When it does write, it
preserves every unrelated capability and relationship (including to-many
relationships such as App Groups and iCloud containers, which previously failed
to parse), sends only the writable `enabled` and `settings` capability
attributes, warns on stderr that existing provisioning profiles are now
invalid, and caches any refreshed session cookies.

The JSON receipt gains two additive fields, `changed` and `status`
(`synced` or `already-synced`); the table and Markdown outputs gain matching
`Changed` and `Status` columns.

---

### Release Notes Web Session Required (`docs/release-notes-web-session-required.md`)

# Clearer error when no Apple web session is available

Changed in 5.9.0. Some `asc web` commands need a signed-in Apple Account
session that they cannot create. This happens when:

- nothing is cached and no account is selected, or
- the selected account has no usable cached session, and `ASC_WEB_PASSWORD`,
  a saved password, and an interactive terminal are all unavailable.

Every web command except `asc web auth login` now prints a short error and a
`Hint:` line with the next step in that case, instead of the command's full
usage page:

```text
Error: no Apple web session is cached
Hint: asc web commands need a signed-in Apple Account session, and signing in needs an interactive terminal for the password and two-factor code. Run 'asc web auth login --apple-id EMAIL' in a terminal, or load a session exported elsewhere with 'asc web auth import --file FILE'. Unattended sign-in needs ASC_WEB_PASSWORD and ASC_WEB_2FA_CODE_COMMAND, plus --apple-id or ASC_WEB_APPLE_ID.
```

When an account was selected, the error names it. Where the public App Store
Connect API can answer instead, the hint also names that command. For example,
`asc web review show` points to `asc review status --app APP_ID`.

The exit code is unchanged: these failures still exit with usage code `2`.
`asc web auth login` keeps its existing usage errors and usage page.

---

### Release Notes Xcode Cloud Status Id Alias (`docs/release-notes-xcode-cloud-status-id-alias.md`)

# Xcode Cloud status ID alias

> Status: the `--id` alias described below was removed in 5.0.0. `asc
> xcode-cloud status --id` is now an unknown flag; use `--run-id`. See
> `migrate-to-5-0.mdx`.

`asc xcode-cloud status` now accepts `--id` as a deprecated compatibility alias
for `--run-id`. Existing alias calls continue after printing this warning to
stderr:

```text
Warning: `--id` is deprecated. Use `--run-id`.
```

New scripts should use `asc xcode-cloud status --run-id "BUILD_RUN_ID"`.
Supplying both `--id` and `--run-id` is a usage error, even when the values
match. The alias may be removed in the next major release after the standard
deprecation window.

---

### Sign In Keys Design (`docs/sign-in-keys-design.md`)

# Sign in with Apple private keys

Add `asc web sign-in-keys list`, `view --key-id ID`, `create --name NAME --bundle-id RESOURCE_ID --output-dir DIR --confirm`, and `download --key-id ID --output-dir DIR --confirm` to the existing web-session command family. The installed CLI has no Developer Portal authentication-key lifecycle commands. Existing App Store Connect API-key commands remain unchanged.

Use the existing Developer Portal session and team selection, request timeout, error handling, root-anchored, identity-checked private file writes on macOS and Linux, and output conventions. Creating a remote key or consuming a one-time download requires `--confirm`; neither is automatically retried after an ambiguous response. Downloaded P8 material must never appear in output; reserve a private destination before downloading, validate a real EC PKCS8 key, and emit a receipt containing only the key ID and saved path. Listing preserves Apple's raw envelope. Do not expose revoke in this bounded addition.

The private Developer Portal endpoints are outside the public OpenAPI schema. Confirm endpoint and payload evidence from maintained portal clients before implementation and verify through the live account. Tests cover required flags before authentication, expected request payloads, one-time download output handling, path collision and invalid responses. Run adjacent package tests, command generation and repository gates plus the mandated isolated Codex review.


Endpoint evidence: Expo's `@expo/apple-utils` 2.2.1 and Fastlane's portal client use legacy `/services-account/QH65B2/account/auth/key/{list,get,v2/create,download}`. The create request uses application/json and `serviceConfigurationsRequests: [{isNew: true, serviceId: "APPLE_ID_AUTH_KEY_CONFIGURATION", identifiers: {bundle: [resourceID]}}]`. The bodyless download GET must omit Content-Type. Listing primes key-family CSRF. Detail configuration entries identify the primary bundle using `type: "bundle"` and the opaque `id`.

Live verification on September 15, 2026: list and view succeeded; a dedicated key was created with the requested bundle association and its P8 was downloaded, parsed as P-256 PKCS8, saved mode 0600, and secured in the operator's vault. Initial download rejected the JSON:API Content-Type with HTTP 415 without consuming the key; removing Content-Type matched Expo's transport and completed the download. Existing keys were preserved.

---

### Subscription Notification Design (`docs/subscription-notification-design.md`)

# App Store Server Notifications URLs

`asc apps update` currently supports bundle ID, primary locale, and content rights,
but cannot configure App Store Server Notifications endpoints. Add two optional
flags in the existing command rather than a separate command or web-only path:

```bash
asc apps update --id APP_ID --subscription-status-url https://example.com/notifications
asc apps update --id APP_ID --sandbox-subscription-status-url https://example.com/sandbox-notifications
```

The offline OpenAPI snapshot supports both fields in `PATCH /v1/apps/{id}` via
`AppUpdateRequest.data.attributes`: `subscriptionStatusUrl` and
`subscriptionStatusUrlForSandbox`. Each URL is paired with its corresponding
`subscriptionStatusUrlVersion` or `subscriptionStatusUrlVersionForSandbox` set to
`V2`. The schema permits V1 and V2; this command explicitly configures V2 signed
payloads, while omitted endpoints retain their current version and URL. URL removal
and configuring legacy V1 payloads are outside this additive change.

URLs must use absolute HTTPS without credentials or fragments. Explicit empty
values are usage errors, validated before authentication or network access. Existing
app metadata flags remain available and can be combined with either URL flag.
Mutations keep the Apple resource envelope and existing output formats. Add all four
fields to app response attributes so JSON reads and mutation responses retain them.

Verification covers exact PATCH payloads for each endpoint, combined metadata,
omitted-field preservation, returned URLs and versions, and invalid URLs without
network activity. Establish RED on the unsupported flags, reach GREEN, verify built
binary help and usage-error exit code 2, and run the repository validation and review
gates. Any authorized live update must target only the operator-selected app and
read its configuration back afterward.

Live verification found that Apple omits notification fields from default GET app
responses even after a successful update. Add `apps view --fields` for the app
attributes decoded by this client, passing the selection through `fields[apps]`.
The exact GET endpoint schema permits all four notification fields. Default reads
stay unchanged; explicit empty or unknown selectors are usage errors. A CLI
regression asserts the sparse-field query and all four decoded response values.

---

### Wall Of Apps.Json (`docs/wall-of-apps.json`)

[
  {
    "app": "6 Nations - Rugby Scores Live",
    "link": "https://apps.apple.com/us/app/6-nations-rugby-scores-live/id414001342?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/34/09/f3/3409f3bc-b011-ef18-f9cf-1e885da8f97f/6nations-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "66-Day Streak: Habit Builder",
    "link": "https://apps.apple.com/us/app/66-day-streak-habit-builder/id6759220059",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5c/ce/67/5cce6782-729c-07af-1e0b-7322b8ffd914/SixtysixStreaksIcon-0-0-1x_U007emarketing-0-9-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Aanvi: Baby Memory Book",
    "link": "https://apps.apple.com/us/app/aanvi-baby-memory-book/id6759002114?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/75/5c/d8/755cd8e5-854a-e583-2908-54e6dea0566f/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Accountable Ai - Habit Blocker",
    "link": "https://apps.apple.com/us/app/accountable-ai-habit-blocker/id6747360923?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/83/4f/eb/834feba8-5b75-ac68-d2f3-4a3c8ce5510f/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "AcheNote - Symptom & Visit Log",
    "link": "https://apps.apple.com/us/app/achenote-symptom-visit-log/id6760022585?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c3/f6/39/c3f63991-8f6c-9a0f-f538-d5eb95fcb77c/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "ACHIEVE: Business Growth Ideas",
    "link": "https://apps.apple.com/us/app/achieve-business-growth-ideas/id6468916239?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/73/f2/2c/73f22ca2-4b29-225e-4a38-300cd2f752f3/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "AdMate App",
    "link": "https://apps.apple.com/us/app/admate-app/id6705137320?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/fb/86/59/fb865967-771a-6e3f-d5b6-325f5c60287f/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "aether: Remote Deck ControlPad",
    "link": "https://apps.apple.com/us/app/aether-remote-deck-controlpad/id6761128470?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4e/44/bd/4e44bde8-ee38-ba9a-c1f1-ff8485e5e5ff/aetherIcon-0-0-1x_U007ephone-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "AI Chat Assistant - Chappie",
    "link": "https://apps.apple.com/us/app/ai-chat-assistant-chappie/id6756681102?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/60/12/19/60121940-0202-b42d-6c0d-074edb4fff49/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "AI Journal & Diary: Unfold",
    "link": "https://apps.apple.com/us/app/ai-journal-diary-unfold/id6747351461?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/21/b4/7b/21b47bb0-055b-2510-f225-c93afde2be61/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "AI Sound Effects: Audio Studio",
    "link": "https://apps.apple.com/us/app/ai-sound-effects-audio-studio/id6759847647?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4b/63/2f/4b632f89-4083-8c06-f708-94270fce6d14/AppIcon-0-0-1x_U007ephone-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Aida Simple Fast Daily Planner",
    "link": "https://apps.apple.com/us/app/aida-simple-fast-daily-planner/id6757127406?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/cc/6b/36/cc6b3631-cfdf-8dbc-a66a-d5f6861a4514/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Airtist",
    "link": "https://apps.apple.com/app/id6745053878",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a1/85/81/a185815f-0e82-98b4-ac37-aed44c57d719/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Akari Glow: Light Up Puzzles",
    "link": "https://apps.apple.com/us/app/akari-glow-light-up-puzzles/id6759948370?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6a/d6/06/6ad6063e-a7c4-ef18-dd4e-afecc0c3ba14/AppIcon_Akari-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Alarm One: Shift Alarm Clock",
    "link": "https://apps.apple.com/us/app/alarm-one-shift-alarm-clock/id6748434148?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/4a/2b/43/4a2b43de-4feb-4758-e0be-5d5fee24f68e/icon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Aletheon",
    "link": "https://apps.apple.com/us/app/aletheon/id6761150392?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/32/b3/7a/32b37a05-8f82-eb89-4388-c0169410b7c3/AppIcon-0-0-1x_U007epad-0-1-0-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Allergy Translate",
    "link": "https://apps.apple.com/us/app/allergy-translate/id1366620257?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6f/d5/6b/6fd56b58-41da-7aa7-4737-04bee755418d/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Alune: Planner & Assistant",
    "link": "https://apps.apple.com/us/app/alune-planner-assistant/id6761420047?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/00/14/93/0014939d-ce61-2386-0bf3-b79c4dd678c4/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "AnimAtlas",
    "link": "https://apps.apple.com/us/app/animatlas/id6792998018?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/50/cd/84/50cd848a-8b56-9d26-4b78-385bf9432f54/ExplorerDex-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "AnLi Kids",
    "link": "https://apps.apple.com/us/app/anli-kids/id6759224873?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/32/55/ba/3255ba71-f714-8d4c-b0b2-2dadd0a9fe26/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "App Time Limit: Screen Leech",
    "link": "https://apps.apple.com/us/app/app-time-limit-screen-leech/id6743212513?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f5/a4/6e/f5a46e54-20c9-8a3f-69d0-df232214a7fc/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Appboard: Find Apps · Rankings",
    "link": "https://apps.apple.com/us/app/appboard-find-apps-rankings/id1667492380?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/b8/86/4e/b8864eea-7c79-4b57-dc58-4a208fc28863/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Aseta - Net Worth Tracker",
    "link": "https://apps.apple.com/us/app/aseta-net-worth-tracker/id6759186841?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/32/51/2d/32512d40-1347-5bcc-5a37-845b35c537ea/AppIconAseta-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Audio Horoscope · Astrologica",
    "link": "https://apps.apple.com/app/id6754387408",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/de/bd/7c/debd7c60-6be9-a231-d7c0-aa54a776dc3c/astrologica-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Audioer: Convert Audio Formats",
    "link": "https://apps.apple.com/us/app/audioer-convert-audio-formats/id6743841447?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/75/4b/bd/754bbd84-d6f5-3393-1de4-b0b04c6be328/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Authenticator app - 2FA, MFA",
    "link": "https://apps.apple.com/us/app/authenticator-app-2fa-mfa/id1625466768?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/fe/d3/08/fed308ed-910f-052c-7046-9280241cc22d/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Auto Metric -  Expense Tracker",
    "link": "https://apps.apple.com/us/app/auto-metric-expense-tracker/id6757914584?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/51/07/86/51078666-e31e-5e8c-14f8-833eb7091863/AppIcon-1x_U007emarketing-0-11-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Axo Tap: Idle Axolotl Pet",
    "link": "https://apps.apple.com/us/app/axo-tap-idle-axolotl-pet/id6799872294?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/25/72/c2/2572c20e-6176-4340-43e3-e07a4c65cf5a/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Azivault",
    "link": "https://apps.apple.com/us/app/azivault/id6774401513?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cc/35/bd/cc35bd9b-409b-876b-e20c-036942bbee7f/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Baby feeding & diaper tracker",
    "link": "https://apps.apple.com/us/app/baby-feeding-diaper-tracker/id6451158784?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/03/45/5a/03455ad1-896f-c341-6d38-13bab0c40e8b/AppIcon-0-1x_U007epad-0-1-P3-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Baby Sleep Sounds - Dozy",
    "link": "https://apps.apple.com/us/app/baby-sleep-sounds-dozy/id6747475875?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/51/a2/ba/51a2babe-ae87-6425-508c-fb303753debb/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BananaSplit: Split Expenses",
    "link": "https://apps.apple.com/us/app/bananasplit-split-expenses/id6752376267?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ec/4a/d1/ec4ad10e-6a0e-166d-3cf5-f22826a5d6a7/app-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BeatS [Afropop Edition]",
    "link": "https://apps.apple.com/us/app/beats-afropop-edition/id418786868?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/92/08/f6/9208f69f-e17c-4457-a10a-a2293abb2a8a/AppIcon-Kwaito-0-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "BeatS [Christmas Edition]",
    "link": "https://apps.apple.com/us/app/beats-christmas-edition/id410016967?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/98/f6/6a/98f66a6c-a1ef-1255-56e5-63642c4d70fd/AppIcon-Xmas-0-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "BeatS [Hip-Hop/Rap Edition]",
    "link": "https://apps.apple.com/us/app/beats-hip-hop-rap-edition/id397611091?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/55/31/9e/55319e96-134d-c7dc-edd7-08552951642e/AppIcon-Hip-Hop-0-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "BeatS [R&B/Soul Edition]",
    "link": "https://apps.apple.com/us/app/beats-r-b-soul-edition/id404929463?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e0/2a/e9/e02ae90f-3742-5a44-bfa9-f5744764e605/AppIcon-R_U0026B-0-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "BeatS [Reggaeton Edition]",
    "link": "https://apps.apple.com/us/app/beats-reggaeton-edition/id410001686?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/8f/a1/44/8fa14469-aa54-2b02-fbe9-748649e27cc1/AppIcon-Reggaeton-0-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Beauty Fonts - Font Keyboard",
    "link": "https://apps.apple.com/us/app/beauty-fonts-font-keyboard/id1438854446?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ce/85/c5/ce85c50d-e009-34b8-96f1-924e6881a364/AppIcon-0-0-1x_U007epad-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Biaura - Sleep & Focus Sounds",
    "link": "https://apps.apple.com/us/app/biaura-sleep-focus-sounds/id6748336447?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/af/9c/df/af9cdf51-1465-d861-f435-b05afb49def6/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Bibliostats",
    "link": "https://apps.apple.com/app/id6759411516",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/06/d8/48/06d84871-63f1-8c85-ceb5-e6a140b2831c/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "bijou.fm",
    "link": "https://apps.apple.com/us/app/bijou-fm-for-last-fm/id6450460066",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/1f/a9/bc/1fa9bcd6-5795-038c-d4ec-80dec18706ac/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Bitbook",
    "link": "https://apps.apple.com/us/app/bitbook/id1672879200?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3c/b9/15/3cb915f7-d30c-9b35-098a-026c8cb7398b/Bitbook-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BitLocal: BTC-Friendly Shops",
    "link": "https://apps.apple.com/us/app/bitlocal-btc-friendly-shops/id6447485666?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/71/18/13/71181314-64e0-ab1a-4e44-9f6134f12f01/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Blimber – Student Planner",
    "link": "https://apps.apple.com/us/app/blimber-student-planner/id6751923150?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/1b/d4/ca/1bd4ca07-3310-f9e9-b2a6-06e0a7c7a955/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Bloom",
    "link": "https://apps.apple.com/app/id6739955926",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/8e/9b/52/8e9b5222-940f-0058-1528-aec270296b39/Blueberry-0-1x_U007ephone-0-1-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Bloom - Coffee Shelf & Recipe",
    "link": "https://apps.apple.com/us/app/bloom-coffee-shelf-recipe/id6759914524?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a4/c8/2a/a4c82a25-fae8-4c04-6d02-91a15b72bb4f/BloomIconFinal-0-1x_U007ephone-0-1-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Board buddy – score counter",
    "link": "https://apps.apple.com/us/app/board-buddy-score-counter/id6743980638?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6a/dc/58/6adc5814-da30-95a2-eb8d-2cca1aaf50b7/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Boba Tycoon - Idle Tea Shop",
    "link": "https://apps.apple.com/us/app/boba-tycoon-idle-tea-shop/id6758029554?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ad/d4/c3/add4c3bf-d7b2-9445-3245-40d4e97403f9/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Bobbin",
    "link": "https://apps.apple.com/us/app/threads-growth-bobbin/id6756035789",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/fe/a3/9d/fea39d77-422f-d815-5af9-c0ee518f5b97/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Box Box Club: Formula Widgets",
    "link": "https://apps.apple.com/us/app/box-box-club-formula-widgets/id1621241916?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f9/73/48/f9734812-afa3-83b4-4862-a9331863f84b/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BrainDrain: 1 Minute Journal",
    "link": "https://apps.apple.com/us/app/braindrain-1-minute-journal/id6741376368?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4b/ce/14/4bce140a-0e8c-3c4b-cba1-80288b5878ae/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BratVPN",
    "link": "https://apps.apple.com/us/app/bratvpn/id6759875957?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/29/91/b8/2991b84b-72ec-130e-071e-18f6fd175a44/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BTCtoMOON",
    "link": "https://apps.apple.com/us/app/btctomoon/id6756674743?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/45/de/cb/45decbde-ac19-a049-67f4-2541a7e5ce48/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "BuddyTask: AI To Do List",
    "link": "https://apps.apple.com/us/app/buddytask-ai-to-do-list/id6758902036?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/80/f5/84/80f584d0-6956-72b2-1294-8a5157c450b9/AppIcon-0-0-1x_U007emarketing-0-7-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "CabinLink",
    "link": "https://apps.apple.com/us/app/cabinlink/id6761247556?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f8/91/a8/f891a8c5-d49e-a5bf-83f4-cbe86e0eac04/CabinLink-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Cairn - Your Expense Tracker",
    "link": "https://apps.apple.com/us/app/cairn-your-expense-tracker/id6762088614?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3d/7e/ab/3d7eabc0-2f4d-52b4-b629-5b979d65de14/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Cal AI - Calorie Tracker",
    "link": "https://apps.apple.com/us/app/cal-ai-calorie-tracker/id6480417616?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5e/5d/e9/5e5de9bc-18ef-3c07-79f0-e0a9e3009629/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Caliscool: Calisthenics",
    "link": "https://apps.apple.com/us/app/caliscool-calisthenics/id6758521484?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/45/fd/12/45fd1294-2f62-d4d9-368e-da68e27adc22/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Calorimeter",
    "link": "https://apps.apple.com/us/app/calorimeter/id6502800890?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d3/da/14/d3da14c9-99e0-1a7c-21b7-7f380b078084/AppIcon-0-1x_U007ephone-0-1-P3-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Capture GO - AI & Online",
    "link": "https://apps.apple.com/us/app/capture-go-ai-online/id6751532288?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/b3/d6/f9/b3d6f97c-0a17-9972-860c-03e401426033/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Cardstock: Sports Card Scanner",
    "link": "https://apps.apple.com/us/app/cardstock-sports-card-scanner/id1508888618?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3f/e4/a2/3fe4a277-9a47-455b-313b-5939c61f7258/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Carmory",
    "link": "https://apps.apple.com/us/app/carmory/id6757974031?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/68/53/44/68534453-8146-fd14-cf29-0e987dbec900/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Cat on Chair",
    "link": "https://apps.apple.com/ca/app/cat-on-chair-focus-timer/id6746562271",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/02/3d/6e/023d6ea0-1a98-d132-613c-d01b115d2e0d/AppIcon-0-0-1x_U007epad-0-1-0-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "CFB GameDay Board",
    "link": "https://apps.apple.com/us/app/cfb-gameday-board/id6809035228?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/be/8b/53/be8b5384-addc-fd00-0aeb-7e605b9d8817/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Checkify: #1 AI Bill Splitter",
    "link": "https://apps.apple.com/us/app/checkify-1-ai-bill-splitter/id6755905554?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fe/da/29/feda29cd-84dc-ad22-66f4-a276004811fc/appicon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ChessLock: Screen Time Control",
    "link": "https://apps.apple.com/us/app/chesslock-screen-time-control/id6744930081?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/57/ee/71/57ee713f-9cec-7083-bd6f-220b5668f523/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Chronoid",
    "link": "https://www.chronoid.app",
    "icon": "https://www.chronoid.app/app-icon.png"
  },
  {
    "app": "Cinefuse: AI Movie Maker",
    "link": "https://apps.apple.com/us/app/cinefuse-ai-movie-maker/id6758482061?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/98/b8/9f/98b89fce-4ee9-3a15-45d5-4def0e3808e0/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Clean Links: URL Privacy",
    "link": "https://apps.apple.com/us/app/clean-links-url-privacy/id6747395062?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/06/bd/0d/06bd0de6-4342-ed6d-357d-6178b1ebce6c/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Clip Jar",
    "link": "https://apps.apple.com/us/app/clip-jar/id1628120600?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/51/13/d7/5113d707-1dce-2010-2488-db621e84ae42/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Cloak: App Locker",
    "link": "https://apps.apple.com/us/app/cloak-app-locker/id6451156267?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/eb/b9/c4/ebb9c4be-fd15-228d-dd9a-05bfc5fa37ad/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "CodexMonitor",
    "link": "https://github.com/Dimillian/CodexMonitor"
  },
  {
    "app": "Color Match Game: Chroma",
    "link": "https://apps.apple.com/us/app/color-match-game-chroma/id1500196580",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e0/22/11/e02211b9-8e91-5250-17ed-a6105cd42922/Chroma-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ColorCub: AI Coloring Pages",
    "link": "https://apps.apple.com/us/app/colorcub-ai-coloring-pages/id6745206164?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4a/7f/ae/4a7faef1-a45e-75d7-d90a-066eda390ea4/AppIcon-0-0-1x_U007emarketing-0-8-0-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Colour Within",
    "link": "https://apps.apple.com/us/app/colour-within/id6763698447?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c4/85/67/c4856759-f9ff-bbc0-08e8-6dea0241ee5f/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Comer en Asturias",
    "link": "https://apps.apple.com/us/app/comer-en-asturias/id6802125809?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/7e/b1/18/7eb118c2-0cfb-c083-839e-cc896b31577c/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Concert Memories 2: Countdown",
    "link": "https://apps.apple.com/us/app/concert-memories-2-countdown/id6744301154?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6c/ce/a1/6ccea161-6da4-52c7-1260-f44955b0bc70/ConcertMemories2-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Connectarr",
    "link": "https://apps.apple.com/us/app/connectarr/id6757109537?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/48/3c/75/483c7585-5910-ef5a-d037-9183b6beafed/Connectarr-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Constellation: Mind Atlas",
    "link": "https://apps.apple.com/us/app/constellation-mind-atlas/id6756132557?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5d/39/50/5d3950a0-3c8d-f420-ab62-877c5b05e48f/AppIcon.lsr/512x512bb.jpg"
  },
  {
    "app": "Contact Sheet",
    "link": "https://apps.apple.com/us/app/contact-sheet-generator/id6744368077",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/cd/14/5e/cd145e49-9277-f536-bcf8-0c323a1985b0/AppIconUpdate-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Contact Sheet: AI Photo Review",
    "link": "https://apps.apple.com/us/app/contact-sheet-ai-photo-review/id6760267143?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/4d/84/49/4d84498d-802a-03b0-262f-bc80aa2c0f1a/AppIcon-0-0-1x_U007epad-0-1-0-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Convusic",
    "link": "https://apps.apple.com/us/app/convusic/id1591366129?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/40/b9/d8/40b9d80c-b3e5-79e4-1fc6-44f04efc95c4/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Copiek",
    "link": "https://apps.apple.com/us/app/copiek/id6813259113?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ce/6e/3d/ce6e3dcf-67ac-f6a0-3f3e-92077eec6b84/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Correspondance: One Inbox",
    "link": "https://apps.apple.com/us/app/correspondance-one-inbox/id6807945434?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/01/4c/d6/014cd6a7-bcf4-1926-4560-bc7261370fa4/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Countdown Visual Timer: Kids",
    "link": "https://apps.apple.com/us/app/countdown-visual-timer-kids/id6474494320?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/35/3d/ab/353dab23-80da-b33d-4091-db8d3d230cbd/ATVC_Icon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Cruisea: Real-time Tracking",
    "link": "https://apps.apple.com/us/app/cruisea-real-time-tracking/id6755068162?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ad/6c/03/ad6c0376-c349-cdee-bfac-c7882f57a4d7/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Crunch: Compress & Convert",
    "link": "https://apps.apple.com/us/app/crunch-compress-convert/id6748700457?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9e/88/85/9e888594-e92b-f739-c358-7fd083cae2f6/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Cubby",
    "link": "https://apps.apple.com/app/id6751732388",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/2b/cf/8b/2bcf8b94-8050-926d-bce1-b95266a061d0/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "cubby: stash & track",
    "link": "https://apps.apple.com/us/app/cubby-stash-track/id6759426602?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/77/e6/29/77e6296a-282e-ba85-74d2-93f673233114/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Curvely",
    "link": "https://apps.apple.com/us/app/curvely/id6794988370?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/b6/b4/2c/b6b42cc0-44fe-0545-0cac-275ebc6fe8d9/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "CUStats",
    "link": "https://apps.apple.com/us/app/custats/id6756333957?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/2f/71/e8/2f71e8b6-40ff-cd21-cd48-0161abdff235/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "CUStats Go",
    "link": "https://apps.apple.com/us/app/custats-go/id6757679404?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5d/65/7f/5d657f01-3cf3-2d0b-c343-45112c16a295/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Custom AI Charades",
    "link": "https://apps.apple.com/us/app/custom-ai-charades/id6475172244?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0b/1f/fa/0b1ffadc-4c03-342a-02c2-92c8014af53d/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "CutWise: AI Calorie Counter",
    "link": "https://apps.apple.com/us/app/cutwise-ai-calorie-counter/id6780189151?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/17/48/67/174867f1-44ab-aaad-58b4-50065f5e412d/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Daily Self Care - Anicca",
    "link": "https://apps.apple.com/us/app/daily-self-care-anicca/id6755129214?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/11/df/b2/11dfb2d2-9666-f952-fb04-b52f22fc971c/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Daily – Hours & Time Tracker",
    "link": "https://apps.apple.com/us/app/daily-hours-time-tracker/id686910553?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f0/02/8d/f0028d3e-b002-ff2a-2c0a-0d60041bc9ff/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Dandelion",
    "link": "https://apps.apple.com/us/app/dandelion-write-and-let-go/id6757363901",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e1/e4/e3/e1e4e3af-42bc-bbb0-cd4d-3876d720de3e/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Dash: Widgets for Workouts",
    "link": "https://apps.apple.com/us/app/dash-widgets-for-workouts/id1556281712",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cf/14/8f/cf148fbc-2902-8a67-35a7-350b1ec57971/AppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Dateminder - Event Reminders",
    "link": "https://apps.apple.com/us/app/dateminder-event-reminders/id6480020164?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a2/23/3c/a2233c68-f1ef-e5a4-4574-9ec5fa2a1d4c/AppIcon-0-1x_U007ephone-0-1-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "DAWN for Mastodon by Nightfox",
    "link": "https://apps.apple.com/us/app/dawn-for-mastodon-by-nightfox/id1668645019?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/da/40/73/da4073fc-6cdb-a01e-3666-3575f021ff74/AppIcon10-0-0-1x_U007epad-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "DayBar",
    "link": "https://apps.apple.com/us/app/daybar/id6739052447?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/79/16/d5/7916d56d-bf7f-37a0-cb0c-bb50f04ed4f3/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Daybard: Your Day as an RPG",
    "link": "https://apps.apple.com/us/app/daybard-your-day-as-an-rpg/id6786213426?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/2e/7f/30/2e7f3052-0c2e-102c-c994-e44e126d3a67/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "DayCount: Countdown Widget",
    "link": "https://apps.apple.com/us/app/daycount-countdown-widget/id6765979160?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/1a/98/4d/1a984db8-c625-4bcc-c3da-2caf3ded04ba/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Deep Hour - Deep Focus Ritual",
    "link": "https://apps.apple.com/app/id6758889630",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/55/90/80/55908099-0de3-f577-f029-7e76d29e3481/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "DeskTally",
    "link": "https://apps.apple.com/us/app/desktally/id6760261861?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ca/bd/6c/cabd6c0c-3283-a46e-e1ed-51e2fc253af6/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "DevHub",
    "link": "https://apps.apple.com/us/app/devhub/id6476452351?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c9/fa/8b/c9fa8b9f-1ec9-3612-2cb6-a0dbb610468d/AppIcon-0-0-85-220-0-0-5-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Devotion: Daily Bible",
    "link": "https://apps.apple.com/us/app/devotion-daily-bible/id6789295449?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/06/b0/6c/06b06ce2-ce56-0e00-040d-374ff0a7d0eb/Icon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "DevTutor: Learn Swift, SwiftUl",
    "link": "https://apps.apple.com/us/app/devtutor-learn-swift-swiftul/id6471227008?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a0/5e/d3/a05ed356-a8d1-47ec-b464-f1422a717a75/AppIcon-0-0-1x_U007epad-0-0-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Dither By Eyed™ - Photo Editor",
    "link": "https://apps.apple.com/us/app/dither-by-eyed-photo-editor/id6751736178?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/75/0a/23/750a23f4-cd91-8ec7-1155-fa2127042e4b/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Do? - Sex Tracker & Map Log",
    "link": "https://apps.apple.com/app/id6757778324",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/74/16/00/7416000d-28a9-7c8f-6537-9b6d8b92cc18/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Doc Translator: PDF, EPUB, OCR",
    "link": "https://apps.apple.com/us/app/doc-translator-pdf-epub-ocr/id6810911331?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0e/5f/8a/0e5f8ae7-a68b-1495-ec8c-fe0e8206dfd4/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Doco Translate: PDF Translator",
    "link": "https://apps.apple.com/us/app/doco-translate-pdf-translator/id6758205667?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/cf/13/14/cf1314c4-8eb3-026f-47c4-52de70683231/AppIcon-0-0-85-220-0-0-1-0-2x-P3.png/512x512bb.png"
  },
  {
    "app": "Don't Spiral: ROCD & Anxiety",
    "link": "https://apps.apple.com/us/app/dont-spiral-rocd-anxiety/id6758227509?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c6/e9/77/c6e977ac-bc3b-5f1c-fbdb-e7f46ae3e467/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Dot. Extend Your Digital World",
    "link": "https://apps.apple.com/us/app/dot-extend-your-digital-world/id6739468133?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3b/74/fe/3b74fe4f-e2d1-eb5c-2c3f-2255d003c618/icon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "DoubleMemory",
    "link": "https://apps.apple.com/app/id6737529034",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/65/58/32/6558325e-7e32-4385-90a8-454c6b80e373/DoubleMemory-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "DreamWhisper: Sleep Stories",
    "link": "https://apps.apple.com/us/app/dreamwhisper-sleep-stories/id6746324271?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f5/89/fb/f589fb1e-656e-3c59-6f4d-4c397f0a63ad/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Dripped",
    "link": "https://apps.apple.com/app/id6749790183",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/45/67/fd/4567fd8b-feb1-c665-e8e0-4b05a3016150/DrippedIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Dtd Playlist-Daily Mood Music",
    "link": "https://apps.apple.com/us/app/dtd-playlist-daily-mood-music/id1631654360?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c3/12/31/c31231ef-8a43-d6dd-84e2-23728b669d59/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "DuoCam Multicam Video Camera",
    "link": "https://apps.apple.com/us/app/duocam-multicam-video-camera/id1482277903?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f5/49/21/f5492122-83dc-594c-82ae-3010784cb984/AppIcon-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Echo Sleep: AI Sleep Sounds",
    "link": "https://apps.apple.com/us/app/echo-sleep-ai-sleep-sounds/id6746497347?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/98/d0/fe/98d0feb7-d137-eb25-a475-9c06caf643ed/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "EchoKeep: Smarter RSS Reader",
    "link": "https://apps.apple.com/us/app/echokeep-smarter-rss-reader/id6752524967?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/23/4a/dc/234adc2b-ac03-64c7-b503-05112d5ad183/EchoKeep-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "EduSwipe: AI Flashcards & SRS",
    "link": "https://apps.apple.com/us/app/eduswipe-ai-flashcards-srs/id6744727106?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/23/60/71/236071d7-7827-efd9-4c8f-03db0cee6aa9/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Elite Soccer Tactic Board",
    "link": "https://apps.apple.com/us/app/elite-soccer-tactic-board/id6476522645?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/22/e0/85/22e085b9-1969-da7c-4d35-3bfe75009463/SoccerIcon-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Embers to Dyson",
    "link": "https://apps.apple.com/us/app/embers-to-dyson/id6756216261?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5c/b3/ab/5cb3ab34-60e5-81f8-c555-518185d61f87/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Emma's Library",
    "link": "https://testflight.apple.com/join/69PadvA5"
  },
  {
    "app": "Envelope Budget - Kualia",
    "link": "https://apps.apple.com/us/app/envelope-budget-kualia/id6466455367?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c9/42/c0/c942c0f5-bb5c-faf0-0306-0eaa72a08112/AppIcon-Kualia-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "EQMonitor",
    "link": "https://apps.apple.com/jp/app/eqmonitor-%E5%9C%B0%E9%9C%87%E9%80%9F%E5%A0%B1/id6447546703?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/84/23/23/8423230f-6766-b3f1-3880-ad95174413a5/iOSAppIcon-0-0-1x_U007epad-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Expense Tracker: ExpenseKit",
    "link": "https://apps.apple.com/us/app/expense-tracker-expensekit/id6756433597?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a5/14/d8/a514d826-9475-8c8e-b4f5-6fe7b4fa660c/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Expenses: Spending Tracker",
    "link": "https://apps.apple.com/us/app/expenses-spending-tracker/id1492055171?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/31/60/8f/31608f6a-8261-eac7-4bc2-2fda5af77a3d/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fablum: EPUB & PDF Reader",
    "link": "https://apps.apple.com/us/app/fablum-epub-pdf-reader/id6761669091?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/2e/d2/fe/2ed2fe2b-1ba6-4378-d8ab-f547fe123b95/AppIcon-0-1x_U007emarketing-0-11-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "FaceScreen - Desktop Camera",
    "link": "https://apps.apple.com/us/app/facescreen-desktop-camera/id6702028512?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/79/71/89/797189ca-6bde-e2e0-9e24-99bd6593b928/AppIcon-0-0-85-220-0-5-0-2x-P3-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Family Flow: Family Organizer",
    "link": "https://apps.apple.com/us/app/family-flow-family-organizer/id6760829141?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/99/a7/c7/99a7c78d-a8dc-3f60-d83f-c368efbcba71/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fasting Works: IF & Meal Log",
    "link": "https://apps.apple.com/us/app/fasting-works-if-meal-log/id6757606218?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/56/72/22/567222b9-40dd-cca2-1a58-01a46cfaa2d9/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fax It: Send & Receive Fax",
    "link": "https://apps.apple.com/us/app/fax-it-send-receive-fax/id1458261691?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0a/06/0b/0a060b1f-ecdb-8d01-84f0-5928690fd028/Icon_4-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FeedbackKit - User Insights",
    "link": "https://apps.apple.com/us/app/feedbackkit-user-insights/id6757585445?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5f/ac/3a/5fac3a58-6406-9fe6-fd75-89e9c4ab7cec/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FeedFlow - RSS Feed Reader",
    "link": "https://apps.apple.com/us/app/feedflow-rss-feed-reader/id6447210518?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d3/68/28/d3682840-58a5-7c5d-31e0-70cc0fa62fad/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fermento Fermentation Journal",
    "link": "https://apps.apple.com/us/app/fermento-fermentation-journal/id6470132496?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e4/e3/59/e4e3593b-0e71-922d-2313-b31c35cde061/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Finny: AI Money Tracker",
    "link": "https://apps.apple.com/vn/app/finny-finance-tracker-app/id6736472735",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/04/e2/b6/04e2b6e5-d5b6-9e1b-dc3a-fbb65b2ddd84/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fish Identifier: 96% Accurate",
    "link": "https://apps.apple.com/us/app/fish-identifier-96-accurate/id6745837821?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/59/12/42/5912423b-8ce4-f251-3698-af61f3accff2/FishAppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fisherman SMS Filtering",
    "link": "https://apps.apple.com/app/id6449192504",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/8d/e5/c9/8de5c9bd-5ad9-3371-378a-b30f074a671a/AppIcon-0-0-1x_U007epad-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FishLogger: Fishing Journal",
    "link": "https://apps.apple.com/us/app/fishlogger-fishing-journal/id6757573416?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/fe/d7/e8/fed7e84b-41b9-35f9-efbc-9ac785c708c2/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fitnit: Health & Fitness",
    "link": "https://apps.apple.com/us/app/fitnit-health-fitness/id6757887175?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/57/64/b6/5764b6e3-f3fb-9a70-925d-00e39f893263/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FitPretty",
    "link": "https://apps.apple.com/app/id6654895919",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/7f/a8/5a/7fa85a04-f258-17dd-f120-63c68061241b/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fix My Treadmill",
    "link": "https://apps.apple.com/us/app/fix-my-treadmill/id6758863304?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/20/33/96/203396ba-0542-cf69-356b-355b9948201b/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Flash - appareil photo jetable",
    "link": "https://apps.apple.com/fr/app/flash-appareil-photo-jetable/id6792328866?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/36/c4/0a/36c40ac9-f831-24c7-7ff1-cf8b02373b0c/expo-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FlightCatcherPro – Plane Spot",
    "link": "https://apps.apple.com/us/app/flightcatcherpro-plane-spot/id6749510087?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/79/4f/bc/794fbcf4-100d-d1ff-8458-5324666f4552/AppIcon-0-0-1x_U007emarketing-0-6-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Flip - Score tracker",
    "link": "https://apps.apple.com/us/app/flip-score-tracker/id6760317673?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a7/2d/95/a72d9540-e585-17c3-2e34-eed82c876bc0/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FlipKit - Flipbook Maker",
    "link": "https://apps.apple.com/us/app/flipkit-flipbook-maker/id1507694594?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a1/e6/4f/a1e64f9f-de99-5fc1-f7e0-7ecbe7fafedb/AppIcon-0-1x_U007emarketing-0-11-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Floating Head, show yourself",
    "link": "https://apps.apple.com/us/app/floating-head-show-yourself/id1565946661?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a8/1c/5f/a81c5f95-aeea-6b4a-08c2-f1e029af022c/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "FluidCast: Podcast Player",
    "link": "https://apps.apple.com/us/app/fluidcast-podcast-player/id6754508580?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c7/d2/d1/c7d2d17a-5764-0da5-d9e4-576b0390989a/AppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FluxRate",
    "link": "https://apps.apple.com/us/app/fluxrate/id6813696413?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ef/1a/66/ef1a66f2-4abe-6e7a-6ad1-cd19acb8f227/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Focus & Streak: That One Thing",
    "link": "https://apps.apple.com/us/app/focus-streak-that-one-thing/id6746286274?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/eb/f0/cc/ebf0ccfc-7693-db0a-0e4a-2fa5851cbca3/AppIcon_Cream-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Focus Rail: Pomodoro Timer",
    "link": "https://apps.apple.com/us/app/focus-rail-pomodoro-timer/id6758016543?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/7d/f9/0e/7df90e3f-3a37-c70c-7bf9-cb3cc9c0f16d/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FocusAir: Deep Focus Timer",
    "link": "https://apps.apple.com/us/app/focusair-deep-focus-timer/id6756526902?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/09/e1/ad/09e1adab-b9fd-1707-57fc-387a7e19a7d0/AppIcon-0-1x_U007ephone-0-1-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "FocusCursor: Highlight &Markup",
    "link": "https://apps.apple.com/us/app/focuscursor-highlight-markup/id6743495172?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/38/68/9e/38689e2a-598d-b3cd-58ac-ae551225d7b7/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "FocusScreen",
    "link": "https://apps.apple.com/us/app/focusscreen/id6757796328?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e9/5a/f2/e95af2c5-aaea-38d5-f33b-5c67a961d1e9/AppIcon-0-0-1x_U007epad-0-1-0-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Fogged: Travel Photo Map",
    "link": "https://apps.apple.com/us/app/fogged-travel-photo-map/id6799502565?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6c/76/2b/6c762b6b-2bb2-1c54-c708-b6211641e813/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Foliax",
    "link": "https://apps.apple.com/us/app/foliax/id6760576005?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/df/ed/b0/dfedb05d-d90c-58bf-6048-c0b5ac57984c/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Font Wizard Pro: Font Manager",
    "link": "https://apps.apple.com/us/app/font-wizard-pro-font-manager/id352966034?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5e/23/bb/5e23bb9c-1a69-86c2-61bb-a3a1b5c6270b/AppIcon-0-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Forerunner: Running & Wellness",
    "link": "https://apps.apple.com/us/app/forerunner-running-wellness/id6745802851?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/06/bb/79/06bb796e-81a0-8812-5c4a-6148ee92e8bf/AppIcon-0-0-1x_U007epad-0-0-0-1-0-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Foundation Lab",
    "link": "https://testflight.apple.com/join/JWR9FpP3"
  },
  {
    "app": "FPLai: FPL AI Assistant",
    "link": "https://apps.apple.com/app/id6757015527",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a3/a3/19/a3a31917-1ef0-2d81-3150-65391212a1bd/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FreeDive Ultra",
    "link": "https://apps.apple.com/us/app/freedive-ultra/id6760556868?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/1d/90/b8/1d90b818-be35-fe3b-6dd1-81bd6ac99858/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Freshwise",
    "link": "https://apps.apple.com/us/app/freshwise/id6786175708?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6a/35/81/6a3581eb-7a78-3a2e-5626-fe0994612676/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Fridgea: Fridge & Grocery List",
    "link": "https://apps.apple.com/us/app/fridgea-fridge-grocery-list/id6756065960?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a8/15/84/a81584b0-2187-9259-5b49-23cf2cb45650/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fuel log tracker: Click2Track",
    "link": "https://apps.apple.com/us/app/fuel-log-tracker-click2track/id6757174917?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fe/27/ca/fe27cafb-6b57-b44d-525e-5b5d3c07b241/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Fuel: AI Nutrition",
    "link": "https://apps.apple.com/app/id6748624107",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/52/0e/ed/520eed9c-d3f0-5d0e-3e68-5b6e698e5efc/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "FunVoice Reverse Audio Singing",
    "link": "https://apps.apple.com/us/app/funvoice-reverse-audio-singing/id6753986607?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c7/88/0f/c7880f37-6273-244c-8ef5-d4095471df2b/funvoice_2025-10-14-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Game Box - Games for Watch",
    "link": "https://apps.apple.com/us/app/game-box-games-for-watch/id6741334696?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/fb/f2/5e/fbf25e58-55d1-77de-a0ad-a1e8488ab7d7/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Gazolina - Prix Essence Gasoil",
    "link": "https://apps.apple.com/fr/app/gazolina-prix-essence-gasoil/id6757231174?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3a/5d/48/3a5d4831-2f9a-7d7e-adfa-2e5eb7057c2f/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "GCC Talent",
    "link": "https://apps.apple.com/us/app/gcc-talent/id6811953234?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a1/1d/21/a11d216e-7b4e-c051-d9e9-019663439105/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Gecko Med",
    "link": "https://apps.apple.com/us/app/gecko-med/id6737719743?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/63/4c/14/634c143e-7e6b-4afb-9748-0816af5d4544/apple-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Geeps: AI Chat BYOK",
    "link": "https://apps.apple.com/us/app/geeps-ai-chat-byok/id6476961115?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/37/b6/e1/37b6e1c0-87da-9cb0-1d76-6d2a88b86b0a/Geeps-0-0-1x_U007epad-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Geni Kids",
    "link": "https://apps.apple.com/us/app/geni-kids/id6761134405?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/72/a9/92/72a99255-2c75-ee9c-6bdf-215957eff996/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Glint: Morse Code Translator",
    "link": "https://apps.apple.com/us/app/glint-morse-code-translator/id6758366218?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/69/e1/b3/69e1b3be-109a-084a-62a3-8df22c8076c2/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "GO Club: Step Counter",
    "link": "https://apps.apple.com/us/app/go-club-step-counter/id6739782103?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/7a/23/fc/7a23fc3e-69d9-26b8-52e0-e2ce1587dab8/AppIcon-0-0-1x_U007ephone-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Goshuin Atlas",
    "link": "https://apps.apple.com/app/goshuin-atlas-japan/id6746737093",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/11/49/d6/1149d644-0ac6-6277-9847-c9ac124510a8/AppIcon-0-1x_U007ephone-0-1-85-220-0.jpeg/512x512bb.jpg"
  },
  {
    "app": "Gradient Match Game: Descent",
    "link": "https://apps.apple.com/us/app/gradient-match-game-descent/id1479784361?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e8/a4/81/e8a4813c-4d02-65b3-442a-3703431813d7/AppIcon-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Greenie: Plant Identifier AI",
    "link": "https://apps.apple.com/us/app/greenie-plant-identifier-ai/id6746816013?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/91/ba/28/91ba28b2-dba6-24bc-c191-bb21508e55f0/GreenieIcon-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Grove ◦ Fiber Tracker",
    "link": "https://apps.apple.com/us/app/grove-fiber-tracker/id6760088502?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/99/ac/ff/99acff42-6f6f-4bbf-70e3-19000f97430d/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "GrowPal: Health & Fitness",
    "link": "https://apps.apple.com/us/app/growpal-health-fitness/id1560604814?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0c/be/8b/0cbe8bfa-a36d-7ebb-6580-959481f43294/Grow-0-0-1x_U007ephone-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Guitar Wiz - Tuner & Chords",
    "link": "https://apps.apple.com/us/app/guitar-wiz-tuner-chords/id6740015002?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fc/d0/65/fcd06547-95ac-9d05-23eb-c67ba81e52de/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Habit Tracker - HabitMax",
    "link": "https://apps.apple.com/us/app/habit-tracker-habitmax/id6752316720?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/1c/5a/13/1c5a13a5-02e5-30c8-a359-e49972d11582/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Hands Time - Minimalist Widget",
    "link": "https://apps.apple.com/us/app/hands-time-minimalist-widget/id6462440720?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ea/cd/e9/eacde9d6-4e33-e631-55fc-05aa961523a5/AppIcon-0-0-1x_U007ephone-0-0-0-1-0-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Harry's: Fitness Challenges",
    "link": "https://apps.apple.com/us/app/harrys-fitness-challenges/id6502330879?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e6/e8/45/e6e845ca-71af-b03e-83fc-e6aa7c246c0c/Harry_U0027s-Challenges-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Harvest - One-Tap Summary",
    "link": "https://apps.apple.com/us/app/harvest-one-tap-summary/id6738302076?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0c/16/4e/0c164e6e-0b93-c0b3-314e-4b6b9742f282/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Haven: Setup Codes",
    "link": "https://apps.apple.com/us/app/haven-setup-codes/id6760440926?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/72/e6/1f/72e61fe0-7f0e-bb59-732d-c89ed669ed59/Haven-0-0-1x_U007epad-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Hedwig - Block Text Messages",
    "link": "https://apps.apple.com/us/app/hedwig-block-text-messages/id6736428346?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/56/a6/e5/56a6e530-ded3-69de-d871-0d0252bd0610/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Home Cameras",
    "link": "https://apps.apple.com/us/app/home-cameras/id6762038582?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/38/05/66/38056606-4649-101f-cdc3-a4ed626181dc/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "hora Calendar",
    "link": "https://apps.apple.com/us/app/hora-calendar/id6761409895?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e5/c2/e3/e5c2e3ee-f9f0-c491-4f22-5f6282853823/Hora-0-0-85-220-0-6-0-2x-sRGB.png/512x512bb.png"
  },
  {
    "app": "HoverCalc",
    "link": "https://apps.apple.com/us/app/hovercalc/id6759318126?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4d/d5/0f/4dd50f26-4b76-e382-9774-5894156f11df/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "HowHigh · Barometer & Altimeter",
    "link": "https://apps.apple.com/us/app/barometer-altimeter-howhigh/id921339656?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/79/66/ab/7966ab76-f237-ff79-e866-a51e96a530f3/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "HRM Battery",
    "link": "https://apps.apple.com/app/id6758920011",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/7b/43/73/7b4373fc-26e2-030b-7162-2f0ab0a7681d/AppIcon-0-0-1x_U007ewatch-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Hungry Wabbit",
    "link": "https://apps.apple.com/us/app/hungry-wabbit/id992325663?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5e/9a/32/5e9a3224-865e-ec24-1b56-87a2823a70ba/HungryWabbit-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Hush for visionOS",
    "link": "https://apps.apple.com/us/app/hush-for-visionos/id6770631510?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f2/00/bf/f200bf3b-4f19-5cf4-d08b-f65e39137346/AppIcon.lsr/512x512bb.jpg"
  },
  {
    "app": "I Need That Widget: Calendar",
    "link": "https://apps.apple.com/us/app/i-need-that-widget-calendar/id6753172187?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c1/21/7d/c1217dec-82c4-f9e7-7c54-9c04da639595/INTW-0-0-1x_U007ephone-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "iCardSort",
    "link": "https://apps.apple.com/us/app/icardsort/id384552728?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/91/5d/55/915d552c-3b3d-a636-622c-525e4d4ca4e2/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Iconize Folder",
    "link": "https://apps.apple.com/us/app/iconize-folder/id6478772538?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/57/e3/94/57e39489-2c7c-c1cc-56bc-13f85096d9cc/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Idle Coffee Empire Clicker",
    "link": "https://apps.apple.com/us/app/idle-coffee-empire-clicker/id6761031587?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4f/c9/94/4fc994dc-39e7-a593-8dd9-b5feff9e579a/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "iGIF",
    "link": "https://apps.apple.com/us/app/igif/id6759003636?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/79/62/f3/7962f38f-f28f-90dd-88a0-34786e747692/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Imposter Game: Fakeout Party",
    "link": "https://apps.apple.com/us/app/imposter-game-fakeout-party/id6761190861?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/8c/08/f18c08d9-6a65-3444-a15e-bb4d0540e180/AppIcon-Default-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Impulses: Stop Impulse Buying",
    "link": "https://apps.apple.com/us/app/impulses-stop-impulse-buying/id6754372500?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/8a/05/57/8a055708-4850-8120-8070-91424269b2f7/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Inferno by Eyed™ - Local LLM",
    "link": "https://apps.apple.com/us/app/inferno-by-eyed-local-llm/id6760921200?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d3/7d/1c/d37d1c47-8457-7bff-a52f-7c7c90feaa1b/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Inkpress",
    "link": "https://apps.apple.com/us/app/inkpress/id6787759999?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d6/32/4e/d6324e8a-b7d9-3ef9-93e9-6ad948ed6081/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Inkput",
    "link": "https://apps.apple.com/app/id6758570182",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/83/e2/68/83e26872-d3d5-ab92-7164-673371673443/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Inline - The inline AI",
    "link": "https://apps.apple.com/us/app/inline-the-inline-ai/id6748410177?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a2/73/bf/a273bfba-ccb0-61ed-4c7e-0c8c49c3347a/Inline-0-0-85-220-0-0-6-0-2x.png/512x512bb.png"
  },
  {
    "app": "Inquizitive: Smart AI Quiz",
    "link": "https://apps.apple.com/us/app/inquizitive-smart-ai-quiz/id6817424653?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a5/5b/30/a55b305a-576b-e07a-386a-a111f736ae94/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Intake: Calorie Counter & Log",
    "link": "https://apps.apple.com/us/app/intake-calorie-counter-log/id6757768955?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9c/df/d1/9cdfd107-e11a-acc5-4859-45921df8cb0e/intake-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "iWave",
    "link": "https://apps.apple.com/us/app/iwave/id6759223120?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/bf/e8/7b/bfe87b02-5721-33b6-cfe0-a62abc95d879/AppIcon-0-0-1x_U007ewatch-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "iWeatherShare",
    "link": "https://apps.apple.com/us/app/iweathershare/id6757788252?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/67/80/bd/6780bd31-e96f-be72-2d30-6597c3d766df/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Jinshu Reader",
    "link": "https://apps.apple.com/us/app/jinshu-reader/id6759705710?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6c/ce/46/6cce46eb-8613-c97d-8913-6888c3d4f1d2/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Jiyu: Days into Seasons",
    "link": "https://apps.apple.com/us/app/jiyu-days-into-seasons/id6789923573?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6e/a3/fd/6ea3fd01-a8ea-fd12-8184-c93ed603e64a/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Joinly",
    "link": "https://apps.apple.com/us/app/joinly/id6759517723?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/29/50/0a/29500a80-7e71-0e44-3039-d117a37a1587/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "journeybot: trip packing list",
    "link": "https://apps.apple.com/us/app/journeybot-trip-packing-list/id6756543673?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/8a/c2/bc/8ac2bcd8-9ddc-da52-cf6a-14990c6e107f/AppIcon-0-1x_U007epad-0-1-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Jurni: Trip Planner",
    "link": "https://apps.apple.com/us/app/jurni-trip-planner/id6759559463?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/67/19/a1/6719a179-73ef-1266-5682-bf1c944f98b9/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Karacho: Digital Speedometer",
    "link": "https://apps.apple.com/us/app/karacho-digital-speedometer/id6819278673?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/bb/b8/ee/bbb8ee0b-b055-b104-0d09-a0cc594d7977/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "kardfolio: Card Benefits",
    "link": "https://apps.apple.com/us/app/kardfolio-card-benefits/id6759400652?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/89/51/81/8951816e-6e84-d3e2-dcce-e1c12a0fdd08/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Kavale - Sécurité Équestre",
    "link": "https://apps.apple.com/fr/app/kavale-s%C3%A9curit%C3%A9-%C3%A9questre/id1458105068?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fd/c6/76/fdc67602-ffd1-66a4-16c5-2ce0a52f278c/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "KeepCount: Tally Counter",
    "link": "https://apps.apple.com/us/app/keepcount-tally-counter/id6758891370",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ee/f4/e6/eef4e6d4-c2af-3856-6cdf-80af085b1cb9/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "KeyClicker",
    "link": "https://apps.apple.com/us/app/keyclicker/id6740425504?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/b3/48/0f/b3480f03-46d0-aa8e-d172-0c0e054890e3/AppIcon-0-0-85-220-0-5-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "KeyScreen - Display Keystrokes",
    "link": "https://apps.apple.com/us/app/keyscreen-display-keystrokes/id6753302381?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c8/38/fd/c838fddf-a2db-5007-12be-8cb9906926e1/AppIcon-0-0-85-220-0-5-0-2x-P3-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Kilowatt – Electric Car Timer",
    "link": "https://apps.apple.com/us/app/kilowatt-electric-car-timer/id1502312657?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cd/ac/b6/cdacb60b-c1c2-79f1-a7cf-7b3e6674b963/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Klick #1 AI Camera Assistant",
    "link": "https://apps.apple.com/us/app/klick-1-ai-camera-assistant/id6749798728?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/47/23/d9/4723d958-44ab-af95-67a7-5431d6ff981a/AppIcon-0-0-1x_U007ephone-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Klick: keyboard sounds",
    "link": "https://apps.apple.com/us/app/klick-keyboard-sounds/id6758880736?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d1/c0/46/d1c046e6-f966-f063-e294-0a1969396b60/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Knight's Graph",
    "link": "https://apps.apple.com/us/app/knights-graph/id6737812039?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/78/83/4a/78834a4b-e815-1bfc-9ab0-bc0bdbea4e20/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "kora: Music Reviews & Ratings",
    "link": "https://apps.apple.com/app/id6502549140",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/48/70/77/487077dc-caef-0687-6ccf-48daffa9cf5b/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Kosaku - Beautify the content",
    "link": "https://apps.apple.com/us/app/kosaku-beautify-the-content/id1611559010?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f4/63/32/f46332d0-c51c-6a40-2022-a2e92f745e2e/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Layered: AI Outfit Maker",
    "link": "https://apps.apple.com/us/app/layered-ai-outfit-maker/id6746713932?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/91/a9/6e/91a96e06-a3de-fcc3-0719-109159431685/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Leafty - Travel Planner",
    "link": "https://apps.apple.com/us/app/leafty-travel-planner/id6502776303?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/93/8c/82/938c8234-a602-a54a-83ea-6f3d85017add/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Lexie: Language Tamagotchi",
    "link": "https://apps.apple.com/us/app/lexie-language-tamagotchi/id6748887471?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6d/fa/a9/6dfaa937-68e3-31e5-a090-9341aa41d70f/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Lifterly",
    "link": "https://apps.apple.com/us/app/lifterly/id6753913630?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/78/df/1e/78df1e26-9a13-20e6-3b37-df45306a684e/app-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Lights Out - Mafia Table Game",
    "link": "https://apps.apple.com/us/app/lights-out-mafia-table-game/id6785313453?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f4/77/3a/f4773ae1-5766-3c78-829c-df59b60f2c7f/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "LilyFM",
    "link": "https://apps.apple.com/app/id6745472149",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a6/aa/4e/a6aa4ed4-1750-12c4-c206-8b1031c9c95c/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Litigate",
    "link": "https://apps.apple.com/us/app/litigate/id6787857503?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0f/47/13/0f471335-1649-ae5d-6da1-8406c48f5a74/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "LivaLog",
    "link": "https://apps.apple.com/us/app/livalog/id6759440574",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/bf/cf/5d/bfcf5d5a-2cd1-b41a-f65d-a751085736c5/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Long Distance: Couple & BFF",
    "link": "https://apps.apple.com/us/app/long-distance-couple-bff/id6757593531?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d0/e1/3e/d0e13e2d-b2e9-70dd-538f-c8aa66ae7f54/AppIcon-0-1x_U007ephone-0-1-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "LookForward: Movies & Games",
    "link": "https://apps.apple.com/us/app/lookforward-movies-games/id1492748952?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/dc/1a/c6/dc1ac682-8609-9d45-d50e-35b11aa45aad/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Loverzz: Couples App & Widgets",
    "link": "https://apps.apple.com/us/app/loverzz-couples-app-widgets/id6744887601?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/34/14/b8/3414b8a0-0647-c6b2-550a-e2ca1eb28e77/AppIcon-0-1x_U007ephone-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Lumical: Scan to Calendar",
    "link": "https://apps.apple.com/us/app/lumical-scan-to-calendar/id6753274309",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/05/6a/92/056a928c-8748-1457-e513-43e96ce8a97e/Lumical-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Mafy - Dog Care Tracker",
    "link": "https://apps.apple.com/us/app/mafy-dog-care-tracker/id6736774671?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/00/95/ed/0095ed43-61ca-da3c-f01d-9d7fb43137dc/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Magic Sticker Printer",
    "link": "https://apps.apple.com/app/id6759186064",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/70/6a/e1/706ae16e-f081-eef4-6970-9d6896c4dbf4/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Mahjong AI - Mahjong Assistant",
    "link": "https://apps.apple.com/app/id6758575182",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/12/8e/fb/128efb33-a5ee-d321-2720-924431d772dd/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Majlis",
    "link": "https://www.joinmajlis.app"
  },
  {
    "app": "Margin: AI Article Reader",
    "link": "https://apps.apple.com/us/app/margin-ai-article-reader/id6748705626?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/1a/dd/73/1add730b-0e85-d8fd-09eb-722cb492877d/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Masseter: Jaw Clench Tracker",
    "link": "https://apps.apple.com/us/app/masseter-jaw-clench-tracker/id6758920098?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9d/34/53/9d345362-2b78-96c5-5294-33d2e084bb90/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Master Indicator",
    "link": "https://apps.apple.com/us/app/master-indicator/id6758697856?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/25/2c/78/252c787f-d9e8-738e-51fb-61c64ee95c32/AppIcon-1x_U007emarketing-0-10-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Matchday - Soccer Planner",
    "link": "https://apps.apple.com/us/app/matchday-soccer-planner/id6776015801?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d5/d8/af/d5d8af6c-325a-56f9-6c88-36b787e6bdc5/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MD Clock - Flip Clock",
    "link": "https://apps.apple.com/us/app/md-clock-flip-clock/id1536358464?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/4c/07/b7/4c07b7c5-0612-7f65-0b65-91594843e0fb/AppIcon-0-0-1x_U007epad-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "MD Vinyl for Music App",
    "link": "https://apps.apple.com/us/app/md-vinyl-for-music-app/id1606306441?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/38/c4/f3/38c4f3b5-4bd2-86ad-e100-5e220c16e08c/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MD Widgets - Countdown widget",
    "link": "https://apps.apple.com/us/app/md-widgets-countdown-widget/id1551277937?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/2f/ca/21/2fca210d-91ad-8736-575b-d729d3002f17/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MealMate: Calorie Counter",
    "link": "https://apps.apple.com/us/app/mealmate-calorie-counter/id6740268220?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0f/f1/68/0ff1684f-f3fd-35c5-b6b3-5ef10b84d907/Icon-0-1x_U007ephone-0-0-0-1-0-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Menuist: Right-Click & MenuBar",
    "link": "https://apps.apple.com/us/app/menuist-right-click-menubar/id6737160756?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/7e/f5/ee/7ef5eea0-ae74-a4a1-475a-4df5b8bab935/AppIcon-0-0-85-220-0-5-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Meshing",
    "link": "https://apps.apple.com/app/id6567933550",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/77/7b/8f/777b8f6c-b8e6-0fef-50fb-9e6a59166d06/Meshing_Icon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Metrociego",
    "link": "https://apps.apple.com/us/app/metrociego/id1231945684?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/50/8e/ed/508eedf3-5d31-7f42-c7dc-4c5551ce5d4a/AppIcon-0-0-1x_U007epad-0-0-0-1-0-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Midnight: Couples App & Games",
    "link": "https://apps.apple.com/us/app/midnight-couples-app-games/id6758623374?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e5/81/e1/e581e1f6-fed0-f6b0-0e70-cb2ad5a0c764/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MileIO",
    "link": "https://apps.apple.com/app/id6758225631",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/09/0a/21/090a21e8-5b19-ec94-c5d0-6c2d0f5175bd/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MiniHue: Color Mini Sudoku 6x6",
    "link": "https://apps.apple.com/us/app/minihue-color-mini-sudoku-6x6/id6761858520?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/28/3c/03/283c0325-ab33-bf68-b7f8-d51ce7e9ec48/AppIcon_MiniHue-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Minimal Diary -  Life timeline",
    "link": "https://apps.apple.com/us/app/minimal-diary-life-timeline/id1568936702?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3d/26/b2/3d26b23e-3e10-76d2-ce22-6ac9d2094499/md-glass-icon-0-0-1x_U007epad-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Minimal Minesweeper",
    "link": "https://apps.apple.com/us/app/minimal-minesweeper/id1029488767?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/dc/a1/50/dca15026-6e92-d75f-11c1-afa3d76b6e4b/AppIcon-1x_U007epad-0-1-P3-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "minimo (video): Compressor",
    "link": "https://apps.apple.com/us/app/minimo-video-compressor/id6787456951?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cb/d0/91/cbd09181-c957-2dce-c8a4-2e258f73dc6d/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "minute cryptic • wordplay",
    "link": "https://apps.apple.com/app/id6748607228",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/95/51/28/955128eb-6041-160a-464c-41795f7d0f12/wordplay-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Mixtape",
    "link": "https://apps.apple.com/us/app/mixtape-personal-music-gift/id6756442910",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/71/af/7f/71af7f55-1c60-1638-6161-8491e23edd0e/AppIcon-0-0-1x_U007ephone-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Moda: Driving Permit Hours Log",
    "link": "https://apps.apple.com/us/app/moda-driving-permit-hours-log/id6760154154?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/86/48/0d/86480d5e-7118-7a35-6a74-7cba92a9099d/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Model Usage",
    "link": "https://apps.apple.com/us/app/model-usage/id6761831599?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6e/78/19/6e7819e8-76dc-2674-3df7-fdc7d8558633/AppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "mojo - GLP-1 Weight Tracker",
    "link": "https://apps.apple.com/app/id6751704399",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ee/00/84/ee008451-9632-a851-0da8-6a69caab37a5/AppIcon-0-0-1x_U007ephone-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Morning Pages",
    "link": "https://apps.apple.com/us/app/morning-pages/id6738604034",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c5/ec/37/c5ec371e-e920-33ec-1541-74d05fc067c2/app-icon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MorseKit: Morse Code Trainer",
    "link": "https://apps.apple.com/us/app/morsekit-morse-code-trainer/id6761839923?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5f/b0/81/5fb081cd-fcd6-998e-cbb2-3c84816d6e6b/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "MultiPods Audio",
    "link": "https://apps.apple.com/us/app/multipods-audio/id6766111375?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/85/12/63/8512639f-60c5-e662-6fb4-0f1214d6a3a1/AppIcon-0-0-85-220-0-5-0-2x-P3.png/512x512bb.png"
  },
  {
    "app": "murmur: ai interview roleplay",
    "link": "https://apps.apple.com/us/app/murmur-ai-interview-roleplay/id6755495030?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a6/6f/1d/a66f1dd5-40fb-a7e8-f089-53f1790e67d7/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Music Plus - Player Extensions",
    "link": "https://apps.apple.com/us/app/music-plus-player-extensions/id1626398921?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/19/ce/4c/19ce4c8e-a1b5-2c3e-e44f-66011b347558/AppIcon-0-1x_U007epad-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Musicer: Simple music player",
    "link": "https://apps.apple.com/us/app/musicer-simple-music-player/id6745227444?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a0/aa/b2/a0aab285-9678-bfb9-a1dc-eff837ccef12/AppIcon-0-0-85-220-0-5-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Musicr",
    "link": "https://apps.apple.com/us/app/musicr/id6767491157?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a9/4a/77/a94a7753-b1a7-4c9d-3f56-26769b923ccb/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "MyFooderPlan: AI Meal Planner",
    "link": "https://apps.apple.com/us/app/myfooderplan-ai-meal-planner/id1342100580?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/86/e6/e4/86e6e45e-f2dd-abe2-e7df-87fa8450c769/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Mynu",
    "link": "https://apps.apple.com/us/app/mynu/id6767801514?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c0/0d/f5/c00df540-1a01-07b5-3676-cad11d54ea66/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Natus: Self-Discovery Journal",
    "link": "https://apps.apple.com/us/app/natus-self-discovery-journal/id6787200064?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/05/12/da/0512da31-1283-74a1-5c00-3cfba5034c21/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Neko Health",
    "link": "https://apps.apple.com/us/app/neko-health/id6751233363?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4d/d7/e9/4dd7e931-3834-d3b3-ffe7-1716ffbdf381/app-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Nemlys: Couple & Relationship",
    "link": "https://apps.apple.com/us/app/nemlys-couple-relationship/id1662262055?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/76/c8/96/76c8965a-cf8c-fc3b-2a26-0be58f991537/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Neurona: Binaural Beats",
    "link": "https://apps.apple.com/us/app/neurona-binaural-beats/id6757725304?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/1c/77/66/1c776632-25a0-9af7-38ac-2d6c31d42826/icon_composed-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Newgrain: Film Photo Community",
    "link": "https://apps.apple.com/us/app/newgrain-film-photo-community/id6444198677?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/de/fa/c2/defac22e-5f4d-1204-4fb7-7bdeb1dfb970/AppIcon-1x_U007ephone-0-1-0-85-220-0.jpeg/512x512bb.jpg"
  },
  {
    "app": "NextCatch",
    "link": "https://apps.apple.com/us/app/nextcatch/id6758589575?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5c/6c/21/5c6c21ac-7b19-3556-a639-55a63601163a/AppIcon-0-0-1x_U007emarketing-0-6-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Ninou: baby tracker",
    "link": "https://apps.apple.com/us/app/ninou-baby-tracker/id6781282254?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/46/3a/34/463a345a-7f7b-7a93-979a-aa72c3e3e1be/Ninou-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Nitra",
    "link": "https://apps.apple.com/us/app/nitra/id6749408802?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/18/b2/20/18b2200d-64cb-13a9-d567-60fcb8304625/nitra_app_icon-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Notch: Target Scoring Tracker",
    "link": "https://apps.apple.com/us/app/notch-target-scoring-tracker/id6747980153?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d7/83/48/d783480d-74fa-4033-44ce-f4154c3ab7de/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Noteit - AI Journal",
    "link": "https://apps.apple.com/app/id1659178001",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/59/57/b4/5957b419-040d-763a-3c16-cf799bdaaa76/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "NullVault: Secret Photo Vault",
    "link": "https://apps.apple.com/us/app/nullvault-secret-photo-vault/id6793223535?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3b/ff/0b/3bff0b2c-52c3-e5e3-5710-e8cff0da6800/NullVault-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Numby Pro",
    "link": "https://apps.apple.com/us/app/numby-pro/id6755249901?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/82/a7/2e/82a72e8a-3dbd-7536-f153-282682b3bdc2/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "OffScreen: Screen Time Control",
    "link": "https://apps.apple.com/us/app/offscreen-screen-time-control/id1474340105?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/29/00/51/2900519e-a076-de61-c91c-7e0313faf41e/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "oh? daily home screen widget",
    "link": "https://apps.apple.com/us/app/oh-daily-home-screen-widget/id6789181341?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/01/c5/6b/01c56bf4-5653-758b-8eb0-e4253687f800/AppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Ordsnok - Snoop for words",
    "link": "https://apps.apple.com/us/app/ordsnok-snoop-for-words/id6797599523?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/94/bb/c5/94bbc530-63d7-d7a6-ee19-32109782919c/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Otis — A friend to your stocks",
    "link": "https://apps.apple.com/us/app/otis-a-friend-to-your-stocks/id6766045265?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/24/e3/57/24e3572d-cb12-9163-09b1-099d3e997dfd/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Otuli: Shared Baby Tracker",
    "link": "https://apps.apple.com/us/app/otuli-shared-baby-tracker/id6801689523?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d9/5a/24/d95a24c8-829e-1c0e-1112-9bd5954099af/Otuli-0-0-1x_U007ephone-0-0-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "P2P View",
    "link": "https://apps.apple.com/us/app/p2p-view/id1643919225?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f3/cc/c0/f3ccc01d-a2ab-5a17-13d7-5095b55763cc/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Padlok - Keypad manager",
    "link": "https://apps.apple.com/us/app/padlok-keypad-manager/id1546719801?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/98/6e/04/986e049d-e097-e10b-a17d-480bf93eed1b/AppIcon-0-1x_U007ephone-0-0-0-1-0-0-P3-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Pal Cal - AI Calorie Counting",
    "link": "https://apps.apple.com/us/app/pal-cal-ai-calorie-counting/id6758598728?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/02/b1/bf/02b1bf63-175e-cf12-6954-9bd80ecbe56c/ProteinIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pal Chat - AI Chat Client",
    "link": "https://apps.apple.com/us/app/pal-chat-ai-chat-client/id6447545085?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0f/43/12/0f431290-6bfc-8f62-5c1c-b9955f947bbc/GlassIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Palabros - Diccionario",
    "link": "https://apps.apple.com/us/app/palabros-diccionario/id6758098070?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a9/25/1a/a9251ac1-2d3d-c390-ffa7-29c0dc645d45/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "PaperlessHero – AI Mail Task",
    "link": "https://apps.apple.com/us/app/paperlesshero-ai-mail-task/id6758307043?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/48/94/45/48944583-bcb5-a4e8-a1d1-9442a0b79676/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Parrot - AI Personas",
    "link": "https://apps.apple.com/us/app/parrot-ai-personas/id6538716280?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6f/f4/7e/6ff47e89-1f65-92c3-9b9a-2314845acd52/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Parsely",
    "link": "https://testflight.apple.com/join/HjUZtzVm"
  },
  {
    "app": "Peakme: Hike, Track, Explore",
    "link": "https://apps.apple.com/app/id6733216670",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/22/a4/41/22a441e6-20c5-a464-2b0f-3db405633856/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Pebbles: Tax & Visa Tracker",
    "link": "https://apps.apple.com/us/app/pebbles-tax-visa-tracker/id6472096747?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/94/7b/d0/947bd072-9006-bf4f-8aa5-9af8f7ec9b37/AppIcon-0-1x_U007epad-0-1-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Pegma - Peg Solitaire",
    "link": "https://apps.apple.com/us/app/pegma-peg-solitaire/id6754343848?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/37/0f/a2/370fa230-9fc0-5651-ebc2-ed60736d7a23/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Peptide Protocol: Dose Tracker",
    "link": "https://apps.apple.com/us/app/peptide-protocol-dose-tracker/id6762055081?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ff/c5/73/ffc57381-77f1-ded8-db61-f9272cf32cc5/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Perchoir: Coding Agents",
    "link": "https://apps.apple.com/us/app/perchoir-coding-agents/id6800054093?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/79/37/23/793723a9-c5d2-c6d4-1b08-151d0e5c468e/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Petal - AI Voice Recorder",
    "link": "https://apps.apple.com/us/app/petal-ai-voice-recorder/id6759932376?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ec/77/3a/ec773a74-274b-bcd4-4587-7f28cbb0cee2/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Pets Check-in - PeekaPaw",
    "link": "https://apps.apple.com/us/app/pets-check-in-peekapaw/id6759892136?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c9/57/6e/c9576e6d-948b-2e9f-fded-18261f597999/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Photo Cleanup: Swiper",
    "link": "https://apps.apple.com/us/app/photo-cleanup-swiper/id6752829792?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9f/00/7a/9f007a81-ffab-5968-ae29-35add00577b1/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Photo Journal & Mood: Loggoo",
    "link": "https://apps.apple.com/us/app/photo-journal-mood-loggoo/id6799161077?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/fb/cb/04/fbcb0465-34d5-c49f-a341-6df00efd1fc2/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Photo Memo+: Memory Game",
    "link": "https://apps.apple.com/us/app/photo-memo-memory-game/id1607756335?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d5/b0/dd/d5b0dd15-8ea1-d153-da8c-ac8d3533fff9/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Photo2Calendar Scanner Planner",
    "link": "https://apps.apple.com/us/app/photo2calendar-scanner-planner/id6736373394?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6c/e0/2f/6ce02fb3-6c3a-902e-3e15-ca189da30cfe/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PhotoCal: AI Calorie Tracker",
    "link": "https://apps.apple.com/us/app/photocal-ai-calorie-tracker/id6747694364?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/b1/6f/c8/b16fc8e3-34ea-a466-39c4-810c992ce82f/photocaloriecam_icon-2025-09-15-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pickle Rite",
    "link": "https://apps.apple.com/us/app/pickle-rite/id6754095362",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cb/8f/0b/cb8f0b7e-ba14-a1cf-49ef-09c64d9ce87e/AppIcon-0-0-1x_U007ephone-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pico-8 Emulator: Picpic",
    "link": "https://apps.apple.com/us/app/pico-8-emulator-picpic/id6759208792?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e7/cb/5c/e7cb5c08-a96f-f0f3-3642-416e1bba991e/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PicSeek: AI Photo Manager",
    "link": "https://apps.apple.com/us/app/picseek-ai-photo-manager/id6755383937?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f3/90/bc/f390bc5a-185e-095a-7ea2-c4a105bd89f7/AppIcon-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "PicVore: Photo Cleaner",
    "link": "https://apps.apple.com/us/app/picvore-photo-cleaner/id6759577942?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/58/a3/4b/58a34be8-0aed-098e-505b-a497386e5db8/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pievra: Jules AI Coding Agent",
    "link": "https://apps.apple.com/us/app/pievra-jules-ai-coding-agent/id6758630855?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/32/40/cf/3240cf5f-61e5-d99e-b395-a24fae6bb6bd/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pill identifier: MedScan",
    "link": "https://apps.apple.com/us/app/pill-identifier-medscan/id6747271665?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/cd/ad/ed/cdaded5e-e415-ec10-8fd1-b09a032506d0/AppIcon-0-0-1x_U007ephone-0-0-0-1-0-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pillcase - Medication Reminder",
    "link": "https://apps.apple.com/us/app/pillcase-medication-reminder/id6762976257?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/87/1d/91/871d9134-d427-2469-ce00-dd1f5929519b/PillcaseIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PixShrink - HEIF Conv.",
    "link": "https://apps.apple.com/us/app/pixshrink-heif-conv/id6744530166?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a9/b8/c7/a9b8c739-32a1-a66d-9b5c-208089c2a66b/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Placify - Map & Trip Planner",
    "link": "https://apps.apple.com/us/app/placify-map-trip-planner/id6739698007?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/80/48/f5/8048f5c8-8ba9-c684-e574-8f8dfa91544f/AppIcon-0-0-1x_U007ephone-0-0-0-1-0-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Planit: Gamified Daily Planner",
    "link": "https://apps.apple.com/us/app/planit-gamified-daily-planner/id6759006335",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/94/b1/94/94b194db-586e-18bf-bc6f-b7a7acaed8bb/testiconPlanit-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PlayTales: Audiobooks for Kids",
    "link": "https://apps.apple.com/us/app/playtales-audiobooks-for-kids/id6444850972?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/36/4d/38/364d38b5-d054-48f5-0528-3e61a266be29/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Plinky",
    "link": "https://apps.apple.com/us/app/plinky-easily-save-links/id1597187737",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/22/d3/2a/22d32adc-7513-2a04-438a-570d6ad8602e/App_Icon__U0028Primary_U0029-0-1x_U007epad-0-1-4-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Plinth.IT",
    "link": "https://apps.apple.com/us/app/plinth-it/id6746931219",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/2f/57/02/2f570236-cd2e-4d0b-a797-1130d5b68c19/AppIcon-0-1x_U007emarketing-0-11-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Plushify",
    "link": "https://apps.apple.com/us/app/plushify/id6758206706?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/5d/49/25/5d4925ae-e8d1-6d72-5f7c-28f78c126f0f/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PlusProMax",
    "link": "https://apps.apple.com/us/app/pluspromax/id6808135519?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/b8/9b/94/b89b942b-2f3e-360e-1f8d-e4d8abdf0bcd/AppIcon-0-0-1x_U007epad-0-1-0-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Pocket Mole Watch",
    "link": "https://apps.apple.com/us/app/pocket-mole-watch/id6766255492?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/89/bd/10/89bd10b6-2a9a-4292-2eaf-16948b018a7a/AppIcon-0-0-1x_U007emarketing-0-11-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Podcast App & Player - Skimr",
    "link": "https://apps.apple.com/us/app/podcast-app-player-skimr/id6744159946?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/90/e4/3d/90e43d21-87d6-fca6-4e77-749af812e0fb/AppIcon-0-0-1x_U007ephone-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PodSized: AI Podcast Summaries",
    "link": "https://apps.apple.com/us/app/podsized-ai-podcast-summaries/id6737673525?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ec/c6/43/ecc6438f-9c3e-2b6e-34d7-c27e82104512/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pollenomics",
    "link": "https://apps.apple.com/us/app/pollenomics/id6747726612?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0a/d3/06/0ad306d3-f064-8598-c9fe-d2637498d5dd/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pomodoro Timer: Tempo Focus",
    "link": "https://apps.apple.com/us/app/pomodoro-timer-tempo-focus/id6758786811?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3a/3f/ff/3a3fff6a-e780-4867-ed3f-7445137a4be6/TempoDefault-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pool, the screenshot app",
    "link": "https://apps.apple.com/us/app/pool-the-screenshot-app/id6752956163?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e4/af/3d/e4af3d29-eedd-8746-8c2d-f81d233c7fff/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Pop - Find by Color",
    "link": "https://apps.apple.com/us/app/pop-find-by-color/id6758629086?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ee/8a/b3/ee8ab36e-f44e-a3d8-baf5-bf9036af5532/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Popcorn Stack: Track Watchlist",
    "link": "https://apps.apple.com/app/id6759187614",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/1b/9c/62/1b9c62c3-b960-84cd-77a1-f1f4a734a572/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Positive",
    "link": "https://apps.apple.com/us/app/positive/id653287635",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ff/41/f6/ff41f60e-7df4-563f-3872-9cb6490e786f/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PPL Exam Prep - PilotTestZone",
    "link": "https://apps.apple.com/us/app/ppl-exam-prep-pilottestzone/id6741020820?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3e/f5/07/3ef507a5-3bdc-0b13-99d5-ad9d1b2d2bf8/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Presentify - Screen Annotation",
    "link": "https://apps.apple.com/us/app/presentify-screen-annotation/id1507246666?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4b/38/19/4b3819d1-89fa-48bc-1ef6-835d0930d82e/AppIcon-0-0-85-220-0-0-5-0-2x-sRGB-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Prickly Pear: Brain & Hormones",
    "link": "https://apps.apple.com/app/id6654889896",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/7a/04/14/7a04148e-e548-f6bf-8c0e-c26b9157a7cb/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Printer Tools App",
    "link": "https://apps.apple.com/us/app/printer-tools-app/id6745528803?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0c/4b/03/0c4b03c9-821c-d641-4c9a-27eb39885edc/AppIcon-0-1x_U007epad-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "PrivaNota: Private AI Notes",
    "link": "https://apps.apple.com/us/app/privanota-private-ai-notes/id6752555660",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/7f/e6/22/7fe622bc-1260-bcfd-a8e2-c179ba33e727/PrivaNota-0.6-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Progressio: Habit Tracker",
    "link": "https://apps.apple.com/app/progressio-habit-tracker/id6740454461",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0a/a6/3a/0aa63a87-186d-e560-42fe-377337abdd7a/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Prolific",
    "link": "https://apps.apple.com/app/id1666347084",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/72/f7/f5/72f7f5de-2856-e0c4-e453-112ad3ad4e30/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "PronoGP",
    "link": "https://apps.apple.com/us/app/pronogp/id6759366137?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/37/8e/c0/378ec069-9b26-137c-a243-24aeba899783/app-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Protocol Zero: Sleep Sounds",
    "link": "https://apps.apple.com/us/app/protocol-zero-sleep-sounds/id6755014746?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/38/1c/5b/381c5b87-8ab7-7351-138b-0cef5c4d5084/AppIcon-1x_U007ephone-0-1-85-220-0.jpeg/512x512bb.jpg"
  },
  {
    "app": "Public Toilet Finder - Phoo",
    "link": "https://apps.apple.com/us/app/public-toilet-finder-phoo/id6804638882?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/66/93/26/66932699-6d97-3ea6-1638-157ad93bcc85/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "QELM",
    "link": "https://apps.apple.com/us/app/qelm/id6762642772?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/17/10/d6/1710d67e-6275-de37-b0b4-6ff940a4a858/AppIcon-0-0-1x_U007ephone-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "QontactR",
    "link": "https://apps.apple.com/us/app/qontactr/id1169525139?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/dd/e8/78/dde87816-0e87-d38d-9b0a-a0fc93dfd60e/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Quick RSS - RSS Reader",
    "link": "https://apps.apple.com/us/app/quick-rss-rss-reader/id6670696072?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d6/55/7f/d6557fd2-7a41-2057-7ca4-0b67c1c0638c/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "QuickPhrase - Quick Text Input",
    "link": "https://apps.apple.com/us/app/quickphrase-quick-text-input/id6756532440?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e5/28/3e/e5283eca-f679-a08e-a84f-8284e09f044d/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Quietude",
    "link": "https://apps.apple.com/app/id6758955377",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/57/73/ae/5773ae23-b9f6-362c-893f-c2d85e59e916/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Quit Caffeine: Stop Coffee",
    "link": "https://apps.apple.com/us/app/quit-caffeine-stop-coffee/id6756549120?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f0/b8/ba/f0b8bad8-af2d-10f9-e8a6-1c2e421736a1/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "QuitNicPouches Pouch Tracker",
    "link": "https://apps.apple.com/us/app/quitnicpouches-pouch-tracker/id6753104415?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/99/f4/3b/99f43be3-8cd4-448c-26db-bbe190492794/AppIcon-0-0-1x_U007ephone-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "QuitShark: Quit Nicotine",
    "link": "https://apps.apple.com/us/app/quitshark-quit-nicotine/id6759577475?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/97/6c/c4/976cc498-7d3d-2d16-008b-f271ca8edcc2/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Quotify: Quotation Maker",
    "link": "https://apps.apple.com/us/app/quotify-quotation-maker/id6764370347?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f4/37/a4/f437a409-1d7a-a3b1-259f-dce2835c47bc/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "RateTier",
    "link": "https://apps.apple.com/us/app/ratetier/id6759999815?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/eb/49/d8/eb49d8c5-20bb-c2f7-068f-d24a970abb2f/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "rawcast — Terminal Weather",
    "link": "https://apps.apple.com/us/app/rawcast-terminal-weather/id6760224045?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/7b/a4/47/7ba447d5-d934-754b-3f42-2b03670f82b7/AppIcon-0-0-1x_U007epad-0-5-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ReaderFlow | Read Later",
    "link": "https://apps.apple.com/us/app/readerflow-read-later/id6739498537?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/67/1d/d7/671dd77c-6284-7b5e-b3d2-ede360d76269/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Receipts Manager: Receipteka",
    "link": "https://apps.apple.com/us/app/receipts-manager-receipteka/id6474066806?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3c/f7/5a/3cf75a8b-78f9-010e-906d-011849ab7ee2/AppIcon-0-0-1x_U007epad-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "RecipeBee: Recipes & Meal Plan",
    "link": "https://apps.apple.com/us/app/recipebee-recipes-meal-plan/id6757370845?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/86/0e/52/860e52e1-9c23-7868-1f43-b43739700fae/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Recurred: Subscription Manager",
    "link": "https://apps.apple.com/us/app/recurred-subscription-manager/id6783705092?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0c/2b/c3/0c2bc310-a846-bc32-781b-1593b5086b60/recurred-icon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Reed: Breathwork & Pranayama",
    "link": "https://apps.apple.com/us/app/reed-breathwork-pranayama/id6793167045?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/b4/0c/ca/b40ccafb-a182-1baf-fbe9-762d450974d2/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "RegexMate",
    "link": "https://apps.apple.com/us/app/regexmate/id6479819388?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6f/b7/29/6fb72938-5335-c724-e9f9-eaea5533a531/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Reloop - AI Transcribe & Speak",
    "link": "https://apps.apple.com/us/app/reloop-ai-transcribe-speak/id6752853818",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/2e/57/88/2e5788c4-8164-e931-a976-fba05975baac/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Reminders at a Glance",
    "link": "https://apps.apple.com/us/app/reminders-at-a-glance/id6760935916?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/03/19/51/031951c7-0539-a87b-5569-b5b60a6581be/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Repetti",
    "link": "https://apps.apple.com/us/app/repetti-the-chores-list-app/id6758055413",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e8/77/5c/e8775cab-2c64-3b11-e132-c636f3993a44/Icon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Reps: AI Fitness & Recovery",
    "link": "https://apps.apple.com/app/id6746460451",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d4/6b/88/d46b88b6-e459-a316-6e0d-321ccb5c501a/Reps_AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ReShoot · Video Reframe & Crop",
    "link": "https://apps.apple.com/us/app/reshoot-video-reframe-crop/id6753581365?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/52/db/cc/52dbcc8f-c4aa-2cf0-36f9-bdeacf943fd7/AppIcon-0-1x_U007epad-0-11-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Rice: Dinner Planner",
    "link": "https://apps.apple.com/us/app/rice-dinner-planner/id6794866173?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/53/8a/61/538a61f8-c666-76f0-1256-fd8af7479ab0/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Ring Widget",
    "link": "https://apps.apple.com/us/app/ring-widget/id1663323988?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f9/a4/20/f9a420ea-1b6b-83f4-25b5-488aef5a11db/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Ripple News - AI Personal News",
    "link": "https://apps.apple.com/us/app/ripple-news-ai-personal-news/id6758356024?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/19/33/17/193317c5-5112-6151-9fd9-5b98ac41b52d/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "RIRLift: Hypertrophy Log",
    "link": "https://apps.apple.com/us/app/rirlift-hypertrophy-log/id6768680416?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c1/f2/40/c1f24082-408f-d80c-85ea-de71d3238b4d/AppIcon-0-0-1x_U007emarketing-0-6-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Riverside Podcast Video Studio",
    "link": "https://apps.apple.com/us/app/riverside-podcast-video-studio/id1554443872?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/50/64/bd/5064bd5c-0e6c-4b46-dd11-c3c7848c845a/AppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "rivra",
    "link": "https://apps.apple.com/us/app/rivra/id6762450774?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ba/68/2d/ba682da9-d0ba-aee5-de20-60eef09ecf15/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Rooted Mind: Self Growth Coach",
    "link": "https://apps.apple.com/us/app/rooted-mind-self-growth-coach/id6787827688?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a1/49/a2/a149a2bc-b775-66bb-307b-91f7b7ad3800/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Rote.ink",
    "link": "https://apps.apple.com/us/app/rote-ink/id6755513897?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/20/6e/3f/206e3fdc-5999-43aa-d7b3-123de15f0c9a/Rote-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "RTSP & RTMP Cam - OctoStream",
    "link": "https://apps.apple.com/us/app/rtsp-rtmp-cam-octostream/id6474928937?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/60/87/cd/6087cd16-7c94-f635-1634-f17dc68bc00d/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Ryform Golf",
    "link": "https://apps.apple.com/us/app/id6746525972",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/63/a9/01/63a90152-943b-ba07-d666-402df1f9694e/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Safe Period Tracker - Soso",
    "link": "https://apps.apple.com/us/app/safe-period-tracker-soso/id6757295825?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e1/e5/26/e1e526f7-2399-d1b2-a7c0-9603fd42acbf/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SajdaTV",
    "link": "https://apps.apple.com/us/app/sajdatv/id6759179587",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a2/86/a6/a286a647-d984-7f1d-6f2b-7d4fda64c3e2/App_Icon-marketing.lsr/512x512bb.jpg"
  },
  {
    "app": "Sam Webpage Summarizer",
    "link": "https://apps.apple.com/us/app/sam-webpage-summarizer/id6769648029?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/09/1e/b6/091eb6fc-de14-9b7c-e7f6-b1e59b8be9ac/AppIcon-0-0-1x_U007emarketing-0-8-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SAO: Luxury Hotel Journal",
    "link": "https://apps.apple.com/us/app/sao-luxury-hotel-journal/id6757209978?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c5/60/ff/c560ffff-1d5c-15c9-ae25-ce1f62a7396d/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Scap: Screenshot & Markup Edit",
    "link": "https://apps.apple.com/us/app/scap-screenshot-markup-edit/id6758053530?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/95/ed/7c/95ed7cad-90a7-5d3b-8742-444542d3dc55/AppIcon-0-0-85-220-0-0-5-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Scenio: Real Scenes to Speak",
    "link": "https://apps.apple.com/us/app/scenio-real-scenes-to-speak/id6782251685?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/72/e6/99/72e69987-205d-397d-1644-136b36033fe2/AppIcon-0-0-1x_U007emarketing-0-8-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Scooter Tools",
    "link": "https://apps.apple.com/us/app/scooter-tools/id6452079114?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/cf/8e/88/cf8e88b7-50bc-e82f-f37b-734549d82fcd/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Secure Storage: Private Vault",
    "link": "https://apps.apple.com/us/app/secure-storage-private-vault/id6794763277?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/02/4e/89/024e8960-34e7-b77b-3da3-d293b7da39b5/SecureStorage-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Seekly AI - Ask and Chat",
    "link": "https://apps.apple.com/us/app/seekly-ai-ask-and-chat/id6743059382?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/12/5f/81/125f8168-16ef-c611-bac6-141a414a668b/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Set - Strength Log",
    "link": "https://apps.apple.com/us/app/set-strength-log/id6759079224?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4d/80/3d/4d803d98-3f09-a0f4-d5d7-65c6f0f08066/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SF Catalog",
    "link": "https://apps.apple.com/us/app/sf-catalog/id6759371914?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/69/3d/6b/693d6bb5-f68d-1ce3-0ec3-2382a45ffc3d/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SharePal - Quick info sharing!",
    "link": "https://apps.apple.com/us/app/sharepal-quick-info-sharing/id6457261534?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f6/53/0e/f6530e8e-faca-4c14-53c8-23b1f08db727/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Sharing Me: Shared Journal",
    "link": "https://apps.apple.com/us/app/sharing-me-shared-journal/id6756325680?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/53/33/66/53336647-1330-96ad-575f-1bdb47d653d1/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Shluffy - A Baby Sleep Coach",
    "link": "https://apps.apple.com/us/app/shluffy-a-baby-sleep-coach/id6758364198?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/27/e1/3a/27e13a84-a7d7-b9f5-1ee3-51a56210385e/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Shua - arXiv Paper Reader",
    "link": "https://apps.apple.com/us/app/shua-arxiv-paper-reader/id6759848629?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/66/fc/74/66fc74ce-6a9d-7344-618d-5b0e5af2110b/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sift - AI Food Scanner",
    "link": "https://apps.apple.com/us/app/sift-ai-food-scanner/id6760613139?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f4/71/8a/f4718a2c-0eab-61f7-19c4-4c3039dfaaa5/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sight words: First Words Match",
    "link": "https://apps.apple.com/us/app/sight-words-first-words-match/id6499176211?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/25/29/8c/25298c71-4e09-d790-53d1-19abc35b4e17/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Silk: Relationship Tracker",
    "link": "https://apps.apple.com/us/app/silk-relationship-tracker/id6748890004?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f4/31/09/f4310949-7d27-ee90-c6df-c235c860d590/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SilkScreen Mesh Gradient",
    "link": "https://apps.apple.com/us/app/silkscreen-mesh-gradient/id6736693342?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c4/40/c4/c440c47a-4a3c-f6f1-d8f1-d17dc120e59d/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Siraj — Your Ramadan Light",
    "link": "https://apps.apple.com/app/id6759178047",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/9c/1d/75/9c1d7502-e0d7-0db1-bb9e-e9f9edd2c6da/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SiteCalc: Trades Calculator",
    "link": "https://apps.apple.com/us/app/sitecalc-trades-calculator/id6761667513?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/98/db/d6/98dbd62d-1923-b384-c9fe-7233fcf3e8e7/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Skyjo - Score Keeper",
    "link": "https://apps.apple.com/us/app/skyjo-score-keeper/id6754793003?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a4/48/f1/a448f16b-10fc-c35c-61df-fd186411a6e8/AppIcon-0-0-1x_U007ephone-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "SlashPhotos: Storage Cleaner",
    "link": "https://apps.apple.com/us/app/slashphotos-storage-cleaner/id6811618133?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f2/cc/29/f2cc2958-3f45-eeec-4849-a23538ba2e28/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sleep Analysis - Lunomia",
    "link": "https://apps.apple.com/us/app/sleep-analysis-lunomia/id6741118991?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/64/36/fa/6436fa8f-de92-01a9-62a3-52a8c8bd8580/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Slop Or Not - AI Fake Detector",
    "link": "https://apps.apple.com/us/app/slop-or-not-ai-fake-detector/id6744578596?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/fb/f9/05/fbf9052f-756b-1660-e575-62e7a85d73b7/AppIcon-0-0-1x_U007ephone-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Smart Music Stations: Wavyy",
    "link": "https://apps.apple.com/us/app/smart-music-stations-wavyy/id6458877273?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5a/c6/8d/5ac68df8-3d3f-4bf4-984c-06554678468d/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "SmoothCapture",
    "link": "https://www.smoothcapture.app",
    "icon": "https://www.smoothcapture.app/presskit/SmoothCapture-1024x1024@1x.png"
  },
  {
    "app": "SnaPOP: Screenshot Editor",
    "link": "https://apps.apple.com/app/snapop/id6756635978",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/83/d3/34/83d334a4-f19b-047a-8d62-75f1866d1c37/snapop-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "SnapSong - AI Music from Photo",
    "link": "https://apps.apple.com/us/app/snapsong-ai-music-from-photo/id6758351507?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/58/de/7b/58de7b85-a047-2d07-b570-acd38f6e873b/SnapSong-Icon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SnapVinyl",
    "link": "https://apps.apple.com/app/id6741057688",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c3/87/7c/c3877c8c-bac8-537d-453e-98ba2e171fe1/AppIcon-0-1x_U007epad-0-1-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Somilo",
    "link": "https://apps.apple.com/us/app/somilo/id6795198829?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/42/3f/f9/423ff919-5b8c-0617-55c4-6a2f35597704/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sona · AI Journal & Companion",
    "link": "https://apps.apple.com/us/app/sona-ai-journal-companion/id6759681212?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/3c/c2/d8/3cc2d877-adb5-24ea-440b-061d98acedd7/AppIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Soundbooth",
    "link": "https://apps.apple.com/app/id1341485079",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/87/cf/44/87cf44ee-79ee-1a91-3edc-d195ad86582b/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sources: RSS Reader",
    "link": "https://apps.apple.com/us/app/sources-rss-reader/id6792150549?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9e/41/03/9e410326-37a9-7216-3eeb-7860cba43e33/S-RSS-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Space - Life Planner",
    "link": "https://apps.apple.com/us/app/space-life-planner/id6502294348?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/68/88/12/688812e9-4f3d-ce80-5822-5c55e715b809/default-0-0-1x_U007ephone-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sparkler Room",
    "link": "https://apps.apple.com/us/app/sparkler-room/id6807456248?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/22/6a/06/226a06a5-0fe4-2264-8750-66d6d7d4be44/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Speaker Cleaner: Sound Eject",
    "link": "https://apps.apple.com/us/app/speaker-cleaner-sound-eject/id6808605832?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/1b/e2/a8/1be2a854-011a-cefe-f937-3298e10c0f2b/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Speed Reading - WPM Arena",
    "link": "https://apps.apple.com/us/app/speed-reading-wpm-arena/id6757508919?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/07/4c/e6/074ce60c-fca7-5361-ddd9-eff545681cd3/icon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SpeedReader",
    "link": "https://apps.apple.com/app/id6757832936",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/90/bc/20/90bc2003-d998-ba7a-ce45-66d846d7685f/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Spending tracker - Budgy",
    "link": "https://apps.apple.com/us/app/spending-tracker-budgy/id6762646444?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3b/38/f3/3b38f3f6-ac6e-4229-3244-704227340f9c/AppIcon-0-0-1x_U007ephone-0-1-sRGB-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Splitie – Split & Settle Bills",
    "link": "https://apps.apple.com/us/app/splitie-split-settle-bills/id6758516466?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/06/18/ba/0618ba46-ef94-b8d0-9f30-0617c08f284f/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Spottable: Marketplace Alerts",
    "link": "https://apps.apple.com/us/app/spottable-marketplace-alerts/id6758812746?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/32/56/af/3256afd1-4a25-9c3d-5873-bd4492c77200/AppIcon-0-0-1x_U007ephone-0-0-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Spotter - The Travel Companion",
    "link": "https://apps.apple.com/us/app/spotter-the-travel-companion/id6761238251?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/43/63/bd/4363bd1c-3f2d-017b-f15c-d06d89736a88/AppIcon_1-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Spritz",
    "link": "https://apps.apple.com/us/app/spritz-cocktail-recipes/id6748970457?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ff/db/6a/ffdb6a73-be0d-ce05-743b-4a6a6f05101d/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Squeaker - Dog Training Sounds",
    "link": "https://apps.apple.com/us/app/squeaker-dog-training-sounds/id6751776211?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/87/d5/10/87d51098-4022-e6e4-1c8d-7d8a8a545538/Squeaker-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SquishPic: Image Compressor",
    "link": "https://apps.apple.com/us/app/squishpic-image-compressor/id6757522104?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/65/d1/ae/65d1ae1f-5f97-acb6-5458-79560afb84ab/AppIcon-0-0-85-220-0-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Stars of AI",
    "link": "https://apps.apple.com/us/app/stars-of-ai/id6760822530?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/43/07/fe/4307fe6d-8f13-39f8-a508-377ff76b9496/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "StartUp Business: Tycoon Sim",
    "link": "https://apps.apple.com/us/app/startup-business-tycoon-sim/id1629065796?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ca/42/94/ca4294fb-858c-1384-49a3-529c31c8971a/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "Step Counter: Steps App",
    "link": "https://apps.apple.com/us/app/step-counter-steps-app/id6753876664?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6e/08/ae/6e08ae21-ae96-34fb-44fc-62d0b99a802c/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Steplee – Love. Walk. Win.",
    "link": "https://apps.apple.com/us/app/steplee-love-walk-win/id6759522498?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/55/06/28/550628b7-72c9-cec6-86d2-21a45577928b/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Steps: Workout & Pedometer",
    "link": "https://apps.apple.com/us/app/steps-workout-pedometer/id6746096378",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/db/43/04/db430406-c791-e82a-1b4a-4283cca28b78/StepsIcon-0-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Sticky Widgets",
    "link": "https://apps.apple.com/us/app/sticky-widgets/id1533254320",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/1e/b8/78/1eb878a7-0955-eb1a-f55a-09b630362b71/sw-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Stitch It · Long Screenshots",
    "link": "https://apps.apple.com/us/app/stitch-it-long-screenshots/id554594252?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/73/7a/3e/737a3e1e-ccbe-8c01-1249-ac12fcec4a17/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Storyie - AI Journal & Diary",
    "link": "https://apps.apple.com/us/app/storyie-ai-journal-diary/id6742196309?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/b3/a8/e6/b3a8e6e7-02a4-5193-3dfe-d5eae455a3b2/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "StrictBlock: App Blocker",
    "link": "https://apps.apple.com/us/app/strictblock-app-blocker/id6749779711?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d8/18/fc/d818fc87-7a0e-8941-ed31-31321d7bd717/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Subbie - Money Calendar",
    "link": "https://apps.apple.com/us/app/subbie-money-calendar/id6759132543?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c5/07/a8/c507a8be-d488-516e-c971-8005a7751fd1/AppIcon-0-0-1x_U007epad-0-1-0-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SubList: Subscription List",
    "link": "https://apps.apple.com/us/app/sublist-subscription-list/id6757860829?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/45/7e/dd/457edd6b-d9f7-89a7-99b2-72a72cab212f/AppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Subscription Tracker: Submindo",
    "link": "https://apps.apple.com/us/app/subscription-tracker-submindo/id6777319489?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/99/30/b5/9930b5c9-1fb5-45f2-9589-e2c9ec83ef2d/AppIcon-0-0-1x_U007emarketing-0-7-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Summit",
    "link": "https://apps.apple.com/app/id6756911679",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/32/ed/56/32ed5661-a8ff-7f13-47bb-08a87a15fd00/Icon-0-1x_U007epad-0-1-sRGB-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Sunbreak: Nightly App Blocker",
    "link": "https://apps.apple.com/us/app/sunbreak-nightly-app-blocker/id6752121964",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6d/87/66/6d87668f-1acf-38dc-9242-4e571ea81a0e/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Sunrise Sunset Time SolarWatch",
    "link": "https://apps.apple.com/us/app/sunrise-sunset-time-solarwatch/id1191365122?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/07/93/2e/07932e05-1ccc-04ea-7dc9-65ed5dffcf1c/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Super Shot: EXIF Photo Frame",
    "link": "https://apps.apple.com/us/app/super-shot-exif-photo-frame/id6760384143?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/49/01/f9/4901f9f9-8d90-7283-bbec-6e506aec4391/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Swayni: Custom Workout Timer",
    "link": "https://apps.apple.com/us/app/swayni-custom-workout-timer/id6795447541?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/73/a6/c7/73a6c72a-0087-866f-ce05-8cf8e64c5e36/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "SweatCount - Workout Calendar",
    "link": "https://apps.apple.com/us/app/sweatcount-workout-calendar/id6755407742?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9c/4a/88/9c4a88f0-7ce8-6c72-8eb4-6462b67625e5/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "SwiftBiu",
    "link": "https://apps.apple.com/app/swiftbiu/id6754772331",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/90/3b/2e/903b2ef1-7097-f00c-3557-db67e054e174/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "SwiftyMenu",
    "link": "https://apps.apple.com/us/app/swiftymenu/id1567748223?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e6/0a/2c/e60a2c36-1e5c-f018-3a56-edc1fd1bfa40/AppIcon.png/512x512bb.png"
  },
  {
    "app": "Swooni: Relationship Tracker",
    "link": "https://apps.apple.com/us/app/swooni-relationship-tracker/id6557063166?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ce/6a/7e/ce6a7e31-0c22-1d6d-20e6-5fb7e5b16171/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "takt - car glance",
    "link": "https://apps.apple.com/us/app/takt-car-glance/id6799290528?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/8f/6a/ae/8f6aaef7-9f39-5dbe-3573-439b29d12f1a/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Talli",
    "link": "https://apps.apple.com/us/app/talli/id6782366555?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/63/8e/dd/638edd28-f1d3-5002-d912-7511ff80de7f/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Tamloot",
    "link": "https://apps.apple.com/il/app/tamloot/id6758220887",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/26/a3/f7/26a3f7f9-3cb6-dc8e-fcbd-0fc93630dd4c/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "TapTape",
    "link": "https://apps.apple.com/us/app/taptape/id6748304805?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/b8/47/72/b8477252-9820-e366-84fc-4736974702a6/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "TeethPath",
    "link": "https://apps.apple.com/us/app/teethpath/id6740765612?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/9d/5a/d8/9d5ad8ac-b8bc-af9e-33f0-32a4d98cba6e/AppIcon-0-0-1x_U007ephone-0-0-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Telescopo: Markdown & Diagrams",
    "link": "https://apps.apple.com/us/app/telescopo-markdown-diagrams/id6747908871?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/eb/ba/d4/ebbad4d9-6a72-5d73-9a18-cf2719e6b8f3/telescopo-icon-0-0-85-220-0-6-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Terraforge: Alchemy Craft Game",
    "link": "https://apps.apple.com/us/app/terraforge-alchemy-craft-game/id6759879809?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f4/f1/65/f4f165e3-9826-8ced-d9be-d33b8a8e154c/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Tessera - Car Black Box",
    "link": "https://apps.apple.com/us/app/tessera-car-black-box/id6778605690?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/6a/8b/a1/6a8ba19d-72e9-bd02-a39a-188c9d936f9f/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "text to speech · speakeasy",
    "link": "https://apps.apple.com/app/id6758816495",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/0a/49/48/0a49486e-92b3-37b5-8bb8-c038aee9a0a4/speakeasy-icon-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ThaiQuest",
    "link": "https://apps.apple.com/us/app/thaiquest/id6759592901?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/49/51/6b/49516bdb-9a33-055e-3296-38f930c5d311/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "The Pot - Stake your claim",
    "link": "https://apps.apple.com/us/app/the-pot-stake-your-claim/id6760322444?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ef/1e/04/ef1e04d3-6b41-92d8-417f-de50d9f07005/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Thought Ease: End Overthinking",
    "link": "https://apps.apple.com/app/id6759291269",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/38/89/82/3889821a-7a9e-d068-89ca-937b482923ed/AppIcon-0-0-1x_U007emarketing-0-7-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "TickerPad",
    "link": "https://apps.apple.com/app/id1578860989",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d0/1b/19/d01b1977-6842-0528-9447-6e24a8cc1bf1/AppIcon-0-0-85-220-0-0-5-0-2x-0-0-0.png/512x512bb.png"
  },
  {
    "app": "Tilt: Shop Live Fashion Deals",
    "link": "https://apps.apple.com/us/app/tilt-shop-live-fashion-deals/id1588409720?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3a/1f/f2/3a1ff2df-b4f5-d67d-ba51-2f30d281b871/AppIcon-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Time Blocking Planner: Align",
    "link": "https://apps.apple.com/us/app/time-blocking-planner-align/id6740183716?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cf/17/ad/cf17adb8-589d-0239-b33f-85adee7c3e26/Align_icon_26-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Timefall",
    "link": "https://apps.apple.com/app/id6757712804",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d5/00/51/d500514a-c728-8598-f562-105dcc8005b9/AppIcon-0-0-1x_U007epad-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Tintz: Color Picker",
    "link": "https://apps.apple.com/us/app/tintz-color-picker/id6748126014?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/53/33/80/5333806a-5c46-8d24-6f30-fc455847922e/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Tiny Decisions",
    "link": "https://apps.apple.com/us/app/tiny-decisions/id1338769645?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/57/19/ea/5719eae3-87a1-f412-34a1-4223cc8315c7/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Tirkit - Tabla Tanpura Riyaz",
    "link": "https://apps.apple.com/us/app/tirkit-tabla-tanpura-riyaz/id6794581805?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c0/0a/a9/c00aa9ac-b310-0b34-32fb-84ebc108b2ca/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "TMS - Bible Memory",
    "link": "https://apps.apple.com/us/app/tms-bible-memory/id6463799590?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/9a/7a/a3/9a7aa376-7dd9-8939-d6ba-90012b0cf417/tmsAppIcon-0-0-1x_U007ephone-0-1-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ToMe: Save to Self",
    "link": "https://apps.apple.com/app/id6755589008",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ea/39/63/ea3963e1-524c-7402-fe36-f930ddedcc36/ToMe_Liquid-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Torqs",
    "link": "https://apps.apple.com/us/app/torqs/id6754500631",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/1d/b0/ea/1db0eaa8-4504-cbe3-b472-5eab73d634c9/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Trailbot: MTB Trail Conditions",
    "link": "https://apps.apple.com/us/app/trailbot-mtb-trail-conditions/id1559573510?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/12/a4/56/12a456ea-73f0-3972-9677-904b25e8c2e3/TrailbotGlass-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Train RunBuddy",
    "link": "https://apps.apple.com/au/app/train-runbuddy/id6738111722?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a3/7b/04/a37b0451-b55b-4b78-b5b1-93dc9e10380a/AppIcon-0-0-1x_U007epad-0-0-0-1-0-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Trip Atar",
    "link": "https://apps.apple.com/us/app/trip-atar/id6758254251?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ac/1a/a1/ac1aa167-3a2b-8d1e-d0aa-4e2c5bfb1c9b/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Truessay: Academic AI Writer",
    "link": "https://apps.apple.com/us/app/truessay-academic-ai-writer/id6757461906?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ea/b1/01/eab10103-3a5d-9de7-5094-0e6233c9c034/AppIcon-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Tunable – Tuner & Metronome",
    "link": "https://apps.apple.com/us/app/tunable-tuner-metronome/id608540795?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple115/v4/93/bd/99/93bd992d-825e-df5d-eedb-550eb17895c5/AppIcon-1x_U007emarketing-0-10-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "TV Graphs: Season Ratings",
    "link": "https://apps.apple.com/us/app/tv-graphs-season-ratings/id6804961357?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/29/b7/b8/29b7b8fa-4bc1-1a38-0ac7-a6c113e03b6f/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "TV Show Tracker",
    "link": "https://apps.apple.com/us/app/tv-show-tracker-tv-club/id6497563903",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/43/b8/67/43b86702-f67b-c314-f65b-cd60980205b0/Icon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "TypeSafe: Verify & Protect",
    "link": "https://apps.apple.com/us/app/typesafe-verify-protect/id6754827519?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5e/d6/b3/5ed6b308-6268-474d-1009-5ba153d05266/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ulives: Gamify Tasks & Habits",
    "link": "https://apps.apple.com/us/app/ulives-gamify-tasks-habits/id6743084261?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/ed/78/34/ed783481-8296-ea6e-eb95-644a7f058353/AppIconDefault-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Unlimited Clipboard History",
    "link": "https://apps.apple.com/us/app/unlimited-clipboard-history/id6705136056",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/6a/1e/8f/6a1e8f8e-1f53-fcde-f26b-f814416eeb69/glass-0-0-85-220-0-6-0-2x-sRGB.png/512x512bb.png"
  },
  {
    "app": "Unseen Accessibility",
    "link": "https://apps.apple.com/us/app/unseen-accessibility/id6770270032?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/59/52/01/595201c5-02a2-75ac-0e10-a00f7a4bd04c/AppIcon-0-0-1x_U007epad-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "UrbanAir: Unlock all escooters",
    "link": "https://apps.apple.com/us/app/urbanair-unlock-all-escooters/id1491025864?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple122/v4/7f/75/08/7f750858-b379-719d-f98c-06e4fbc9a984/AppIcon-0-0-1x_U007emarketing-0-0-0-10-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/512x512bb.jpg"
  },
  {
    "app": "Vakta: Prayer Times & Quran",
    "link": "https://apps.apple.com/us/app/vakta-prayer-times-quran/id6759549958?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c0/a4/8e/c0a48e40-32c3-8e31-6779-9b75593d9e38/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Vaultaire: Photo Vault",
    "link": "https://apps.apple.com/us/app/vaultaire-photo-vault/id6758529311?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3f/90/6f/3f906ff7-be78-e596-b72c-c5dee3bb0b7c/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Velko: IPTV Player & TV Guide",
    "link": "https://apps.apple.com/us/app/velko-iptv-player-tv-guide/id6775969853?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/53/c3/b7/53c3b765-6630-5db0-a5f8-abf88f38d181/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Verbi: Speak a New Language",
    "link": "https://apps.apple.com/us/app/verbi-speak-a-new-language/id6760734657?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/9a/75/c8/9a75c847-6917-09ba-3f07-9ddc003b25e4/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Victus: Discipline & Habits",
    "link": "https://apps.apple.com/us/app/victus-discipline-habits/id6754204999?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/cc/a6/0a/cca60a66-1e16-573f-043e-97f4c092a07d/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Video Annotation",
    "link": "https://apps.apple.com/us/app/video-annotation/id6758316355",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/43/c3/a9/43c3a9e9-e32b-081c-8a8b-a4f401846771/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Videoer",
    "link": "https://apps.apple.com/us/app/videoer/id6742680573?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f6/51/f5/f651f5b7-8052-5407-77ec-a4203bbf456c/AppIcon-0-0-85-220-0-5-0-2x.png/512x512bb.png"
  },
  {
    "app": "Vigil: Situation Room",
    "link": "https://apps.apple.com/us/app/vigil-situation-room/id6773060534?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5e/39/70/5e397004-de16-52f5-051c-03a5bf09db23/AppIcon.lsr/512x512bb.jpg"
  },
  {
    "app": "Vintsnap: AI Listing Photos",
    "link": "https://apps.apple.com/us/app/vintsnap-ai-listing-photos/id6761347142?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/43/ab/e6/43abe6b7-c579-18a2-c562-be9663d47a82/vintsnap-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Visa Day Tracker: Residency",
    "link": "https://apps.apple.com/us/app/visa-day-tracker-residency/id6749048224?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/73/d0/88/73d088d4-104c-b8ea-4b0d-863da7932f6d/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "VivaDicta - Speech to Text",
    "link": "https://apps.apple.com/us/app/vivadicta-speech-to-text/id6758147238?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/e4/3d/81/e43d810c-3bf3-ea24-9aa6-1ced116d0f1d/AppIcon3-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "VivyShot: Screen Studio",
    "link": "https://apps.apple.com/us/app/vivyshot-screen-studio/id6760658121?mt=12&uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cb/3a/3d/cb3a3d36-ba8b-6e36-a332-5a51209a2a86/AppIcon-85-220-0-5-0-0-2x-0-0.png/512x512bb.png"
  },
  {
    "app": "Voice Do",
    "link": "https://apps.apple.com/us/app/voice-do-task-manager/id6746357453",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/27/ed/a2/27eda257-5dd2-5150-7b0e-977f35636552/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Voice Reminder Planner: Echo",
    "link": "https://apps.apple.com/us/app/voice-reminder-planner-echo/id6752544263?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/05/25/f3/0525f38c-b2cd-3b22-c1d7-420535cc43f4/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "VoiceBee: AI Voice Keyboard",
    "link": "https://apps.apple.com/us/app/voicebee-ai-voice-keyboard/id6760564766?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/dd/b5/26/ddb52642-a03e-5ff7-ad0c-d973c80726c6/AppIcon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Voxel Max: 3D Modeling",
    "link": "https://apps.apple.com/us/app/voxel-max-3d-modeling/id1442352186?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f3/59/69/f35969ec-2a16-d98a-830a-55d92ce8ff81/VoxelMax-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "VVTerm: Native SSH Client",
    "link": "https://apps.apple.com/us/app/vvterm-native-ssh-client/id6757482822?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/df/80/ae/df80aeb3-38cc-afe1-e15c-e0b27e957f66/AppIcon-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Wander: Markdown Editor",
    "link": "https://apps.apple.com/us/app/wander-markdown-editor/id6791206855?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/86/fc/6f/86fc6f47-00d7-4465-b567-fe4246d47945/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Wat Kham",
    "link": "https://apps.apple.com/us/app/wat-kham/id6760705576?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/86/4a/45/864a4587-87e9-4a25-e53e-2e9e025800e0/AppIcon-1x_U007emarketing-0-8-0-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Watchletic - Run tracker",
    "link": "https://apps.apple.com/us/app/watchletic-run-tracker/id1606429298?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/e0/4b/bd/e04bbd15-6b77-184e-381f-3d5e5654d97b/app-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "WatchRadar: Watches Identifier",
    "link": "https://apps.apple.com/us/app/watchradar-watches-identifier/id6760263209?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c9/7e/e0/c97ee08b-0e99-5233-ebcc-acba477cafd3/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Weather mini - Trip Forecasts",
    "link": "https://apps.apple.com/us/app/weather-mini-trip-forecasts/id892589817",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/3c/c6/c1/3cc6c19c-acec-fb4d-ee88-63a1bfc422c4/AppIcon-0-0-1x_U007epad-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Woofdoku: Sudoku with Dogs",
    "link": "https://apps.apple.com/us/app/woofdoku-sudoku-with-dogs/id6799775594?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/84/4b/eb/844beb9b-2b97-1180-fb47-81f76f90a701/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "WordFlux: Word Puzzles",
    "link": "https://apps.apple.com/us/app/wordflux-word-puzzles/id6757514622?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/eb/e9/b7/ebe9b79a-9dd1-d0a7-3bd9-ecb2a4517c83/AppIcon-0-0-1x_U007ephone-0-1-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Wordroot",
    "link": "https://apps.apple.com/us/app/wordroot/id6794988021?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/4b/9d/f9/4b9df903-d342-3152-ec35-c823dcd86bc6/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "World Clock Time Zones - Timo",
    "link": "https://apps.apple.com/us/app/world-clock-time-zones-timo/id6762064286?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/c9/df/36/c9df3620-c419-146f-154a-746d9f7a9f45/Icon-0-0-1x_U007ephone-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Wozi Vocabulary Builder",
    "link": "https://apps.apple.com/app/id1536585848",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/cd/33/e3/cd33e3a5-2c7d-34e8-5c22-07bca8f44d7d/WoziAppIcon-0-0-1x_U007epad-0-1-P3-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Writone - Reply Assistant",
    "link": "https://apps.apple.com/app/id6758207392",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/d8/48/f4/d848f4b9-7de8-3d14-57d0-bf534e142069/AppIcon-1x_U007ephone-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "wrkout - gym plan, log & track",
    "link": "https://apps.apple.com/us/app/wrkout-gym-plan-log-track/id6759204353?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/01/0e/ce/010ece8d-5bcf-70fc-ef4a-7ed6c8aaa6ef/Logo-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "XO: Have I Ever",
    "link": "https://apps.apple.com/us/app/xo-have-i-ever/id6757594745",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/d7/4d/4c/d74d4c07-c147-fa7e-86fe-cff01727629b/XO-0-0-1x_U007ephone-0-1-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Yabaleli",
    "link": "https://apps.apple.com/us/app/yabaleli/id6748582441?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/a4/78/92/a4789210-875e-8984-6ef0-5f2c034b53fd/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "YouInMeme: Face Swap Memes",
    "link": "https://apps.apple.com/us/app/youinmeme-face-swap-memes/id6760035781?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/0e/38/cb/0e38cb41-820e-8d78-2547-eb2f945a66b2/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Your Next Tours: Audio Guide",
    "link": "https://apps.apple.com/us/app/your-next-tours-audio-guide/id6783743090?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/a2/9a/cb/a29acbaa-94ac-05f9-3c76-84b654782781/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "Zen Flip Clock - Minimal Timer",
    "link": "https://apps.apple.com/us/app/zen-flip-clock-minimal-timer/id1265404088?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/4b/f4/13/4bf4138d-b804-9563-f684-92a9a2845b16/zfc-glass-icon-0-0-1x_U007epad-0-0-0-1-0-0-sRGB-85-220.png/512x512bb.jpg"
  },
  {
    "app": "ZenJournal:Stress-free Journal",
    "link": "https://apps.apple.com/us/app/zenjournal-stress-free-journal/id1399816360?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/33/eb/e1/33ebe137-a850-63be-ef48-7a7a804ec151/ZenJournalIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "ZenShelf",
    "link": "https://apps.apple.com/app/id6670147622",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/61/5a/dc/615adc54-4abd-f5f0-34a8-0ee239868470/AppIcon-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "Zenther: AI Calorie Tracker",
    "link": "https://apps.apple.com/us/app/zenther-ai-calorie-tracker/id6748252780?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/c6/49/14/c6491404-390d-82e2-3c3c-1b6053594311/Zenther-0-1x_U007epad-0-1-85-220-0.png/512x512bb.jpg"
  },
  {
    "app": "你的话 - 懂你的人在这",
    "link": "https://apps.apple.com/us/app/%E4%BD%A0%E7%9A%84%E8%AF%9D-%E6%87%82%E4%BD%A0%E7%9A%84%E4%BA%BA%E5%9C%A8%E8%BF%99/id6760377155?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/ef/91/e6/ef91e682-10d1-f57b-a5d3-20de2e4c0fe7/AppIcon-0-0-1x_U007emarketing-0-11-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "吃了吗您",
    "link": "https://apps.apple.com/us/app/%E5%90%83%E4%BA%86%E5%90%97%E6%82%A8/id6757486351?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/7d/b2/56/7db25643-249f-507e-6bd5-2085d64e5b13/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "极品ID",
    "link": "https://apps.apple.com/us/app/%E6%9E%81%E5%93%81id/id6744751474?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/8f/8c/05/8f8c0557-70e8-9a04-eb6c-4eb79d366392/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "知了，RSS智能聚合阅读",
    "link": "https://apps.apple.com/app/id1525056652",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/11/3e/43/113e432e-0c18-db1b-fff8-0dec1e3d1fcb/AppIcon-0-0-1x_U007emarketing-0-6-0-85-220.png/512x512bb.jpg"
  },
  {
    "app": "织诗 - AI写诗与古诗词创作",
    "link": "https://apps.apple.com/us/app/%E7%BB%87%E8%AF%97-ai%E5%86%99%E8%AF%97%E4%B8%8E%E5%8F%A4%E8%AF%97%E8%AF%8D%E5%88%9B%E4%BD%9C/id6777856347?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/4b/05/11/4b051114-60b4-13e8-f567-23d7f4170600/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "补光HDR相机",
    "link": "https://apps.apple.com/us/app/%E8%A1%A5%E5%85%89hdr%E7%9B%B8%E6%9C%BA/id6757137922?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/89/3d/5b/893d5b0e-f1c0-bf21-8292-4fcd4847bf67/AppIcon-0-0-1x_U007epad-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "表魂",
    "link": "https://apps.apple.com/us/app/%E8%A1%A8%E9%AD%82/id6756651527?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/41/58/86/41588611-db95-a624-2fd6-f2ee3a069f71/AppIcon-0-0-1x_U007ewatch-0-1-85-220.jpeg/512x512bb.jpg"
  },
  {
    "app": "词忆MemQuirk",
    "link": "https://apps.apple.com/us/app/%E8%AF%8D%E5%BF%86memquirk/id6756350749?uo=4",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/12/8c/44/128c4424-fd03-64c3-1e8e-8cf913513550/AppIcon-0-0-1x_U007epad-0-1-85-220.png/512x512bb.jpg"
  },
  {
    "app": "시선 - 사진 한 장, 시 한 줄",
    "link": "https://apps.apple.com/app/id6748271659",
    "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/4e/a3/3b/4ea33b80-8dcb-11ec-4699-6ccff7efcfa9/AppIcon-0-0-1x_U007emarketing-0-8-0-85-220.png/512x512bb.jpg"
  }
]

---

### Guides Agent Ios Release (`guides/agent-ios-release.mdx`)

# Agent iOS Release Checklist

> Use this checklist as an agent release gate. See
> [App Store Submission](/guides/app-store-submission) for the full workflow and
> troubleshooting guide.

This page adds the agent-specific contract: resolve exact resources, use scoped
validation, preview each mutation, and prove the final App Store Connect
state before reporting success.

## Operating rules

* Run `--help` for every command before using it.
* Resolve the app ID and version target before changing anything. Resolve the
  build ID before staging or submitting.
* Use the most specific `asc` command for the workflow, including `asc web` when the operation lives there.
* Preview supported workflows with `--dry-run`. Use `--confirm` only after
  checking every identifier available at that stage.
* Never print credentials or personal identity values in logs or handoffs.
* A successful mutation response is not proof that the version entered review.

## 1. Validate authentication and resolve the target

Select the intended credential source before making a network request. When
using a named profile:

```bash  theme={null}
export ASC_PROFILE="release"
asc auth status --output table
```

If `ASC_PROFILE` is unset, confirm that the status shows the intended default
profile or environment credential. Do not use `asc auth status --validate` as
the release gate because it validates every stored credential. The filtered
`asc apps list` call below is the scoped network validation for the selected
credential and must succeed.

Resolve the app and release version:

```bash  theme={null}
asc apps list --bundle-id "com.example.app" --output table

export ASC_APP_ID="1234567890"
export VERSION="1.2.3"

asc versions list \
  --app "$ASC_APP_ID" \
  --version "$VERSION" \
  --platform IOS \
  --output json
```

For a build that is already in App Store Connect, resolve a valid processed
build and record its ID:

```bash  theme={null}
export BUILD_NUMBER="42"

asc builds info \
  --app "$ASC_APP_ID" \
  --build-number "$BUILD_NUMBER" \
  --version "$VERSION" \
  --platform IOS \
  --output json

export ASC_BUILD_ID="BUILD_ID"
```

Continue only when the response is for the expected build number and its
processing state is `VALID`.

Skip the build lookup when uploading a local IPA; resolve the build ID after
upload and processing. Do not create a duplicate version. The staging and
publishing commands below can create a missing version for their workflows.

## 2. Prepare the version

Choose the path that matches the current release state.

### Stage an existing processed build

Preview the staging plan:

```bash  theme={null}
asc release stage \
  --app "$ASC_APP_ID" \
  --version "$VERSION" \
  --build-id "$ASC_BUILD_ID" \
  --platform IOS \
  --copy-metadata-from "1.2.2" \
  --dry-run \
  --output table
```

A dry-run cannot attach the build. If its only readiness blocker is the missing
relationship for the exact resolved build, the confirmed pipeline fixes that
condition before its readiness step. Resolve every other blocker first.

After checking the resolved resources and plan, run the confirmed preparation
pipeline. It applies metadata and attaches the build. The final release gate runs
after any digital goods are submitted:

```bash  theme={null}
asc release stage \
  --app "$ASC_APP_ID" \
  --version "$VERSION" \
  --build-id "$ASC_BUILD_ID" \
  --platform IOS \
  --copy-metadata-from "1.2.2" \
  --confirm \
  --output table
```

### Upload and attach a local IPA

Preview the upload and preparation plan:

```bash  theme={null}
asc publish appstore \
  --app "$ASC_APP_ID" \
  --ipa "./build/ios/ipa/MyApp.ipa" \
  --version "$VERSION" \
  --platform IOS \
  --wait \
  --dry-run \
  --output table
```

After checking the plan, run the preparation command and request structured
output:

```bash  theme={null}
asc publish appstore \
  --app "$ASC_APP_ID" \
  --ipa "./build/ios/ipa/MyApp.ipa" \
  --version "$VERSION" \
  --platform IOS \
  --wait \
  --output json
```

Do not add `--submit`; release validation must run against the build after it is
attached. Set `ASC_BUILD_ID` to the exact `buildId` returned by this command and
retain its `buildNumber`:

```bash  theme={null}
export ASC_BUILD_ID="BUILD_ID_FROM_PUBLISH_RESULT"
export BUILD_NUMBER="BUILD_NUMBER_FROM_PUBLISH_RESULT"
```

Never re-resolve an uploaded build with `--latest`. If the result must be
re-read, query the known build number and verify that the response has the same
build ID and a `VALID` processing state.

If the version is already prepared and has the intended build attached, skip
preparation and continue to the readiness gate.

## 3. Gate readiness and submit

After the build is attached, apps with digital goods must inspect and submit
their products before using the final readiness gate:

```bash  theme={null}
asc validate iap --app "$ASC_APP_ID" --output json
asc validate subscriptions --app "$ASC_APP_ID" --output json
```

Resolve the exact product `resourceId` and its current version ID for every
product included in this release. Use `asc iap versions list` and
`asc subscriptions versions list` to find versions in
`PREPARE_FOR_SUBMISSION`. `READY_TO_SUBMIT` is an action item, not a passing
state.

Create one review submission, retain its returned ID, and add every affected
product version. A submission supports up to 200 items:

```bash  theme={null}
asc review submissions-create \
  --app "$ASC_APP_ID" \
  --platform IOS \
  --output json

asc review items add \
  --submission "SUBMISSION_ID" \
  --item-type inAppPurchaseVersions \
  --item-id "IAP_VERSION_ID" \
  --output json

asc review items add \
  --submission "SUBMISSION_ID" \
  --item-type subscriptionVersions \
  --item-id "SUBSCRIPTION_VERSION_ID" \
  --output json

asc review submissions-submit \
  --id "SUBMISSION_ID" \
  --confirm \
  --output json
```

These review-submission commands do not support `--dry-run`; use the validation
report and exact product and version IDs as the pre-mutation check. Do not
submit the review submission until every intended item has been added.

After product submission, rerun the applicable validators:

```bash  theme={null}
asc validate iap --app "$ASC_APP_ID" --output json
asc validate subscriptions --app "$ASC_APP_ID" --output json
```

For every selected product ID, require the review-readiness warning to disappear
and verify a state such as `WAITING_FOR_REVIEW`, `IN_REVIEW`,
`PENDING_BINARY_APPROVAL`, or `APPROVED`. These validators cover the full product
catalog and do not accept a product filter. Do not add `--strict` as an app
release gate when unrelated draft or legacy products exist.

Then run the unified app-version validation:

```bash  theme={null}
asc validate \
  --app "$ASC_APP_ID" \
  --version "$VERSION" \
  --platform IOS \
  --output json
```

Require `summary.blocking` to be `0`, and resolve every warning that applies to
the target version, build, or selected products. Non-strict output keeps
unrelated product warnings advisory; it does not make them safe to ignore when
their resource IDs are part of this release.

Do not submit while a required metadata, build, product, privacy, or review
check is failing. When all applicable gates pass, preview the submission:

```bash  theme={null}
asc review submit \
  --app "$ASC_APP_ID" \
  --version "$VERSION" \
  --build-id "$ASC_BUILD_ID" \
  --platform IOS \
  --dry-run \
  --output table
```

After checking the preview, repeat that command with `--confirm` instead of
`--dry-run`.

## 4. Prove the final state

Re-read App Store Connect and include the attached build and submission:

```bash  theme={null}
asc status \
  --app "$ASC_APP_ID" \
  --platform IOS \
  --include builds,appstore,submission,review \
  --output json

asc versions view \
  --version-id "VERSION_ID" \
  --include-build \
  --include-submission \
  --output json

asc submit status --version-id "VERSION_ID" --output json
```

The handoff must name the app ID or bundle ID, version string, version ID, build
number or build ID, submission ID when available, and the observed state. Report
a draft as a draft. Only claim submission after the exact version and build show
a review state such as `WAITING_FOR_REVIEW`, `IN_REVIEW`, or
`PENDING_DEVELOPER_RELEASE`.

## Web fallback

Use an explicit `asc web` command when the operation lives in the web-session
command family. `asc web auth login` supports interactive two-factor authentication,
but account-holder agreements,
payment issues, CAPTCHA, and unsupported surfaces may still require the browser.

---

### Guides Analytics Reports (`guides/analytics-reports.mdx`)

# Analytics and Reports

> Download sales, analytics, and financial reports from App Store Connect

Access and download comprehensive analytics, sales, and financial reports for your apps.

## Overview

The CLI provides access to:

* **Sales Reports**: Daily, weekly, and monthly sales data
* **Analytics Reports**: App usage, crashes, and performance metrics
* **Financial Reports**: Revenue and payment information
* **Custom Analytics**: Request specific metrics and segments

## Sales Reports

Sales reports provide transaction-level data for your apps.

### Download Daily Sales Report

```bash  theme={null}
asc analytics sales \
  --vendor "YOUR_VENDOR_NUMBER" \
  --type SALES \
  --subtype SUMMARY \
  --frequency DAILY \
  --date "2024-01-20"
```

### Download Weekly Sales Report

```bash  theme={null}
asc analytics sales \
  --vendor "YOUR_VENDOR_NUMBER" \
  --type SALES \
  --subtype SUMMARY \
  --frequency WEEKLY \
  --date "2024-01-21"
```

### Download Monthly Sales Report

```bash  theme={null}
asc analytics sales \
  --vendor "YOUR_VENDOR_NUMBER" \
  --type SALES \
  --subtype SUMMARY \
  --frequency MONTHLY \
  --date "2024-01-01"
```

### Sales Report Types

* **SALES**: Standard sales transactions
* **PRE\_ORDER**: Pre-order transactions
* **NEWSSTAND**: Newsstand sales (if applicable)
* **SUBSCRIPTION**: Subscription events
* **SUBSCRIPTION\_EVENT**: Detailed subscription lifecycle events

### Sales Report Subtypes

* **SUMMARY**: Aggregated sales summary
* **DETAILED**: Transaction-level details
* **OPT\_IN**: Reports for apps with opted-in features

<Note>
  The vendor number is found in App Store Connect under **Sales and Trends > Reports**. Set it as `ASC_VENDOR_NUMBER` environment variable to avoid repeating it.
</Note>

## Analytics Reports

Request custom analytics reports for detailed app metrics.

### Create Analytics Request

<Steps>
  <Step title="Request analytics report">
    Request ongoing analytics access and reuse an existing active request when possible:

    ```bash  theme={null}
    asc analytics request \
      --app "YOUR_APP_ID" \
      --access-type ONGOING \
      --reuse-existing
    ```

    Or create a request directly:

    ```bash  theme={null}
    asc analytics request \
      --app "YOUR_APP_ID" \
      --access-type ONGOING
    ```

    For one-time reports:

    ```bash  theme={null}
    asc analytics request \
      --app "YOUR_APP_ID" \
      --access-type ONE_TIME_SNAPSHOT
    ```
  </Step>

  <Step title="List analytics requests">
    View all analytics requests for your app:

    ```bash  theme={null}
    asc analytics requests --app "YOUR_APP_ID"
    ```
  </Step>

  <Step title="Check request status">
    Get detailed status of a request:

    ```bash  theme={null}
    asc analytics view --request-id "REQUEST_ID"
    ```
  </Step>

  <Step title="List available report instances">
    Once the request is processed, inspect the available reports and instances:

    ```bash  theme={null}
    asc analytics view --request-id "REQUEST_ID"
    ```
  </Step>
</Steps>

### Download Analytics Data

Download the actual report data:

```bash  theme={null}
asc analytics download \
  --request-id "REQUEST_ID" \
  --instance-id "INSTANCE_ID" \
  --output "./analytics-report.csv"
```

### Analytics Report Segments

Segment analytics by various dimensions:

```bash  theme={null}
asc analytics segments view \
  --segment-id "SEGMENT_ID" \
  --output json
```

Common segments include:

* App version
* Device type
* Platform version
* Territory
* Source type

## Financial Reports

Access payment and financial information:

### Download Financial Report

```bash  theme={null}
asc finance reports \
  --vendor "YOUR_VENDOR_NUMBER" \
  --report-type FINANCIAL \
  --region "ZZ" \
  --date "2024-01"
```

### Report Regions

Financial reports are organized by region:

```bash  theme={null}
asc finance reports \
  --vendor "YOUR_VENDOR_NUMBER" \
  --report-type FINANCIAL \
  --region "US" \
  --date "2024-01"
```

Common region codes:

* `US` - United States
* `EU` - European Union
* `JP` - Japan
* `AU` - Australia
* `CN` - China

## Complete Reporting Workflow

Here's a complete workflow for downloading reports:

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Automated Daily Reports (CI/CD)

### GitHub Actions Example

```yaml  theme={null}
name: Daily Reports

on:
  schedule:
    - cron: '0 9 * * *'  # 9 AM UTC daily
  workflow_dispatch:

jobs:
  download_reports:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install asc CLI
        run: |
          curl -fsSL https://raw.githubusercontent.com/rorkai/App-Store-Connect-CLI/main/install.sh | bash
      
      - name: Configure credentials
        env:
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
          ASC_VENDOR_NUMBER: ${{ secrets.ASC_VENDOR_NUMBER }}
        run: |
          echo "Credentials configured"
      
      - name: Download daily sales report
        run: |
          YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
          asc analytics sales \
            --vendor "$ASC_VENDOR_NUMBER" \
            --type SALES \
            --subtype SUMMARY \
            --frequency DAILY \
            --date "$YESTERDAY" \
            --output "./sales-$YESTERDAY.txt"
      
      - name: Upload reports as artifact
        uses: actions/upload-artifact@v3
        with:
          name: sales-reports
          path: ./sales-*.txt
          retention-days: 30
      
      - name: Send to analytics platform (optional)
        run: |
          # Parse and send to your analytics/BI platform
          # Example: curl -X POST https://your-analytics.com/api/ingest -d @sales-*.txt
```

## Report Data Processing

### Parse Sales Report

Sales reports are typically tab-delimited:

```bash  theme={null}
# Extract download counts
grep "^[0-9]" sales-report.txt | cut -f7 | awk '{sum+=$1} END {print sum}'

# Extract revenue by app
grep "^[0-9]" sales-report.txt | awk -F'\t' '{revenue[$3]+=$14} END {for(app in revenue) print app, revenue[app]}'

# Extract top countries by downloads
grep "^[0-9]" sales-report.txt | awk -F'\t' '{country[$13]+=$7} END {for(c in country) print country[c], c}' | sort -rn | head -10
```

### Convert to JSON

```python  theme={null}
#!/usr/bin/env python3
import csv
import json
import sys

with open(sys.argv[1], 'r') as tsv_file:
    reader = csv.DictReader(tsv_file, delimiter='\t')
    rows = list(reader)
    
    with open('sales-report.json', 'w') as json_file:
        json.dump(rows, json_file, indent=2)

print(f"Converted {len(rows)} rows to JSON")
```

Run with:

```bash  theme={null}
python convert_sales.py sales-report.txt
```

## Troubleshooting

### "Vendor number not found"

**Problem**: Invalid or missing vendor number.

**Solution**: Find your vendor number in App Store Connect:

1. Go to **Sales and Trends**
2. Click **Reports**
3. Your vendor number is displayed at the top

Set it as an environment variable:

```bash  theme={null}
export ASC_VENDOR_NUMBER="YOUR_VENDOR_NUMBER"
```

### "Report not available for date"

**Problem**: Report for the requested date doesn't exist yet.

**Solution**:

* Daily reports are available \~24 hours after the date
* Weekly reports are available on Sundays for the previous week
* Monthly reports are available \~14 days after month end

### "Analytics request pending"

**Problem**: Analytics request is still processing.

**Solution**: Check request status:

```bash  theme={null}
asc analytics view --request-id "REQUEST_ID"
```

Retry `asc analytics view --request-id "REQUEST_ID" --paginate --output json` until `data[].instances` is non-empty, then download.

### "Failed to download report"

**Problem**: Network issues or API errors during download.

**Solution**: Retry with increased timeout:

```bash  theme={null}
export ASC_TIMEOUT=180s
asc analytics sales --vendor "..." --date "..." --output report.txt
```

## Best Practices

1. **Automate daily downloads**: Set up scheduled jobs to download reports automatically

2. **Store reports securely**: Reports contain sensitive financial data—encrypt and restrict access

3. **Parse and analyze**: Don't just download—parse reports and feed into analytics platforms

4. **Set up monitoring**: Alert on anomalies like sudden drop in downloads or revenue

5. **Use environment variables**: Store vendor number and other config in environment variables

6. **Handle missing reports gracefully**: Not all reports are available for all dates

7. **Archive historical data**: Keep long-term archives for trend analysis

8. **Validate data**: Cross-check totals with App Store Connect web interface

## Related Commands

* [TestFlight Analytics](/guides/testflight) - Beta tester metrics
* [Build Analytics](/commands/builds) - Build processing and distribution metrics

---

### Guides App Store Submission (`guides/app-store-submission.mdx`)

# App Store Submission

> Publish with `publish appstore --submit`, then use `validate` or `submit` for follow-up checks

Automate the complete App Store submission process with the canonical
high-level publish command, then use the lower-level submission lifecycle
commands to inspect or cancel review state when needed.

## Overview

Use these surfaces deliberately:

* `asc publish appstore --submit` - canonical App Store publish path
* `asc release stage` - prepare a version without submitting it
* `asc validate` - canonical readiness check before submission
* `asc submit status` - inspect submission or version review state
* `asc submit cancel` - cancel an active submission

In 1.0, the deprecated `asc submit preflight` and deprecated `asc submit create`
paths were removed.

## Prerequisites

Before submitting, ensure you have:

1. A processed build uploaded to App Store Connect
2. App Store version created with required metadata
3. All localizations configured with required fields
4. Screenshots uploaded for at least one device type
5. Privacy information and app review details completed

## Canonical Publish Workflow

<Steps>
  <Step title="Prepare your app version">
    Create an App Store version if it doesn't exist:

    ```bash  theme={null}
    asc versions create \
      --app "YOUR_APP_ID" \
      --version "1.0.0" \
      --platform IOS
    ```

    Or list existing versions:

    ```bash  theme={null}
    asc versions list --app "YOUR_APP_ID" --platform IOS
    ```
  </Step>

  <Step title="Verify localization metadata">
    The CLI automatically validates that required localization fields are present:

    * Description
    * Keywords
    * What's New text
    * Marketing URL (if applicable)

    Update missing localizations:

    ```bash  theme={null}
    asc app-setup info set \
      --app "YOUR_APP_ID" \
      --locale "en-US" \
      --name "My App" \
      --subtitle "Amazing app subtitle" \
      --privacy-policy-url "https://example.com/privacy"
    ```

    Update version-specific metadata:

    ```bash  theme={null}
    asc localizations update \
      --version "VERSION_ID" \
      --locale "en-US" \
      --description "Full app description" \
      --keywords "productivity,tools,utility" \
      --whats-new "Bug fixes and improvements"
    ```
  </Step>

  <Step title="Find your build ID">
    List builds for your app version:

    ```bash  theme={null}
    asc builds list \
      --app "YOUR_APP_ID" \
      --version "1.0.0" \
      --build-number "42" \
      --output json
    ```

    Or get the latest build:

    ```bash  theme={null}
    asc builds list \
      --app "YOUR_APP_ID" \
      --sort "-uploadedDate" \
      --limit 1
    ```
  </Step>

  <Step title="Run the publish command">
    Publish with the canonical App Store flow:

    ```bash  theme={null}
    asc publish appstore \
      --app "YOUR_APP_ID" \
      --ipa "./build/MyApp.ipa" \
      --version "1.0.0" \
      --submit \
      --confirm
    ```

    Use `asc release stage` instead when you want the same preparation flow
    without creating the review submission yet:

    ```bash  theme={null}
    asc release stage \
      --app "YOUR_APP_ID" \
      --version "1.0.0" \
      --build-id "BUILD_ID" \
      --copy-metadata-from "0.9.0" \
      --confirm
    ```
  </Step>
</Steps>

## Lower-level submission lifecycle

Use the `submit` command family when you need to inspect or manage the review
submission itself instead of running the full publish pipeline.

### Validate readiness

```bash  theme={null}
asc validate --app "YOUR_APP_ID" --version "1.0.0"
```

## Monitoring Submission Status

<Steps>
  <Step title="Check submission status">
    Using submission ID:

    ```bash  theme={null}
    asc submit status --id "SUBMISSION_ID"
    ```

    Using version ID:

    ```bash  theme={null}
    asc submit status --version-id "VERSION_ID"
    ```
  </Step>

  <Step title="Check version state">
    View detailed version information:

    ```bash  theme={null}
    asc versions view --version-id "VERSION_ID" --output json
    ```

    `asc versions view` is the canonical command here. If older scripts still
    use the deprecated `asc versions get`, update them during your 1.0 migration.

    The version state indicates the current stage:

    * `PREPARE_FOR_SUBMISSION`: Metadata incomplete
    * `WAITING_FOR_REVIEW`: Submitted, awaiting review
    * `IN_REVIEW`: Currently being reviewed
    * `PENDING_DEVELOPER_RELEASE`: Approved, awaiting your release
    * `READY_FOR_SALE`: Live on the App Store
    * `REJECTED`: Rejected by review team
  </Step>
</Steps>

## Managing Submissions

### Cancel a Submission

If you need to cancel a submission before review:

```bash  theme={null}
asc submit cancel --id "SUBMISSION_ID" --confirm
```

Or cancel using version ID:

```bash  theme={null}
asc submit cancel --version-id "VERSION_ID" --confirm
```

<Note>
  You can only cancel submissions that are in `WAITING_FOR_REVIEW` state. Once review begins, you cannot cancel.
</Note>

### Resubmit After Changes

If you need to make changes after submitting:

<Steps>
  <Step title="Cancel the current submission">
    ```bash  theme={null}
    asc submit cancel --version-id "VERSION_ID" --confirm
    ```
  </Step>

  <Step title="Make your changes">
    Update metadata, upload new screenshots, or attach a different build as needed.
  </Step>

  <Step title="Resubmit">
    ```bash  theme={null}
    asc publish appstore \
      --app "YOUR_APP_ID" \
      --ipa "./build/MyApp.ipa" \
      --version "1.0.0" \
      --submit \
      --confirm
    ```
  </Step>
</Steps>

## Complete Example: iOS App Submission

Here's a complete workflow for publishing an iOS app:

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Preflight Validation

The CLI performs automatic preflight checks before submission:

### Localization Validation

Checks that all required fields are present for each localization:

* Description
* Keywords
* What's New text (for updates)

If any fields are missing, the submission will fail with a detailed error:

```
Submit preflight failed: submission-blocking localization fields are missing:
  - en-US: description, keywords
  - es-ES: whats-new
Fix these with `asc apps info edit` (optionally using `--copy-from-locale`) before retrying `asc validate` or `asc publish appstore --submit`.
```

### Fixing Validation Errors

Copy complete localization from one locale to another:

```bash  theme={null}
asc apps info edit \
  --app "YOUR_APP_ID" \
  --locale "es-ES" \
  --copy-from-locale "en-US"
```

Then update locale-specific fields:

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "es-ES" \
  --description "Descripción de la aplicación" \
  --keywords "productividad,herramientas"
```

## Troubleshooting

### "No app store version localizations found"

**Problem**: The version has no localizations configured.

**Solution**: Add at least one localization:

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "en-US" \
  --description "App description" \
  --keywords "keyword1,keyword2" \
  --whats-new "What's new in this version"
```

### "Failed to attach build"

**Problem**: The build may not be compatible with the version or is still processing.

**Solution**: Verify build status:

```bash  theme={null}
asc builds info --build-id "BUILD_ID"
```

Ensure the build:

* Has finished processing
* Matches the app ID
* Supports the correct platform
* Has passed automatic checks

### "Stale review submission found"

**Problem**: A previous submission attempt left an orphaned submission.

**Solution**: The CLI automatically cancels stale submissions. If you see this warning, it's informational and the new submission will proceed.

### Submission Rejected

If your submission is rejected:

1. Check rejection details in App Store Connect web interface
2. Address the issues mentioned in the rejection
3. Upload a new build if code changes are needed, or just update metadata
4. Resubmit with the canonical publish command:

```bash  theme={null}
asc publish appstore \
  --app "YOUR_APP_ID" \
  --ipa "./build/MyApp.ipa" \
  --version "1.0.0" \
  --submit \
  --confirm
```

## Platform-Specific Considerations

### macOS Apps

For macOS apps, specify the `MAC_OS` platform:

```bash  theme={null}
asc publish appstore \
  --app "YOUR_APP_ID" \
  --ipa "./build/MyMacApp.ipa" \
  --version "1.0.0" \
  --platform MAC_OS \
  --submit \
  --confirm
```

### tvOS Apps

For tvOS apps, use the `TV_OS` platform:

```bash  theme={null}
asc publish appstore \
  --app "YOUR_APP_ID" \
  --ipa "./build/MyTVApp.ipa" \
  --version "1.0.0" \
  --platform TV_OS \
  --submit \
  --confirm
```

### visionOS Apps

For visionOS apps, use the `VISION_OS` platform:

```bash  theme={null}
asc publish appstore \
  --app "YOUR_APP_ID" \
  --ipa "./build/MyVisionApp.ipa" \
  --version "1.0.0" \
  --platform VISION_OS \
  --submit \
  --confirm
```

## Best Practices

1. **Always use `--confirm` flag**: Prevents accidental submissions

2. **Test with TestFlight first**: Distribute to internal testers before submitting to the App Store

3. **Validate metadata beforehand**: Use `asc localizations list` to verify all required fields are present

4. **Monitor submission status**: Check status regularly during the review process

5. **Keep builds ready**: Have builds processed and ready before starting the submission workflow

6. **Use version IDs for lifecycle automation**: Version IDs are reliable for `submit status` and `submit cancel`

7. **Handle stale submissions**: The CLI automatically cleans up orphaned submissions, but verify in case of errors

## Related Commands

* [TestFlight Distribution](/guides/testflight) - Test builds before App Store submission
* [Metadata Management](/guides/metadata-management) - Manage app metadata and localizations
* [Screenshots](/guides/screenshots) - Upload App Store screenshots
* [Build Management](/commands/builds) - Manage and monitor builds

---

### Guides Apple Ads Playbooks (`guides/apple-ads-playbooks.mdx`)

# Apple Ads Operator Playbooks

> Practical Apple Ads workflows for safe setup, read-only checks, reporting, and guarded mutations

Use these playbooks when you are operating Apple Ads from `asc` in a terminal,
CI job, or agent run. The goal is to start read-only, confirm the ad-account context,
and only mutate resources after the target IDs and payload files are reviewed.

The primary examples use the direct Platform API v1 resource commands under
`asc ads` with `--ad-account`. The deprecated Campaign Management API v5 tree
is isolated in the migration and legacy sections below.

## Setup and Credential Safety

Apple Ads credentials are separate from App Store Connect API credentials.
`asc auth login` does not configure `asc ads`.

Create a named Apple Ads profile and store the default ad account when you know it:

```bash  theme={null}
asc ads auth login \
  --name "Marketing" \
  --client-id "SEARCHADS_CLIENT_ID" \
  --team-id "SEARCHADS_TEAM_ID" \
  --key-id "KEY_ID" \
  --private-key "$HOME/.asc/apple-ads-private-key.pem" \
  --ad-account "987654" \
  --network
```

For CI, prefer secrets and environment variables over checked-in config:

```bash  theme={null}
export ASC_ADS_CLIENT_ID="SEARCHADS_CLIENT_ID"
export ASC_ADS_TEAM_ID="SEARCHADS_TEAM_ID"
export ASC_ADS_KEY_ID="KEY_ID"
export ASC_ADS_PRIVATE_KEY_PATH="$HOME/.asc/apple-ads-private-key.pem"
export ASC_ADS_AD_ACCOUNT_ID="987654"
export ASC_ADS_BYPASS_KEYCHAIN=1

asc ads apps search --ad-account "$ASC_ADS_AD_ACCOUNT_ID" --query "Example" --output json
```

Use a read-only API command for CI validation so the run resolves the exported
`ASC_ADS_*` credentials and org context instead of only listing stored profiles.

Use `ASC_ADS_PRIVATE_KEY_B64` when your CI secret store handles single-line
values more reliably than PEM blocks. Keep `ASC_ADS_ACCESS_TOKEN` for cases
where another trusted system already minted a short-lived token.

## Platform API v1 migration

For new automation, start with the Apple Ads Platform API v1 command tree:

```bash  theme={null}
asc ads me view --output json
asc ads acls list --output json
asc ads apps search --ad-account "987654" --query "Example" --output json
```

The legacy Campaign Management API v5 commands under `asc ads v5` still run in
4.4.0 and print a deprecation warning on stderr. Apple retires v5 on January
26, 2027. A v5 command keeps its existing `--org` context; a v1 command uses
the separate `--ad-account` context. Do not pass an organization ID where a v1
ad account ID is required, or assume that the two IDs identify the same thing.

V1 query, report, and mutation bodies use the Platform API schemas. Put the
appropriate v1 object in `--file`; the CLI does not convert a v5 selector,
reporting request, numeric ID, or response envelope. V1 report pagination stays
in the request JSON, so do not add the legacy `--paginate` flag to a v1 report
command:

```bash  theme={null}
asc ads reports apps campaigns \
  --ad-account "987654" \
  --file report.json \
  --output json
```

Platform v1 exposes one `negative-keywords` resource for both campaign and ad
group scope. Move the scope into the v1 query or mutation body rather than
looking for separate campaign-negative-keywords and ad-group-negative-keywords
commands. The v5 `asc ads v5 reports preset` helper remains a warning-producing
compatibility path; replace it with an explicit v1 report leaf and reviewed
request file.

Seven v5 leaves have no one-command v1 replacement in 4.4.0: product-page
countries, product-page devices, targeting-keyword bulk delete, campaign
negative-keyword bulk delete, ad-group negative-keyword bulk delete, and
impression-share report list and view. Related v1 commands are not drop-in
replacements for those contracts. In particular, `geo search` is not the v5
country/device mapping, `insights impression-share` is not the custom report
list/view API, and v1 has no bulk-delete operation.

For raw calls, keep `asc ads v5 api request` for v5 paths and its `--org` context.
It remains runnable with a warning and is not rewritten. Use
`asc ads api request --path v1/... --ad-account ...` for a raw v1 call.

## Account Access Inspection

Start outside an ad-account context. These commands do not require
`--ad-account`:

```bash  theme={null}
asc ads acls list --output json
```

Use the ACL response to confirm the ad account ID, then pin it explicitly for
the rest of the session:

```bash  theme={null}
export ASC_ADS_AD_ACCOUNT_ID="987654"
asc ads apps search --ad-account "$ASC_ADS_AD_ACCOUNT_ID" --query "Example" --limit 1 --output json
```

When operating multiple Apple Ads accounts, avoid relying on the ambient
default. Pass both profile and ad account:

```bash  theme={null}
asc ads apps search \
  --ads-profile "Marketing" \
  --ad-account "987654" \
  --query "Example" \
  --output json
```

## Safe Read-Only Smoke Test

Use this sequence before an automation run or after rotating credentials:

```bash  theme={null}
ASC_BYPASS_KEYCHAIN=1 asc ads acls list --output json
ASC_BYPASS_KEYCHAIN=1 asc ads apps search --ad-account "987654" --query "Example" --limit 1 --output json
```

Expected result: every command exits successfully, and the app search returns
either a `result` envelope or an empty result. Treat auth failures, missing account
errors, or unexpected account names as stop conditions.

When using stored Ads profiles, add profile validation before the API smoke
test:

```bash  theme={null}
asc ads auth status --validate --output json
```

## Campaign and Ad Group Inventory

Query campaigns first, then inspect ad groups under one confirmed campaign ID:

```bash  theme={null}
asc ads campaigns find \
  --ad-account "987654" \
  --file campaign-query.json \
  --output json

asc ads ad-groups find \
  --ad-account "987654" \
  --file ad-group-query.json \
  --output json
```

For a full v1 query, keep pagination in the request body:

```bash  theme={null}
asc ads campaigns find --ad-account "987654" --file campaign-query.json --output json
asc ads ad-groups find --ad-account "987654" --file ad-group-query.json --output json
```

Do not use report payloads as inventory replacements. Reports answer
performance questions; list and find endpoints answer "what exists right now?"
questions.

## Report Workflow

Use Platform API v1 for new report automation. Its body shape differs from v5:

```json platform-report.json
{
  "pagination": {"offset": 0, "pageSize": 20},
  "filters": [
    {"field": "campaignId", "operator": "EQUALS", "value": ["444555666"]}
  ],
  "groupBy": ["countryOrRegion"],
  "timeRange": {
    "start": "2025-01-01",
    "end": "2025-01-31",
    "timeZone": "ORTZ",
    "granularity": "DAILY"
  }
}
```

```bash  theme={null}
asc ads reports apps campaigns \
  --ad-account "987654" \
  --file platform-report.json \
  --output json
```

The v1 report response keeps Apple's raw `result` and `pagination` fields.
Change `offset` and `pageSize` in the file to fetch another page. Run
`asc ads reports apps campaigns --help` for body and option guidance; report
entities do not all accept the same `groupBy` values.

### Legacy v5 report compatibility

The request below is the deprecated v5 shape. Keep it only while existing
automation migrates:

```json reporting-request.json
{
  "startTime": "2026-05-01",
  "endTime": "2026-05-31",
  "returnRowTotals": true,
  "returnGrandTotals": true,
  "selector": {
    "pagination": {
      "offset": 0,
      "limit": 100
    },
    "orderBy": [
      {
        "field": "impressions",
        "sortOrder": "DESCENDING"
      }
    ]
  }
}
```

Run the campaign report with an explicit org and output format:

```bash  theme={null}
asc ads v5 reports campaigns \
  --org "123456" \
  --file reporting-request.json \
  --output json
```

For ad-group, keyword, search-term, or ad-level reports, first verify the parent
campaign ID with `asc ads v5 campaigns list`. Report pagination lives inside the
Apple Ads request body, so update the file's selector pagination rather than
adding `--paginate`.

## Raw API Safety

Use `asc ads api request` for Platform API v1 debugging, newly added Apple
fields, or support captures. Prefer first-class commands for routine work.

Read-only raw request:

```bash  theme={null}
asc ads api request \
  --method POST \
  --path v1/campaigns/query \
  --ad-account "987654" \
  --file campaign-query.json \
  --output json
```

Raw v1 requests only accept Platform API v1 paths or
`https://api.ads.apple.com/v1/...` URLs. `DELETE` requests and risky mutation
payloads require `--confirm`; multipart asset upload must use
`asc ads assets upload`.

For legacy troubleshooting only, keep `asc ads v5 api request` with its v5 path
and `--org` context:

```bash  theme={null}
asc ads v5 api request \
  --method POST \
  --path v5/campaigns/find \
  --org "123456" \
  --file selector.json \
  --output json
```

Raw requests only accept Apple Ads v5 paths or Apple Ads API URLs. `DELETE`
requests require `--confirm`; do not pass it until the command line includes
the exact target path you intend to delete.

## Mutation Safety Checklist

Before running create, update, delete, or bulk commands:

1. Run the matching `--help` command and confirm required flags.
2. Resolve account access with `asc ads acls list --output json`.
3. Run a read-only resource check:
   `asc ads apps search --ad-account "987654" --query "Example" --limit 1 --output json`.
4. Pass `--ads-profile` and `--ad-account` explicitly when more than one account is possible.
5. Store request bodies in reviewed JSON files; avoid shell-escaped inline JSON.
6. Create paused or clearly named test resources when validating workflow shape.
7. Print or log the target IDs before using `--confirm`.
8. Prefer deleting temporary parent campaigns only after confirming Apple allows the cleanup.

Example guarded delete:

```bash  theme={null}
asc ads campaigns view --ad-account "987654" --campaign "campaign-id" --output json
asc ads campaigns delete --ad-account "987654" --campaign "campaign-id" --confirm
```

## Troubleshooting Auth and Source Precedence

Use the auth doctor first:

```bash  theme={null}
asc ads auth doctor --output json
asc ads auth status --verbose --output json
```

Common fixes:

* Missing ad account: pass `--ad-account`, export `ASC_ADS_AD_ACCOUNT_ID`, or store `--ad-account` during login.
* Wrong profile: pass `--ads-profile "Marketing"` or export `ASC_ADS_PROFILE`.
* Keychain prompts in CI: export `ASC_ADS_BYPASS_KEYCHAIN=1`.
* Mixed sources: set `ASC_ADS_STRICT_AUTH=1` to fail when both profile and env token/key sources are present.
* Token-only runs: `ASC_ADS_ACCESS_TOKEN` still needs `--ad-account`, `ASC_ADS_AD_ACCOUNT_ID`, or a stored profile account for scoped v1 commands.

When debugging API responses, use an explicit output format and redact tokens,
client IDs, team IDs, key IDs, org IDs, and account names before sharing logs.

---

### Guides Asset Library (`guides/asset-library.mdx`)

# Asset Library

> Upload, inspect, and reuse App Store images and videos

Asset Library stores media independently of an app version. Uploading creates a library asset; a placement assigns that asset to a localization. App Review and storefront publication have their own requirements.

<Note>
  Apple's OpenAPI 4.5.1 documents the core Asset Library and placement resources. Apple controls media compatibility, asset eligibility, and permissions. Successful upload or placement requests do not establish App Review approval or storefront publication.
</Note>

## Find a library and inspect its assets

```bash theme={null}
asc asset-library view --app APP_ID
asc asset-library images list --library-id LIBRARY_ID --paginate
asc asset-library videos list --library-id LIBRARY_ID --paginate
asc asset-library specs
```

Use `images view --id IMAGE_ID` or `videos view --id VIDEO_ID` to inspect one asset. Use `images placements --id IMAGE_ID` or `videos placements --id VIDEO_ID` to inspect its usage. JSON reads preserve Apple's resource envelope, including unknown fields. Collection commands support `--limit`, `--next`, and `--paginate`; `--output table` or `--output markdown` provides human-readable output.

### Filter and sort collections

```bash theme={null}
asc asset-library images list --library-id LIBRARY_ID \
  --category CREATIVE_ASSETS --state APPROVED --sort referenceName
asc asset-library videos list --library-id LIBRARY_ID \
  --state FAILED --sort=-lastModifiedDate --paginate
asc asset-library images list --library-id LIBRARY_ID --spec-id SPEC_ID
asc asset-library images list --library-id LIBRARY_ID \
  --reference-name "Summer header" --sort=-createdDate
asc asset-library videos list --library-id LIBRARY_ID --id VIDEO_ID --sort createdDate
```

Filter by `--category`, `--state`, `--spec-id`, `--reference-name`, or `--id`. Filters and sort fields accept comma-separated values. Supported sort fields are `referenceName`, `createdDate`, and `lastModifiedDate`; prefix a field with `-` for descending order. Reference names are internal App Store Connect names. Apple validates state values and filter support. The CLI does not expose an `--archived` filter.

These filters belong to image/video `list` commands. Do not combine them or a library selector with `--next`: use Apple's next URL unchanged. `--paginate` follows those URLs automatically.

## Upload images and videos

The default upload category is `CREATIVE_ASSETS`, for headers, search creative media, and events. Use `APP_SCREENSHOTS_AND_PREVIEWS` for media showing your app in use.

```bash theme={null}
asc asset-library images upload --library-id LIBRARY_ID --file ./header.png
asc asset-library videos upload --library-id LIBRARY_ID --file ./header.mp4
asc asset-library images upload --library-id LIBRARY_ID \
  --file ./duo-screenshot.png --category APP_SCREENSHOTS_AND_PREVIEWS
asc asset-library videos upload --library-id LIBRARY_ID \
  --file ./duo-preview.mp4 --category APP_SCREENSHOTS_AND_PREVIEWS
asc asset-library images upload --library-id LIBRARY_ID \
  --file ./cms-8f3a21.png --reference-name "Hero en-US 3.14"
```

Apple derives each asset's file name and default reference name from the uploaded file, and the file name cannot change after upload. Pass `--reference-name` to set the searchable reference name at upload instead of running `asc asset-library images rename` (or `videos rename`) afterwards.

Image files must meet the selected category's media specifications. Screenshots and creative images cannot contain an alpha channel or transparency, even when all pixels appear opaque.

Video upload requires `ffprobe` from FFmpeg and a regular, nonempty `.mp4`, `.m4v`, or `.mov` file within the upload size limit. Before reservation, the CLI checks the staged file against Apple's current reference specifications: category, dimensions, aspect ratio, duration, frame rate, required audio, extension, container MIME type, and file size. Apple's processing remains authoritative for codec acceptance.

[Creative asset specifications](https://developer.apple.com/help/app-store-connect/reference/app-information/creative-assets-specifications) distinguish header/search videos, typically 5–30 seconds at 30 or 60 fps, from app previews. [App preview specifications](https://developer.apple.com/help/app-store-connect/reference/app-information/app-preview-specifications) require 15–30 seconds and have their own delivery and encoding requirements. Check `asc asset-library specs` for the current reference data rather than treating these examples as a complete specification.

`ASC_UPLOAD_TIMEOUT` bounds upload and processing. Video uploads default to `30m`, because Apple commonly takes 10 to 15 minutes to process a video; images use the general `5m` default. Processing may continue after the command times out; increase the timeout when needed. A failure after reservation prints a partial receipt containing the asset ID. Inspect that ID with `asc asset-library videos view --id VIDEO_ID` (or `images view`) before retrying or deleting it; the CLI does not delete the reservation automatically.

Video transfer or a completed poster thumbnail does not prove the video is playable. Readiness requires the expected preparation state, a matched specification, and a usable video asset URL. Processing failures return an error with the reserved ID.

### iPhone Duo sizes

Duo screenshot sizes are **1398×2034** for the outer display and **2007×2853** for the inner display, with landscape counterparts. Apple's documented Duo app preview delivery sizes are **886×1920** or **1920×886**, which differ from the physical display sizes. Use the accepted preview delivery size when preparing a video.

See [screenshot specifications](https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications) and [app preview specifications](https://developer.apple.com/help/app-store-connect/reference/app-information/app-preview-specifications). Native asset placements use the `IPHONE_DUO_PROFILE` group.

## Manage asset names, lifecycle, and poster frames

```bash theme={null}
asc asset-library images rename --id IMAGE_ID --name "Summer header"
asc asset-library images archive --id IMAGE_ID --confirm
asc asset-library images unarchive --id IMAGE_ID
asc asset-library images delete --id IMAGE_ID --confirm
asc asset-library videos set-poster-frame --id VIDEO_ID --time-code 00:00:01:00
```

Videos also support `rename`, `archive`, `unarchive`, and `delete` with `--id`. The reference name is internal to App Store Connect.

Only approved assets can be archived. Archived assets cannot be used in new placements; restoring an asset makes it available for new uses, subject to Apple's checks. Apple validates whether an asset can be deleted based on its current state and usage. To remove an assignment while retaining the media, delete its placement instead.

Poster changes affect every placement using that library video. Poster frames cannot be edited after the asset is approved. `--time-code` accepts `HH:MM:SS:FF` or `HH:MM:SS.mmm`; the CLI does not interpret frame values as a particular playback frame rate.

Apple validates lifecycle eligibility. These commands do not submit assets for review.

## Reuse media in localized placements

Placement reads, creation, swaps, and deletion are available under four command groups. Reordering is available only for version, CPP, and PPO localizations:

| Context | Command group |
| --- | --- |
| App version localization | `asc localizations placements` |
| Custom product page localization | `asc product-pages custom-pages localizations placements` |
| Product page optimization treatment localization | `asc product-pages experiments treatments localizations placements` |
| In-App Event localization | `asc app-events localizations placements` |

Use the localization ID for the selected context, not its parent page, treatment, event, or version ID.

```bash theme={null}
asc localizations placements list --localization-id LOCALIZATION_ID \
  --include image,video --sort placementGroupPosition --paginate
asc localizations placements create --localization-id LOCALIZATION_ID \
  --image-id IMAGE_ID --placement-type PRODUCT_PAGE_HEADER_ASSET
asc localizations placements create --localization-id LOCALIZATION_ID \
  --video-id VIDEO_ID --placement-type APP_PREVIEW --placement-group IPHONE_DUO_PROFILE
asc app-events localizations placements create --localization-id EVENT_LOCALIZATION_ID \
  --image-id IMAGE_ID --placement-type EVENT_CARD_ASSET
```

Supply exactly one `--image-id` or `--video-id`. Screenshots require images; app previews require videos and an explicit device `--placement-group`. Header, search, and event creative media use `DEFAULT_PROFILE`. Header/search types are `PRODUCT_PAGE_HEADER_ASSET` and `APP_STORE_SEARCH_RESULTS_ASSET`. Events accept only `EVENT_CARD_ASSET` and `EVENT_DETAILS_PAGE_ASSET`.

Version localizations also support `IMESSAGE_APP_SCREENSHOT`. CPP and PPO creation do not support iMessage screenshot assignments. Apple validates media compatibility and available slots. Placement lists accept comma-separated type/group filters, related `image,video` inclusion, sorting, and pagination.

### Replace an existing placement

```bash theme={null}
asc product-pages custom-pages localizations placements swap \
  --localization-id CPP_LOCALIZATION_ID --placement-id OLD_PLACEMENT_ID \
  --image-id IMAGE_ID --placement-type PRODUCT_PAGE_HEADER_ASSET --confirm
```

`swap` uses a live-supported extension outside the OpenAPI 4.5.1 create schema to link the existing placement to its replacement in one request. It accepts an image or video of the same media type as the existing placement and requires `--confirm`. Apple requires the replacement to keep the existing placement's group, so the command first reads that group from the existing placement and sends it with the swap; there is no group flag. Apple validates the existing context, media, and editability. A failed swap does not trigger a separate deletion or fallback removal.

For an already-live version, Apple's website guides users through **Next → Publish** when selecting an approved replacement. The CLI's swap receipt does not promise storefront publication; no separate public publication command is available. New-version assets still follow the version's review and release workflow.

### Reorder and remove placements

```bash theme={null}
asc localizations placements reorder --localization-id LOCALIZATION_ID \
  --placement-group IPHONE_DUO_PROFILE --placement-ids FIRST_ID,SECOND_ID
asc localizations placements delete --id PLACEMENT_ID --confirm
asc localizations placements delete --placement-ids FIRST_ID,SECOND_ID --confirm
```

Reordering sends unique placement IDs in the requested order for version, CPP, or PPO localizations. Apple validates membership in the localization and group. Omitted placements are not deleted by the CLI. Event placement reordering is not supported.

Multiple removal uses sequential public DELETE requests. It is not atomic: processing stops at the first failure, prints a partial receipt with `deletedPlacementIds` and `failedPlacementId`, and returns a nonzero error. Earlier successful removals are not rolled back. Inspect current state before retrying an uncertain failure.

Placement deletion keeps the library media. IDs control removal; the command does not check that an ID belongs to the command group's localization or placement type.

## Review and publication

Upload, placement, reorder, and lifecycle commands do not submit App Review. The CLI supports `asc review items add` with `--item-type appAssetLibraryImages` or `appAssetLibraryVideos` and `--item-id` using Apple's documented review relationships. Live image/video review-item acceptance has not been independently verified. A [community image submission report](https://github.com/rorkai/App-Store-Connect-CLI/pull/2932#issuecomment-6013396808) reached `WAITING_FOR_REVIEW`; video acceptance and final asset approval remain unverified.

Separate asset review requires an approved app version. For the first version, include the assets with that version's submission. Assets assigned to a version, CPP, PPO test, or event are reviewed with that item; active submissions can restrict standalone submission. Apple's [standalone submission guide](https://developer.apple.com/help/app-store-connect/manage-submissions-to-app-review/submit-assets-from-asset-library) explains platform selection and eligibility.

This guide's examples stop before submitting review or publishing. A processed upload, an `ACTIVE` placement, or a successful swap is not evidence of App Review approval or live storefront content.

---

### Guides Automation (`guides/automation.mdx`)

# Workflow Automation

> Automate complex App Store Connect workflows with multi-step sequences

Define and execute multi-step automation workflows that combine multiple CLI commands and shell scripts.

## Overview

Workflows allow you to:

* Chain multiple `asc` commands together
* Run shell scripts and commands
* Pass data between steps
* Handle errors with hooks
* Execute conditional steps
* Compose reusable sub-workflows

<Warning>
  Workflows intentionally execute arbitrary shell commands. Only run workflow files you trust, especially when using `--file`. Treat `.asc/workflow.json` like code: review it before running.
</Warning>

## Quick Start

### Create a Workflow File

Create `.asc/workflow.json` in your project:

```json  theme={null}
{
  "env": {
    "APP_ID": "123456789",
    "VERSION": "1.0.0"
  },
  "workflows": {
    "beta": {
      "description": "Distribute latest build to beta testers",
      "steps": [
        {
          "name": "get_latest_build",
          "run": "asc builds list --app $APP_ID --sort -uploadedDate --limit 1 --output json"
        },
        {
          "name": "add_to_group",
          "run": "asc builds add-groups --build-id $BUILD_ID --group $GROUP_ID"
        }
      ]
    }
  }
}
```

### Run a Workflow

```bash  theme={null}
asc workflow run beta BUILD_ID:abc123 GROUP_ID:def456
```

### List Available Workflows

```bash  theme={null}
asc workflow list
```

### Validate Workflow File

```bash  theme={null}
asc workflow validate
```

## Workflow Structure

### Basic Workflow

```json  theme={null}
{
  "env": {
    "GLOBAL_VAR": "value"
  },
  "before_all": "echo Starting workflows",
  "after_all": "echo All workflows complete",
  "error": "echo Error occurred",
  "workflows": {
    "workflow_name": {
      "description": "What this workflow does",
      "env": {
        "WORKFLOW_VAR": "value"
      },
      "steps": [
        {
          "name": "step_1",
          "run": "command to execute"
        }
      ]
    }
  }
}
```

### Environment Variables

**Global Environment** (`env` at root):

* Available to all workflows
* Merged with system environment

**Workflow Environment** (`env` in workflow):

* Scoped to specific workflow
* Overrides global variables

**Runtime Parameters**:

* Passed via command line: `KEY:VALUE`
* Override workflow environment

Precedence for steps in the workflow you invoked (highest to lowest):

1. Runtime parameters
2. Workflow `env`
3. Global `env`
4. System environment

Precedence for steps inside a called sub-workflow (highest to lowest):

1. The calling step's `with`
2. Runtime parameters
3. Calling workflow's `env`
4. Global `env`
5. Called workflow's own `env`
6. System environment

A sub-workflow's own `env` is the lowest-priority map in its own scope, so it
provides defaults the caller overrides. The calling step's `with` is the only
source that outranks runtime parameters: a key hardcoded there cannot be
overridden with `KEY:VALUE`. `with` values are literal — only
`${steps.NAME.OUTPUT}` references are interpolated.

### Hooks

**before\_all**: Runs before any workflow

```json  theme={null}
"before_all": "asc auth status"
```

**after\_all**: Runs after successful workflow

```json  theme={null}
"after_all": "echo Success! && notify-send 'Workflow Complete'"
```

**error**: Runs when workflow fails

```json  theme={null}
"error": "echo Failed! && notify-send 'Workflow Failed'"
```

## Step Types

### Command Steps

Execute shell commands:

```json  theme={null}
{
  "name": "list_builds",
  "run": "asc builds list --app $APP_ID --limit 5"
}
```

### Conditional Steps

`if` names a variable and runs the step only when that variable holds a truthy
value: `1`, `true`, `yes`, `y`, or `on` (case-insensitive, surrounding
whitespace ignored):

```json  theme={null}
{
  "name": "optional_step",
  "if": "INSPECT_BUILD",
  "run": "asc builds info --build-id $BUILD_ID"
}
```

Run it with `asc workflow run beta INSPECT_BUILD:true`. The variable is resolved
from the workflow environment first (runtime parameters, `with`, workflow `env`,
global `env`) and then from the process environment.

Every other value is falsy, including an unset variable, an empty string, and
any other non-empty value. `"if": "BUILD_ID"` with `BUILD_ID:abc123`, or
`"if": "IPA_PATH"` with a path, always skips the step. Use `if` as a boolean
feature flag, never as a "was this ID supplied?" check.

<Warning>
  A skipped step is not a failure: the run still reports `"status": "ok"` and
  exits `0`, with `"status": "skipped"` on that entry in `steps[]`. A mutating
  step gated on a non-boolean value never runs while CI stays green.
</Warning>

### Sub-Workflow Steps

Call another workflow:

```json  theme={null}
{
  "workflow": "preflight",
  "with": {
    "EXTRA_VAR": "value"
  }
}
```

The called workflow receives `EXTRA_VAR` in addition to its own environment.

## Passing Data Between Steps

Each step runs in its own shell, so a variable assigned inside one step is gone
by the next one. `BUILD_ID=$(...)` in step 2 is **not** visible to step 3.

To move a value forward, declare `outputs` on a step and reference them as
`${steps.<step_name>.<OUTPUT_NAME>}`. The step must print JSON on stdout, so
pass `--output json` to `asc` commands. Each output is a JSON path rooted at
`$.` and may only walk object keys (no array indexes):

```json  theme={null}
{
  "steps": [
    {
      "name": "resolve_build",
      "run": "asc builds info --app $APP_ID --latest --platform IOS --output json",
      "outputs": {
        "BUILD_ID": "$.data.id"
      }
    },
    {
      "name": "distribute",
      "run": "asc builds add-groups --build-id ${steps.resolve_build.BUILD_ID} --group \"$GROUP_NAME\""
    }
  ]
}
```

Interpolated values are shell-quoted, and numbers keep the exact form the
command emitted (a build number of `42` stays `42`). Declared outputs are also
persisted in the run-state file, so never map a secret into `outputs`.

## Complete Workflow Examples

### Beta Distribution Workflow

```json  theme={null}
{
  "env": {
    "APP_ID": "123456789"
  },
  "workflows": {
    "beta": {
      "description": "Distribute latest build to TestFlight beta group",
      "env": {
        "GROUP_NAME": "External Beta",
        "DISTRIBUTE": "false"
      },
      "steps": [
        {
          "name": "validate_auth",
          "run": "asc auth status"
        },
        {
          "name": "resolve_build",
          "run": "asc builds info --app $APP_ID --latest --platform IOS --output json",
          "outputs": {
            "BUILD_ID": "$.data.id"
          }
        },
        {
          "name": "add_build_to_group",
          "if": "DISTRIBUTE",
          "run": "asc builds add-groups --build-id ${steps.resolve_build.BUILD_ID} --group \"$GROUP_NAME\"",
          "retry": {
            "max_attempts": 6,
            "delay": "10s"
          },
          "timeout": "2m"
        },
        {
          "name": "notify_success",
          "if": "DISTRIBUTE",
          "run": "echo Build ${steps.resolve_build.BUILD_ID} distributed to $GROUP_NAME"
        }
      ]
    }
  }
}
```

Run with:

```bash  theme={null}
asc workflow run beta DISTRIBUTE:true
```

Leave `DISTRIBUTE` off to resolve the latest build without touching the group:

```bash  theme={null}
asc workflow run beta
```

### App Store Submission Workflow

```json  theme={null}
{
  "env": {
    "APP_ID": "123456789",
    "IPA_PATH": "./build/MyApp.ipa",
    "PLATFORM": "IOS",
    "SUBMIT": "false"
  },
  "before_all": "echo Starting submission workflow",
  "after_all": "echo Submission complete!",
  "error": "echo Submission failed - check logs",
  "workflows": {
    "release": {
      "description": "Publish latest build to the App Store",
      "steps": [
        {
          "name": "validate_metadata",
          "run": "asc localizations list --version $VERSION_ID --output json | jq -e '.data | length > 0'"
        },
        {
          "name": "ensure_ipa",
          "run": "test -f \"$IPA_PATH\""
        },
        {
          "name": "publish_release",
          "if": "SUBMIT",
          "run": "asc publish appstore --app $APP_ID --ipa $IPA_PATH --version $VERSION --submit --confirm"
        },
        {
          "name": "check_status",
          "run": "asc submit status --version-id $VERSION_ID"
        }
      ]
    }
  }
}
```

Run with:

```bash  theme={null}
asc workflow run release SUBMIT:true VERSION:1.0.0 VERSION_ID:abc123
```

`SUBMIT:true` is what makes `publish_release` execute. Gating it on `IPA_PATH`
instead would skip the submission on every run, because `./build/MyApp.ipa` is
not a truthy value. Presence of the file belongs in a real check such as the
`ensure_ipa` step above. Drop `SUBMIT:true` for a metadata-only dry pass:

```bash  theme={null}
asc workflow run release VERSION:1.0.0 VERSION_ID:abc123
```

### Complete Release Workflow

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run with:

```bash  theme={null}
asc workflow run release SUBMIT:true VERSION:2.0.0
```

`create_version` declares `VERSION_ID` once, and every later sub-workflow reads
it as `${steps.create_version.VERSION_ID}`. Step names that declare `outputs`
must stay unique across workflows that can run together, which is why the
producer is named once and referenced everywhere else.

## CI/CD Integration

### GitHub Actions

```yaml  theme={null}
name: Release Workflow

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version number (e.g., 1.0.0)'
        required: true

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install asc CLI
        run: |
          curl -fsSL https://raw.githubusercontent.com/rorkai/App-Store-Connect-CLI/main/install.sh | bash
      
      - name: Configure credentials
        env:
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
          ASC_APP_ID: ${{ secrets.ASC_APP_ID }}
        run: |
          echo "Credentials configured"
      
      - name: Run release workflow
        env:
          VERSION: ${{ github.event.inputs.version }}
        run: |
          asc workflow run release "VERSION:$VERSION"
      
      - name: Upload workflow output
        uses: actions/upload-artifact@v3
        with:
          name: workflow-output
          path: /tmp/workflow-*.log
```

### GitLab CI

```yaml  theme={null}
stages:
  - release

release:production:
  stage: release
  tags:
    - linux
  script:
    - curl -fsSL https://raw.githubusercontent.com/rorkai/App-Store-Connect-CLI/main/install.sh | bash
    - asc workflow run release VERSION:$VERSION
  variables:
    VERSION: "1.0.0"
  only:
    - tags
  when: manual
```

## Advanced Features

### Dry Run Mode

Preview workflow execution without running commands:

```bash  theme={null}
asc workflow run beta --dry-run
```

Shows:

* Steps that would be executed
* Environment variables
* Command interpolation
* Workflow order

### Private Workflows

Mark workflows as private (only callable from other workflows):

```json  theme={null}
{
  "workflows": {
    "helper": {
      "private": true,
      "description": "Helper workflow - not directly callable",
      "steps": [
        {"name": "help", "run": "echo Helping..."}
      ]
    },
    "main": {
      "description": "Main workflow that calls helper",
      "steps": [
        {"workflow": "helper"}
      ]
    }
  }
}
```

```bash  theme={null}
# Works
asc workflow run main

# Fails - private workflow
asc workflow run helper
```

List including private workflows:

```bash  theme={null}
asc workflow list --all
```

### Custom Workflow File

Use a different workflow file:

```bash  theme={null}
asc workflow run beta --file ./custom-workflows.json
```

### Pretty JSON Output

```bash  theme={null}
asc workflow run beta --pretty
```

## Best Practices

1. **Use descriptive names**: Name workflows and steps clearly

2. **Add descriptions**: Document what each workflow does

3. **Validate before running**: Always run `asc workflow validate` after editing

4. **Use environment variables**: Parameterize workflows for reusability

5. **Handle errors**: Define `error` hooks to clean up on failure

6. **Test with dry-run**: Use `--dry-run` to preview execution

7. **Keep workflows focused**: Break complex workflows into composable sub-workflows

8. **Version control workflows**: Store `.asc/workflow.json` in git

9. **Document required parameters**: Note required `KEY:VALUE` parameters in descriptions

10. **Use private workflows**: Extract common logic into private sub-workflows

## Security Considerations

<Warning>
  * Only run workflows from trusted sources
  * Review workflow files before execution
  * Avoid hardcoding secrets—use environment variables
  * In CI/CD, use encrypted secrets for credentials
  * Don't run untrusted workflows on PRs with access to secrets
  * Set `ASC_READ_ONLY=1` for audit, reporting, and agent sessions that must never write
</Warning>

### Read-only runs

Export `ASC_READ_ONLY=1` (or pass the global `--read-only` flag) when a workflow, CI job, or agent session is only allowed to inspect App Store Connect. Every command still runs, but any `POST`, `PATCH`, `PUT`, or `DELETE` is refused before it leaves the process with exit code `6`. This is enforced in the HTTP clients, not per command, so it holds for every command in the workflow, and workflow steps inherit the mode as `ASC_READ_ONLY=1` so a nested `asc` call is refused too. Steps that call other tools are outside the guarantee. See [Read-only mode](/concepts/read-only-mode).

```bash  theme={null}
ASC_READ_ONLY=1 asc workflow run audit
```

### Safe Practices

```json  theme={null}
// Good - uses environment variable
"run": "asc builds list --app $APP_ID"

// Bad - hardcoded sensitive data
"run": "asc builds list --app 123456789"

// Good - credentials from environment
"env": {
  "ASC_KEY_ID": "$CI_ASC_KEY_ID"
}

// Bad - credentials in workflow file
"env": {
  "ASC_KEY_ID": "ABC123XYZ"
}
```

## Troubleshooting

### Validation Errors

**Problem**: `asc workflow validate` reports errors.

**Solution**: Check for:

* Circular workflow dependencies
* Missing workflow definitions
* Invalid JSON syntax
* Unknown step types

### "Workflow not found"

**Problem**: Workflow name doesn't exist in file.

**Solution**: List available workflows:

```bash  theme={null}
asc workflow list
```

### Variable Not Interpolated

**Problem**: `$VARIABLE` appears literally in output.

**Solution**: Ensure the variable is:

* Defined in `env` (global or workflow)
* Passed as runtime parameter: `VARIABLE:value`
* Available in system environment

### Step Fails Silently

**Problem**: Step executes but produces no output.

**Solution**:

* Check step output is written to stderr (workflow stdout is JSON-only)
* Enable verbose mode (if available)
* Add explicit `echo` statements

### Workflow Hangs

**Problem**: Workflow stops and doesn't complete.

**Solution**:

* Check for interactive commands (not supported)
* Verify commands don't wait for user input
* Add timeout to long-running commands

## Related Topics

* [TestFlight Distribution](/guides/testflight) - Automate beta distribution
* [App Store Submission](/guides/app-store-submission) - Automate submissions
* [Metadata Management](/guides/metadata-management) - Automate metadata updates
* [Screenshots](/guides/screenshots) - Automate screenshot workflows

---

### Guides Code Signing (`guides/code-signing.mdx`)

# Code signing

> Prepare bundle IDs, certificates, private keys, and provisioning profiles for local or CI builds.

A downloaded certificate is not a signing identity. The certificate contains a public key; Xcode also needs the matching private key. App Store Connect can return certificates and provisioning profiles, but it cannot return a private key after the certificate request has been created.

Use the [signing command reference](/commands/signing) for every flag, profile-type table, encrypted repository format, encryption guarantees, multi-target manifest, CI example, rotation and revocation sequence, and troubleshooting case. This guide explains which workflow to choose.

## Existing identity on one Mac

If the matching private key already exists in the selected macOS keychain, fetch the public certificate and provisioning profile:

```bash  theme={null}
asc signing fetch \
  --app APP_ID \
  --bundle-id com.example.app \
  --profile-type IOS_APP_STORE \
  --output .asc/signing/app-store \
  --format json \
  --pretty
```

Install the profile by file rather than copying it to a guessed Xcode directory:

```bash  theme={null}
asc profiles local install \
  --path .asc/signing/app-store/PROFILE.mobileprovision \
  --output json
```

The `.cer` file is useful for inspection and verification. Importing it without its private key does not make the machine able to sign.

## New certificate

Generate the private key and certificate request locally, then send the request to App Store Connect:

```bash  theme={null}
mkdir -p .asc/signing .asc/secrets
chmod 700 .asc/signing .asc/secrets
certificate_response=.asc/signing/distribution-certificate.json

asc certificates create \
  --certificate-type IOS_DISTRIBUTION \
  --generate-csr \
  --key-out .asc/secrets/distribution.key \
  --csr-out .asc/signing/distribution.csr \
  --output json \
  --pretty > "$certificate_response"
```

The private key remains in `.asc/secrets/distribution.key`. Do not commit it, print it, or upload it as a CI artifact. Back it up through encrypted signing sync before removing the bootstrap copy.

The key and CSR are written before the API request. After a timeout or ambiguous failure, first list certificates and check whether the request created one whose public key matches the preserved private key. Do not rerun `--generate-csr` or use `--force`:

```bash  theme={null}
asc certificates list \
  --certificate-type IOS_DISTRIBUTION \
  --paginate \
  --output json \
  --pretty > .asc/signing/distribution-certificates-after-ambiguous-create.json
```

When a matching certificate exists, retrieve that resource into a fresh response file:

```bash  theme={null}
certificate_response=.asc/signing/distribution-certificate-recovered.json
asc certificates view \
  --id MATCHING_CERTIFICATE_ID \
  --output json \
  --pretty > "$certificate_response"
```

If no matching certificate exists, retry with the preserved CSR and a fresh response file:

```bash  theme={null}
certificate_response=.asc/signing/distribution-certificate-retry.json
asc certificates create \
  --certificate-type IOS_DISTRIBUTION \
  --csr .asc/signing/distribution.csr \
  --output json \
  --pretty > "$certificate_response"
```

## Shared App Store identity

Read the certificate resource ID and public certificate from the same create response. `signing sync push` uses the SHA-256 fingerprint to bind the generated private key, while profile reconciliation uses the resource ID to select that same certificate:

```bash  theme={null}
CERTIFICATE_ID="$(
  jq -r '.data.id' "$certificate_response"
)"

CERTIFICATE_SHA256="$(
  jq -r '.data.attributes.certificateContent' \
    "$certificate_response" \
  | openssl base64 -d -A \
  | openssl x509 -inform DER -outform DER \
  | shasum -a 256 \
  | awk '{print toupper($1)}'
)"
```

`signing sync pull` does not transport the App Store Connect certificate resource ID. Save `CERTIFICATE_ID` as a protected, non-secret CI variable named `ASC_DISTRIBUTION_CERTIFICATE_ID`, scoped to the environment that uses this signing repository. Keep that variable paired with the synced identity and update both during rotation.

Push that verified private key, certificate, and profile into a private encrypted Git repository:

```bash  theme={null}
chmod 600 \
  ~/.config/asc/signing-sync-password \
  .asc/secrets/distribution.key

asc signing sync push \
  --bundle-id com.example.app \
  --profile-type IOS_APP_STORE \
  --repo git@github.com:team/signing.git \
  --password-file ~/.config/asc/signing-sync-password \
  --private-key .asc/secrets/distribution.key \
  --identity-sha256 "$CERTIFICATE_SHA256" \
  --create-missing \
  --output json \
  --pretty
```

For an app with embedded targets, use one strict target manifest so the repository update covers all bundle identifiers in one commit:

```json
{
  "schemaVersion": 1,
  "targets": [
    {"bundleId": "com.example.app"},
    {"bundleId": "com.example.app.widget"}
  ]
}
```

```bash  theme={null}
asc signing sync push \
  --targets-file ./signing-targets.json \
  --profile-type IOS_APP_STORE \
  --repo git@github.com:team/signing.git \
  --password-file ~/.config/asc/signing-sync-password \
  --private-key .asc/secrets/distribution.key \
  --identity-sha256 "$CERTIFICATE_SHA256" \
  --create-missing \
  --output json
```

The same profile type, certificate filter, identity, and device set apply to every target. A batch can create profiles before the final repository push, so a later failure may leave those immutable App Store Connect profiles in place.

What AES-256-GCM, PBKDF2, scrypt, and authenticated metadata do and do not guarantee is in [What the encrypted store protects](/commands/signing#what-the-encrypted-store-protects).

## Pull in CI

Write the repository password from the CI secret manager to a runner-private file, then pull the signing assets. `signing sync` already forces noninteractive Git (`GIT_TERMINAL_PROMPT=0` and, unless you overrode SSH, `GIT_SSH_COMMAND=ssh -o BatchMode=yes`); keep any custom SSH wrapper equally noninteractive so a prompt cannot hang the runner. The full CI example lives in the [signing command reference](/commands/signing#ci-example).

```bash  theme={null}
set +x
umask 077
signing_tmp="$(mktemp -d "$RUNNER_TEMP/asc-signing.XXXXXX")"
password_file="$signing_tmp/repository-password"
cleanup_signing_pull() {
  rm -f "$password_file"
  rmdir "$signing_tmp" >/dev/null 2>&1 || true
}
trap cleanup_signing_pull EXIT
printf '%s' "$SIGNING_SYNC_PASSWORD" > "$password_file"
mkdir -p .asc/signing
chmod 700 .asc/signing

asc signing sync pull \
  --repo git@github.com:team/signing.git \
  --password-file "$password_file" \
  --output-dir .asc/signing/pulled \
  --output json > .asc/signing/pull-result.json
```

The result lists private identities under `sensitiveFiles`. Install the pulled
PKCS#12 into a new dedicated keychain when separate archive and export commands
need the identity to persist:

```bash  theme={null}
identity_path="$(
  jq -er '
    [.sensitiveFiles[]? | select(endswith(".p12"))]
    | if length == 1 then .[0]
      else error("expected exactly one signing identity")
      end
  ' .asc/signing/pull-result.json
)"
keychain_path="$signing_tmp/release.keychain-db"
keychain_password_file="$signing_tmp/keychain-password"
cleanup_signing() {
  asc signing keychain delete \
    --keychain "$keychain_path" \
    --confirm >/dev/null 2>&1 || true
  rm -f "$password_file" "$keychain_password_file"
  rmdir "$signing_tmp" >/dev/null 2>&1 || true
}
trap cleanup_signing EXIT
openssl rand -base64 32 > "$keychain_password_file"

asc signing keychain install \
  --identity ".asc/signing/pulled/$identity_path" \
  --identity-password-file "$password_file" \
  --keychain "$keychain_path" \
  --keychain-password-file "$keychain_password_file" \
  --add-to-search-list \
  --confirm \
  --output json

jq -r '.files[] | select(test("\\.(mobileprovision|provisionprofile)$"))' \
  .asc/signing/pull-result.json \
| while IFS= read -r profile_path; do
    asc profiles local install \
      --path ".asc/signing/pulled/$profile_path" \
      --output json
  done
```

The receipt supplies the identity's complete relative path. This example
requires exactly one pulled PKCS#12 identity and stops if that invariant is not
met. The identity password is the encrypted-repository password because sync
normalizes the pulled PKCS#12 under that secret. The keychain password is a
separate generated secret. The install command is macOS-only, requires a
cgo-enabled build, refuses an existing destination, and rolls back the new
keychain if import, codesigning verification, or search-list activation fails.
It does not change the default keychain. The run-owned temporary directory
ensures the cleanup trap cannot delete a pre-existing keychain if installation
fails.

The keychain remains available after the command succeeds. Keep archive and
export inside the lifetime of the cleanup trap on disposable CI workers; for a
long-lived machine, omit the trap only when the dedicated keychain is intended
to remain. Do not upload the decrypted directory, password files, or keychain.

## Bootstrap a fresh CI runner

A fresh macOS runner starts with an App Store Connect API key and nothing else.
Before `xcodebuild` can sign, it needs a private key with its certificate, a
provisioning profile for every target, a keychain that `codesign` can use, and
project build settings that name those profiles. `asc` covers each step:

| Step | Command |
| --- | --- |
| Authenticate | `ASC_KEY_ID`, `ASC_ISSUER_ID`, and `ASC_PRIVATE_KEY_B64` from the CI secret store |
| Create or obtain the identity and profiles | First run: `asc signing fetch --create-missing --create-missing-certificate`. Later runs: `asc signing sync pull` |
| Keep the identity for later runs | `asc signing sync push --identity ...` |
| Install the identity and profiles | `asc signing keychain install --confirm` and `asc profiles local install` |
| Point the project at the profiles | `asc xcode signing plan --profile ...`, then `asc xcode signing apply --confirm` |
| Archive and export | `asc xcode archive`, then `asc xcode export --export-options ...` |
| Clean up | `asc signing keychain delete --confirm` |

Provide the API key through [environment variables](/configuration/environment-variables)
from the CI secret store. The key's role must allow creating certificates and
profiles for the first run.

### First run: one pass from API key to signed archive

When the team has no active certificate of the required type, one
`signing fetch` creates the private key, certificate signing request,
certificate, profile, and password-protected `.p12`. The private key exists only on this
runner, so the script pushes the new identity to the encrypted store before it
builds anything. A disposable runner that skips that push loses the key when
the job ends.

```
/* Detailed source-code truncated for AI context efficiency. */
```

Read these boundaries before relying on the script:

* A certificate is created only when no active certificate of the inferred type
  exists. Otherwise the receipt reports `certificateCreationState: "reused"` and
  has no `p12Path`, so the `jq -e` lookup stops the script under `set -e`. The
  runner cannot sign with a certificate whose private key lives elsewhere; push that identity
  once from the machine that holds it, as in [Shared App Store identity](#shared-app-store-identity).
* `--create-missing-certificate` fetches one bundle ID and cannot be combined
  with `--match-extensions`. For an app with extensions, run a second
  `asc signing fetch --match-extensions --create-missing` into another output
  directory after the certificate exists. Pass every `profileFile` from its
  `results` to `profiles local install` and to `xcode signing plan` as repeated
  `--profile` flags. Push the identity for all of those bundle IDs with
  `signing sync push --bundle-id com.example.app --match-extensions`, or with a
  `--targets-file` manifest that lists each bundle ID as in
  [Shared App Store identity](#shared-app-store-identity); the manifest takes
  bundle IDs, not profile paths.
* `.asc/signing/bootstrap` holds the private key, CSR, and `.p12`. Do not upload
  it as an artifact. The `.p12` password in this script is a generated secret
  that exists only for this job; the pushed copy is re-protected with the
  store password.
* `xcode signing plan` exits 0 with `ready: false` when a target has no matching
  profile, so the script checks `ready`. `xcode signing apply` edits the
  checked-out project file and refuses a blocked or stale plan.
* `sync push` can also create the identity itself:
  `asc signing sync push --create-missing --create-missing-certificate --identity-password-file PATH`.
  Use that form in a one-time bootstrap job when you do not want the first run
  to build.

### Every later run

Once the store holds the identity, a fresh runner pulls it instead of creating
another certificate. The [CI example](/commands/signing#ci-example) runs
`signing sync pull`, `signing keychain install`, `profiles local install`,
`xcode signing plan --profile`, `xcode signing apply`, `xcode archive`, and
`xcode export` in one step, and removes the keychain afterward. A pulled `.p12`
is protected by the store password, not by the bootstrap `.p12` password.

## Registered-device release testing

For an iOS archive with extensions or other embedded targets, reconcile the archive against an exact protected device file:

```bash  theme={null}
: "${ASC_DISTRIBUTION_CERTIFICATE_ID:?configure the resource ID paired with the synced identity}"
chmod 600 .asc/distribution/devices.json

asc signing reconcile plan \
  --archive-path .asc/artifacts/Example.xcarchive \
  --devices-file .asc/distribution/devices.json \
  --certificate "$ASC_DISTRIBUTION_CERTIFICATE_ID" \
  --minimum-validity-days 14 \
  --output json \
  --pretty

asc signing reconcile apply \
  --plan .asc/distribution/signing/plan.json \
  --confirm \
  --output json > .asc/distribution/signing/apply-result.json
```

Planning is read-only and can return a successful `ready: false` result with blockers. Apply performs only the additive actions recorded in a ready plan; it does not delete profiles, create certificates, or change capabilities.

Planning creates `plan.json` without replacing an existing file. If a plan reports blockers, review it and fix them first. Then rerun the same plan command with `--overwrite`, or choose a new `--state-dir` when the blocked plan must remain on disk.

`signing run` accepts exactly one profile. Use the temporary-keychain export below only for a single-target archive whose main app uses that profile. When this runs in a later CI step than the pull, recreate the runner-private password file because the pull step's cleanup trap removed its copy:

```bash  theme={null}
set +x
umask 077
password_file="$RUNNER_TEMP/asc-signing-password"
trap 'rm -f "$password_file"' EXIT
printf '%s' "$SIGNING_SYNC_PASSWORD" > "$password_file"

signing_command=(
  asc xcode export
  --archive-path .asc/artifacts/Example.xcarchive
  --ipa-path .asc/artifacts/Example.ipa
  --method release-testing
  --signing-style manual
  --team-id TEAM_ID
)

asc signing run \
  --identity .asc/signing/pulled/identities/distribution/IDENTITY.p12 \
  --identity-password-file "$password_file" \
  --profile .asc/distribution/signing/profiles/PROFILE.mobileprovision \
  --receipt .asc/distribution/signing-run.json \
  -- "${signing_command[@]}"
```

`signing run` accepts iOS ad hoc profiles with registered devices. It rejects App Store, development, enterprise, macOS, and tvOS profiles.

For an archive with extensions, App Clips, Watch apps, or other embedded targets, don't use this single-profile wrapper for export. On a disposable macOS worker, use `asc signing keychain install` for the job-scoped identity, install each completed profile action's `outputPath` from `.asc/distribution/signing/apply-result.json`, export, and discard the worker state when the job ends.

## What the encrypted store does not do

The default store is Git. `--storage object` keeps the same ciphertext in an S3 or S3-compatible bucket, with credentials from the standard AWS configuration chain, including workload identity; `--storage gitlab-secure-files` and `--storage aws-secrets-manager` are experimental alternatives. Password rotation supports Git and object storage. The store does not renew certificates or assign different profile types or device sets per target. Profile renewal, device refresh, and one-type cleanup are explicit `sync push` options and `sync nuke`, all Git-only; see [Renew, refresh, and remove synced profiles](/commands/signing#renew-refresh-and-remove-synced-profiles). Per-target Xcode build settings come from `xcode signing plan --profile`, and the dedicated keychain lifecycle is under [Manage a dedicated keychain](/commands/signing#manage-a-dedicated-keychain). Treat certificate rotation as a separate reviewed operation. Renew first, push the successor identity, verify a clean pull, then revoke or deactivate; the ordering is under [Rotation and expiry](/commands/signing#rotation-and-expiry).

Read the [signing command reference](/commands/signing) before the first live push; it includes file-permission rules, exact output fields, failure boundaries, profile maintenance, and recovery behavior.

---

### Guides Metadata Management (`guides/metadata-management.mdx`)

# Metadata Management

> Manage app metadata and localizations across multiple languages

Manage your app's metadata, descriptions, keywords, and localizations efficiently using the CLI.

## Overview

App metadata includes:

* **App Info**: Name, subtitle, privacy policy, category (persistent across versions)
* **Version Localizations**: Description, keywords, what's new, release notes (version-specific)
* **Screenshots and Previews**: Visual assets for the App Store
* **Search Keywords**: Terms to improve discoverability

The CLI separates version-specific metadata from app-level metadata, allowing you to manage each independently.

## App-Level Metadata (App Info)

App Info contains metadata that persists across all versions:

### View App Info Localizations

```bash  theme={null}
asc localizations list --app "YOUR_APP_ID" --type app-info
```

Filter by locale:

```bash  theme={null}
asc localizations list --app "YOUR_APP_ID" --type app-info --locale "en-US,es-ES"
```

### Update App Info Localization

```bash  theme={null}
asc app-setup info set \
  --app "YOUR_APP_ID" \
  --locale "en-US" \
  --name "My Awesome App" \
  --subtitle "The best app for productivity" \
  --privacy-policy-url "https://example.com/privacy"
```

### Create Another Language Localization

Seed the target locale explicitly with app-level metadata:

```bash  theme={null}
asc app-setup info set \
  --app "YOUR_APP_ID" \
  --locale "fr-FR" \
  --name "Mon Application Géniale" \
  --subtitle "L'app de productivité idéale"
```

Then update locale-specific fields:

```bash  theme={null}
asc app-setup info set \
  --app "YOUR_APP_ID" \
  --locale "fr-FR" \
  --privacy-policy-url "https://example.com/fr/privacy"
```

## Version-Specific Metadata

Version localizations contain metadata specific to each app version:

### View Version Localizations

```bash  theme={null}
asc localizations list --version "VERSION_ID"
```

Filter by specific locales:

```bash  theme={null}
asc localizations list --version "VERSION_ID" --locale "en-US,ja"
```

### Update Version Localization

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "en-US" \
  --description "Comprehensive app description with key features and benefits" \
  --keywords "productivity,tools,utility,workflow" \
  --whats-new "New features in this version:\n- Feature 1\n- Feature 2\n- Bug fixes"
```

### Update Marketing URL and Support URL

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "en-US" \
  --marketing-url "https://example.com/myapp" \
  --support-url "https://support.example.com"
```

### Update Promotional Text

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "en-US" \
  --promotional-text "Limited time offer: Get 50% off premium features!"
```

## Working with Localization Files

For bulk operations, use `.strings` file format:

### Download Localizations

<Steps>
  <Step title="Download version localizations">
    Download all version localizations to a directory:

    ```bash  theme={null}
    asc localizations download \
      --version "VERSION_ID" \
      --path "./localizations" \
      --paginate
    ```

    This creates files like:

    ```
    localizations/
    ├── en-US.strings
    ├── es-ES.strings
    ├── fr-FR.strings
    └── ja.strings
    ```
  </Step>

  <Step title="Download specific locales">
    Download only certain locales:

    ```bash  theme={null}
    asc localizations download \
      --version "VERSION_ID" \
      --locale "en-US,es-ES" \
      --path "./localizations"
    ```
  </Step>

  <Step title="Download app info localizations">
    Download app-level metadata:

    ```bash  theme={null}
    asc localizations download \
      --app "YOUR_APP_ID" \
      --type app-info \
      --path "./app-info-localizations" \
      --paginate
    ```
  </Step>
</Steps>

### Edit .strings Files

The `.strings` files use standard format:

```strings  theme={null}
/* App Store Description */
"description" = "Your comprehensive app description here...";

/* Search Keywords (comma-separated) */
"keywords" = "productivity,tools,utility";

/* What's New */
"whatsNew" = "New in this version:\n- Feature 1\n- Feature 2";

/* Marketing URL */
"marketingUrl" = "https://example.com/myapp";

/* Support URL */
"supportUrl" = "https://support.example.com";

/* Promotional Text */
"promotionalText" = "Limited time promotion!";
```

### Upload Localizations

<Steps>
  <Step title="Test with dry-run">
    Validate files without uploading:

    ```bash  theme={null}
    asc localizations upload \
      --version "VERSION_ID" \
      --path "./localizations" \
      --dry-run
    ```
  </Step>

  <Step title="Upload version localizations">
    Upload all edited localizations:

    ```bash  theme={null}
    asc localizations upload \
      --version "VERSION_ID" \
      --path "./localizations"
    ```
  </Step>

  <Step title="Upload specific locales">
    Upload only certain locales:

    ```bash  theme={null}
    asc localizations upload \
      --version "VERSION_ID" \
      --locale "en-US,es-ES" \
      --path "./localizations"
    ```
  </Step>

  <Step title="Upload app info localizations">
    Upload app-level metadata:

    ```bash  theme={null}
    asc localizations upload \
      --app "YOUR_APP_ID" \
      --type app-info \
      --path "./app-info-localizations"
    ```
  </Step>
</Steps>

## Search Keywords Management

Optimize App Store search with keywords:

### List Current Keywords

```bash  theme={null}
asc localizations search-keywords list --localization-id "LOCALIZATION_ID"
```

### Update Keywords

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "en-US" \
  --keywords "productivity,calendar,tasks,todo,organizer,planner,schedule"
```

<Note>
  Keywords are limited to 100 characters total (including commas). Focus on relevant, high-traffic search terms.
</Note>

## Preview and Screenshot Sets

### List Preview Sets

```bash  theme={null}
asc localizations preview-sets list --localization-id "LOCALIZATION_ID"
```

### Get Preview Set Details

```bash  theme={null}
asc localizations preview-sets view --id "PREVIEW_SET_ID"
```

### List Screenshot Sets

```bash  theme={null}
asc localizations screenshot-sets list --localization-id "LOCALIZATION_ID"
```

### Get Screenshot Set Details

```bash  theme={null}
asc localizations screenshot-sets view --id "SCREENSHOT_SET_ID"
```

For uploading screenshots, see the [Screenshots Guide](/guides/screenshots).

## Complete Localization Workflow

Here's a complete workflow for managing localizations:

```bash  theme={null}
#!/bin/bash
set -e

# Configuration
export ASC_APP_ID="YOUR_APP_ID"
VERSION="1.0.0"

# 1. Get version ID
VERSION_ID=$(asc versions list \
  --app "$ASC_APP_ID" \
  --output json | \
  jq -r '.data[] | select(.attributes.versionString == "'$VERSION'") | .id' | head -1)

echo "Version ID: $VERSION_ID"

# 2. Download existing localizations
asc localizations download \
  --version "$VERSION_ID" \
  --path "./localizations" \
  --paginate

# 3. Edit files locally
echo "Edit the .strings files in ./localizations/"
read -p "Press enter when ready to upload..."

# 4. Test with dry-run
asc localizations upload \
  --version "$VERSION_ID" \
  --path "./localizations" \
  --dry-run

# 5. Upload changes
echo "Uploading localizations..."
asc localizations upload \
  --version "$VERSION_ID" \
  --path "./localizations"

echo "Localizations updated successfully!"

# 6. Verify changes
asc localizations list --version "$VERSION_ID" --output table
```

## Multi-Language Support

### Supported Locales

Common App Store locales:

Use `asc localizations supported-locales --version "VERSION_ID"` to inspect the
shared CLI locale catalog and see which version locales are already configured.

* `en-US` - English (US)
* `en-GB` - English (UK)
* `es-ES` - Spanish (Spain)
* `es-MX` - Spanish (Mexico)
* `fr-FR` - French
* `de-DE` - German
* `it` - Italian
* `ja` - Japanese
* `ko` - Korean
* `pt-BR` - Portuguese (Brazil)
* `pt-PT` - Portuguese (Portugal)
* `ru` - Russian
* `zh-Hans` - Chinese (Simplified)
* `zh-Hant` - Chinese (Traditional)
* `ar-SA` - Arabic
* `nl-NL` - Dutch
* `th` - Thai
* `vi` - Vietnamese

### Add a New Language

<Steps>
  <Step title="Create app info localization">
    ```bash  theme={null}
    asc app-setup info set \
      --app "YOUR_APP_ID" \
      --locale "ja" \
      --name "アプリ名" \
      --subtitle "日本語のサブタイトル"
    ```
  </Step>

  <Step title="Create version localization">
    ```bash  theme={null}
    asc localizations create \
      --version "VERSION_ID" \
      --locale "ja" \
      --description "日本語の説明" \
      --keywords "生産性,ツール" \
      --whats-new "新機能"
    ```
  </Step>

  <Step title="Upload screenshots for the locale">
    See the [Screenshots Guide](/guides/screenshots) for uploading locale-specific screenshots.
  </Step>
</Steps>

### Bulk Language Setup

Set up multiple languages at once:

```bash  theme={null}
#!/bin/bash

LOCALES=("es-ES" "fr-FR" "de-DE" "ja" "zh-Hans")
VERSION_ID="YOUR_VERSION_ID"
APP_ID="YOUR_APP_ID"

for locale in "${LOCALES[@]}"; do
  echo "Setting up $locale..."
  
  # Seed app info localization for this locale
  asc app-setup info set \
    --app "$APP_ID" \
    --locale "$locale" \
    --name "TODO localized app name" \
    --subtitle "TODO localized app subtitle"
  
  # Note: You'll still need to translate the actual text content
  # This just creates the locale structure
done

echo "All locales created. Update translations in App Store Connect or via CLI."
```

## Best Practices

1. **Download before editing**: Always download current localizations before making changes

2. **Use dry-run**: Test uploads with `--dry-run` to catch errors before applying changes

3. **Separate app info from version metadata**: Update persistent metadata (name, subtitle) via `app-setup info set`; create new version locales with `localizations create`, then edit existing version metadata with `localizations update`

4. **Optimize keywords**: Research popular search terms and use all 100 characters effectively

5. **Keep what's new concise**: Focus on 3-5 key updates that matter to users

6. **Maintain consistency**: Keep terminology and tone consistent across all locales

7. **Use professional translation**: For important markets, use professional translation services

8. **Test in target regions**: If possible, have native speakers review localizations

## Troubleshooting

### "Localization not found"

**Problem**: The locale doesn't exist for the version.

**Solution**: Create the localization first:

```bash  theme={null}
asc localizations create \
  --version "VERSION_ID" \
  --locale "ja" \
  --description "Initial description"
```

### "Invalid keywords format"

**Problem**: Keywords exceed 100 characters or contain invalid characters.

**Solution**: Trim keywords to fit the limit:

```bash  theme={null}
# Good (under 100 chars)
--keywords "productivity,task,calendar,todo,planner"

# Bad (over 100 chars)
--keywords "productivity,task,calendar,todo,planner,organizer,scheduler,reminder,notes,documents,collaboration"
```

### "Failed to upload .strings file"

**Problem**: File format is incorrect or contains parsing errors.

**Solution**: Validate the file format:

```strings  theme={null}
/* Correct format */
"description" = "Value here";

/* Incorrect - missing semicolon */
"description" = "Value here"

/* Incorrect - missing quotes */
description = "Value here";
```

### "Submission preflight failed: missing fields"

**Problem**: Required localization fields are empty when submitting.

**Solution**: Ensure all required fields are populated:

```bash  theme={null}
asc localizations update \
  --version "VERSION_ID" \
  --locale "en-US" \
  --description "Required description" \
  --keywords "required,keywords" \
  --whats-new "Required what's new text"
```

## Related Commands

* [App Store Submission](/guides/app-store-submission) - Submit apps with validated metadata
* [Screenshots](/guides/screenshots) - Manage visual assets
* [App Info API](/commands/app-info) - Detailed API reference

---

### Guides Screenshots (`guides/screenshots.mdx`)

# Screenshots and Previews

> Manage App Store screenshots and preview videos

Manage your App Store screenshots from capture to upload, including local automation workflows.

## Overview

The CLI provides two screenshot workflows:

1. **App Store Workflow**: Upload and manage screenshots in App Store Connect
2. **Local Automation**: Capture, frame, review, and prepare screenshots locally

## App Store Screenshot Management

The commands below upload screenshots directly to an app version. To store images
and videos independently and reuse them across version, custom product page,
optimization treatment, and event localizations, see the [Asset Library guide](/guides/asset-library).

### Understanding Screenshot Requirements

View required screenshot sizes for App Store submission:

```bash  theme={null}
asc screenshots sizes
```

For the full matrix including all device types:

```bash  theme={null}
asc screenshots sizes --all
```

<Note>
  For most iOS submissions, you need:

  * One iPhone set: `IPHONE_65` (6.5" display)
  * One iPad set: `IPAD_PRO_3GEN_129` (12.9" iPad Pro 3rd gen)

  The App Store will automatically scale these to other sizes.
</Note>

### Upload Screenshots

<Steps>
  <Step title="Prepare screenshot files">
    Export RGB PNG or JPEG images without alpha channels or transparency.
    Screenshot and Asset Library image uploads reject PNG alpha channels and
    transparency before reserving an upload. Even an alpha channel whose pixels
    are all opaque must be removed when exporting.

    Organize screenshots by device type:

    ```
    screenshots/
    ├── iphone/
    │   ├── 01-home.png
    │   ├── 02-features.png
    │   └── 03-settings.png
    └── ipad/
        ├── 01-home.png
        ├── 02-features.png
        └── 03-settings.png
    ```
  </Step>

  <Step title="Upload iPhone screenshots">
    Select the localization with the app, version string, and locale. No
    localization ID lookup is needed:

    ```bash  theme={null}
    asc screenshots upload \
      --app "YOUR_APP_ID" \
      --version "1.2.3" \
      --locale "en-US" \
      --path "./screenshots/iphone" \
      --device-type "IPHONE_65" \
      --concurrency 4
    ```

    With `--locale`, `--path` points directly at that locale's screenshots.
    The version must already have an `en-US` localization; otherwise the
    command fails and lists the locales the version has.

    Uploads within one screenshot set are bounded to four concurrent requests
    by default. Use `--concurrency 1` for serial uploads or any value through
    `8` when the API and network can sustain more fan-out; output ordering stays
    deterministic.
  </Step>

  <Step title="Upload iPad screenshots">
    ```bash  theme={null}
    asc screenshots upload \
      --app "YOUR_APP_ID" \
      --version "1.2.3" \
      --locale "en-US" \
      --path "./screenshots/ipad" \
      --device-type "IPAD_PRO_3GEN_129"
    ```
  </Step>
</Steps>

If you already have a version localization ID, `--version-localization
"LOCALIZATION_ID"` uploads to it directly. `asc localizations list --app
"YOUR_APP_ID" --version "1.2.3"` shows each localization's ID.

### Device Types

Common device types for screenshots:

**iPhone:**

* `IPHONE_65` - 6.5" display (iPhone 14 Plus, etc.)
* `IPHONE_55` - 5.5" display (iPhone 8 Plus)
* `IPHONE_40` - 4" display (iPhone SE 1st gen)

**iPad:**

* `IPAD_PRO_3GEN_129` - 12.9" iPad Pro (3rd gen)
* `IPAD_PRO_129` - 12.9" iPad Pro (1st/2nd gen)
* `IPAD_105` - 10.5" iPad Pro
* `IPAD_97` - 9.7" iPad

**Apple Watch:**

* `APPLE_WATCH_SERIES_7` - 45mm Series 7
* `APPLE_WATCH_SERIES_4` - 44mm Series 4
* `APPLE_WATCH_SERIES_3` - 42mm Series 3

**Mac:**

* `DESKTOP`

**Apple TV:**

* `APPLE_TV`

### List Existing Screenshots

```bash  theme={null}
asc screenshots list --version-localization "LOCALIZATION_ID"
```

Filter client-side by device type:

```bash  theme={null}
asc screenshots list \
  --version-localization "LOCALIZATION_ID" \
  --output json | jq '.data[] | select(.attributes.displayType == "IPHONE_65")'
```

### Download Screenshots

Download all screenshots for a localization:

```bash  theme={null}
asc screenshots download \
  --version-localization "LOCALIZATION_ID" \
  --output-dir "./downloaded-screenshots"
```

### Delete Screenshots

Delete a specific screenshot:

```bash  theme={null}
asc screenshots delete --id "SCREENSHOT_ID" --confirm
```

<Warning>
  Deleting screenshots is permanent. Download backups before deleting.
</Warning>

## Local Screenshot Automation

<Note>
  Please report issues at:
  [https://github.com/rorkai/App-Store-Connect-CLI/issues/new/choose](https://github.com/rorkai/App-Store-Connect-CLI/issues/new/choose)
</Note>

### Complete Local Workflow

<Steps>
  <Step title="Capture screenshots from simulator">
    Launch your app in iOS Simulator, then capture screenshots:

    ```bash  theme={null}
    asc screenshots capture \
      --bundle-id "com.example.myapp" \
      --name "home-screen" \
      --output-dir "./screenshots/raw"
    ```

    This captures from the currently running simulator.
  </Step>

  <Step title="Frame screenshots with device bezel">
    Add device frame around raw screenshots:

    ```bash  theme={null}
    asc screenshots frame \
      --input "./screenshots/raw/home-screen.png" \
      --device "iphone-air" \
      --output-dir "./screenshots/framed"
    ```

    See available device frames:

    ```bash  theme={null}
    asc screenshots list-frame-devices
    ```
  </Step>

  <Step title="Generate HTML review gallery">
    Create an HTML page to review all screenshots:

    ```bash  theme={null}
    asc screenshots review-generate \
      --framed-dir "./screenshots/framed" \
      --output-dir "./screenshots/review"
    ```
  </Step>

  <Step title="Open review in browser">
    ```bash  theme={null}
    asc screenshots review-open --output-dir "./screenshots/review"
    ```

    This opens a browser with an interactive gallery for review.
  </Step>

  <Step title="Approve screenshots">
    Mark screenshots as ready for upload:

    ```bash  theme={null}
    asc screenshots review-approve \
      --all-ready \
      --output-dir "./screenshots/review"
    ```

    Or approve selectively in the browser UI.
  </Step>

  <Step title="Preview uploads from approved screenshots">
    Build a deterministic upload plan from the approved review artifacts:

    ```bash  theme={null}
    asc screenshots plan \
      --app "123456789" \
      --version "1.2.3"
    ```
  </Step>

  <Step title="Apply the approved upload plan">
    Apply the reviewed upload plan explicitly:

    ```bash  theme={null}
    asc screenshots apply \
      --app "123456789" \
      --version "1.2.3" \
      --confirm
    ```
  </Step>
</Steps>

### Screenshot Plan Automation

Define a complete screenshot plan in `.asc/screenshots.json`:

```json  theme={null}
{
  "bundle_id": "com.example.myapp",
  "device_types": ["iphone-air", "ipad-pro-129"],
  "locales": ["en-US", "es-ES", "ja"],
  "screenshots": [
    {
      "name": "home",
      "description": "Home screen showing main features",
      "device_type": "iphone-air"
    },
    {
      "name": "feature-detail",
      "description": "Detailed feature view",
      "device_type": "iphone-air"
    },
    {
      "name": "settings",
      "description": "Settings and customization",
      "device_type": "iphone-air"
    }
  ],
  "output_dirs": {
    "raw": "./screenshots/raw",
    "framed": "./screenshots/framed",
    "review": "./screenshots/review"
  }
}
```

Run the entire workflow:

```bash  theme={null}
asc screenshots run --plan .asc/screenshots.json
```

### Available Frame Devices

List supported devices for framing:

```bash  theme={null}
asc screenshots list-frame-devices --output json
```

Supported devices:

* iPhone: `iphone-air` (default), `iphone-17-pro`, `iphone-17-pro-max`, `iphone-16e`, `iphone-17`
* iPad: `ipad-pro-13`, `ipad-pro-11`, `ipad-air-13`, `ipad-air-11`, `ipad-mini`
* Apple Watch: `watch-series-11`, `watch-ultra-3`
* Apple TV: `apple-tv`
* Mac: `mac` (canvas without a bezel)

Frame each device from a capture of the same screen shape, such as a 422x514
Apple Watch Ultra 3 capture for `watch-ultra-3`. When an input's aspect ratio
differs from the device screen by more than 2%, `asc screenshots frame` prints a
warning on stderr, because the image is letterboxed inside the screen.

Native simulator captures from `xcrun simctl io <udid> screenshot` fill the
frame without that warning:

| Frame device | Simulator capture | Size |
| --- | --- | --- |
| `watch-series-11` | Apple Watch Series 11 or 12 (46mm) | 416x496 |
| `watch-ultra-3` | Apple Watch Ultra 3 or 4 (49mm) | 422x514 |
| `apple-tv` | Apple TV 4K | 3840x2160 |

```bash  theme={null}
xcrun simctl io "$WATCH_UDID" screenshot ./screenshots/raw/watch-home.png
asc screenshots frame --input ./screenshots/raw/watch-home.png --device watch-ultra-3 \
  --title "Roll the die" --text-position bottom
```

Each device entry lists its `frameColors`; pass one with `--frame-color`. See [Frame Screenshots](/commands/screenshots#frame-screenshots) for the full device and color matrix.

To frame a whole directory and retry only failed inputs on the next run:

```bash  theme={null}
asc screenshots frame --input-dir ./screenshots/raw --output-dir ./screenshots/framed \
  --device ipad-air-13 --frame-color blue --title "Plan every trip" --resume
```

To keep captions legible on a busy or light background and use a brand font file that is not installed on the machine (for example, a CI runner), add a text box and pass the font file to `--font`:

```bash  theme={null}
asc screenshots frame --input-dir ./screenshots/raw --output-dir ./screenshots/framed \
  --device iphone-air --title "Plan every trip" --bg-color "#f2c14e" \
  --font ./brand/Brand-Bold.ttf --text-box --text-box-color "#1b3a5bcc"
```

Per-file box settings go in `--overlay-config` entries as `textBox`, `textBoxColor`, `textBoxPadding`, and `textBoxRadius`. See [Frame Screenshots](/commands/screenshots#frame-screenshots) for the defaults and validation rules.

Large batches are CPU-bound. Add `--parallel-workers 4` (up to your CPU count) to render four inputs at once; the receipt order and `--resume` behavior stay the same.

## Complete Screenshot Workflow Example

Here's a complete workflow for managing screenshots:

```bash  theme={null}
#!/bin/bash
set -e

# Configuration
export ASC_APP_ID="YOUR_APP_ID"
VERSION="1.2.3"
BUNDLE_ID="com.example.myapp"

# 1. Check the version's localizations
asc localizations list --app "$ASC_APP_ID" --version "$VERSION" --output table

# 2. Create directories
mkdir -p screenshots/{raw,framed,iphone,ipad}

# 3. Capture screenshots (requires simulator running)
echo "Launch your app in iOS Simulator and press enter..."
read

for screen in home features settings details; do
  echo "Navigate to $screen screen and press enter..."
  read
  asc screenshots capture \
    --bundle-id "$BUNDLE_ID" \
    --name "$screen" \
    --output-dir "./screenshots/raw"
done

# 4. Frame screenshots
echo "Framing screenshots..."
for raw_file in screenshots/raw/*.png; do
  filename=$(basename "$raw_file")
  asc screenshots frame \
    --input "$raw_file" \
    --device "iphone-air" \
    --output-path "./screenshots/framed/$filename"
done

# 5. Copy to upload directories
cp screenshots/framed/*.png screenshots/iphone/

# 6. Upload to App Store Connect
echo "Uploading iPhone screenshots..."
asc screenshots upload \
  --app "$ASC_APP_ID" \
  --version "$VERSION" \
  --locale "en-US" \
  --path "./screenshots/iphone" \
  --device-type "IPHONE_65"

echo "Screenshots uploaded successfully!"

# 7. Verify upload
asc screenshots list --app "$ASC_APP_ID" --version "$VERSION" --locale "en-US"
```

If an upload stops partway through, the command prints a failure artifact path like
`.asc/reports/screenshots-upload/failures-123.json`. Resume the remaining files with:

```bash  theme={null}
asc screenshots upload --resume ".asc/reports/screenshots-upload/failures-123.json"
```

## Preview Videos

Upload app preview videos:

### Upload Preview Video

```bash  theme={null}
asc video-previews upload \
  --version-localization "LOCALIZATION_ID" \
  --path "./previews/app-preview.mp4" \
  --device-type "IPHONE_65"
```

### Preview Video Requirements

* **Format**: H.264 or HEVC codec, .mov or .mp4
* **Duration**: 15-30 seconds
* **Resolution**: Matches device screenshot size
* **File size**: Up to 500 MB
* **Aspect ratio**: Matches device (typically 16:9 or device aspect)

## Multi-Locale Screenshot Workflow

For apps with multiple localizations, you can now fan out one upload across
all matching locale directories:

```bash  theme={null}
asc screenshots upload \
  --app "YOUR_APP_ID" \
  --version "1.2.3" \
  --path "./screenshots" \
  --device-type "IPHONE_65"
```

Recommended directory layout:

```text
screenshots/
  en-US/
    iphone/
      01-home.png
      02-settings.png
  fr-FR/
    iphone/
      01-home.png
  ja/
    iphone/
      01-home.png
```

The command resolves the app store version, fetches its version localizations,
and uploads each locale subtree to the matching remote localization. It only
includes files whose dimensions match the selected `--device-type`, so mixed
iPhone/iPad locale trees are safe as long as each locale contains the slot
you are uploading. A device-rooted layout such as `screenshots/iphone/en-US`
also works when `--path` points at `./screenshots/iphone`.

For CI or large locale batches, save the printed failure artifact as a workflow
artifact so a rerun can continue with
`asc screenshots upload --resume <artifact>`.

## Troubleshooting

### "Invalid image dimensions"

**Problem**: Screenshot dimensions don't match the device type.

**Solution**: Verify required dimensions:

```bash  theme={null}
asc screenshots sizes
```

Ensure your images match exactly:

* iPhone 6.5": 1242 x 2688 pixels
* iPad Pro 12.9": 2048 x 2732 pixels

### "Capture failed - no simulator running"

**Problem**: `asc screenshots capture` requires a running simulator.

**Solution**:

1. Launch iOS Simulator
2. Open your app in the simulator
3. Run the capture command

### "Frame device not found"

**Problem**: Invalid device name for framing.

**Solution**: List available devices:

```bash  theme={null}
asc screenshots list-frame-devices
```

Use exact device names from the output.

### "Upload failed - file too large"

**Problem**: Image file size exceeds limits.

**Solution**: Optimize images:

```bash  theme={null}
# Using ImageMagick
mogrify -quality 85 -resize '1242x2688>' screenshots/*.png

# Using pngquant for compression
pngquant --quality=80-95 screenshots/*.png
```

## Best Practices

1. **Use consistent naming**: Name screenshots clearly (`01-home.png`, `02-features.png`)

2. **Optimize file sizes**: Compress screenshots to reduce upload time while maintaining quality

3. **Follow Apple guidelines**:
   * No UI chrome (status bars are optional)
   * Show actual app content
   * Use device-appropriate assets

4. **Localize screenshots**: Create locale-specific screenshots showing localized UI

5. **Test on real devices**: Capture from actual devices when possible for best quality

6. **Version control**: Keep screenshot sources in git for easy updates

7. **Automate where possible**: Use the local workflow tools to streamline repetitive tasks

8. **Review before upload**: Always review screenshots in context before uploading to App Store Connect

## Related Commands

* [Metadata Management](/guides/metadata-management) - Manage app descriptions and metadata
* [App Store Submission](/guides/app-store-submission) - Submit with screenshots
* [Localizations API](/commands/localizations) - Manage version localizations

---

### Guides Testflight (`guides/testflight.mdx`)

# TestFlight Distribution

> Distribute builds to beta testers using TestFlight

Manage your TestFlight beta testing program end-to-end, from organizing beta groups to collecting feedback from testers.

## Overview

TestFlight allows you to distribute pre-release builds to internal and external testers. The CLI provides complete automation for:

* Managing beta groups and testers
* Distributing builds to specific groups
* Collecting and reviewing crash reports and feedback
* Tracking beta tester metrics

## Beta Groups Workflow

<Steps>
  <Step title="List existing beta groups">
    View all beta groups for your app:

    ```bash  theme={null}
    asc testflight groups list --app "YOUR_APP_ID"
    ```

    Filter to internal or external groups:

    ```bash  theme={null}
    asc testflight groups list --app "YOUR_APP_ID" --internal
    asc testflight groups list --app "YOUR_APP_ID" --external
    ```
  </Step>

  <Step title="Create a new beta group">
    Create an external beta group:

    ```bash  theme={null}
    asc testflight groups create --app "YOUR_APP_ID" --name "External Beta Testers"
    ```

    Create an internal beta group:

    ```bash  theme={null}
    asc testflight groups create --app "YOUR_APP_ID" --name "QA Team" --internal
    ```
  </Step>

  <Step title="Configure group settings">
    Enable public link for external distribution:

    ```bash  theme={null}
    asc testflight groups edit --id "GROUP_ID" \
      --public-link-enabled \
      --public-link-limit-enabled \
      --public-link-limit 100
    ```

    Enable feedback collection:

    ```bash  theme={null}
    asc testflight groups edit --id "GROUP_ID" --feedback-enabled
    ```
  </Step>
</Steps>

## Managing Beta Testers

<Steps>
  <Step title="Add beta testers">
    Add a new tester to a beta group:

    ```bash  theme={null}
    asc testflight testers add \
      --app "YOUR_APP_ID" \
      --email "tester@example.com" \
      --group "Beta Testers"
    ```

    Optionally include name details:

    ```bash  theme={null}
    asc testflight testers add \
      --app "YOUR_APP_ID" \
      --email "tester@example.com" \
      --first-name "Jane" \
      --last-name "Doe" \
      --group "Beta Testers"
    ```
  </Step>

  <Step title="Import testers in bulk">
    Export existing testers to CSV:

    ```bash  theme={null}
    asc testflight testers export \
      --app "YOUR_APP_ID" \
      --output "./testers.csv"
    ```

    <Note>
      Export prefixes a cell with a single quote when its first character would start a spreadsheet formula (`=`, `+`, `-`, `@`), so opening the file in Excel, Numbers, or Google Sheets cannot execute tester or group values. When a prefix is needed, export adds an `_asc_formula_escaping` provenance column. `asc testflight testers import` only removes prefixes from rows carrying that marker, so leading apostrophes in third-party or user-authored CSV files remain unchanged.
    </Note>

    Import testers from CSV (with dry-run first):

    ```bash  theme={null}
    asc testflight testers import \
      --app "YOUR_APP_ID" \
      --input "./testers.csv" \
      --dry-run

    # After verifying, apply the plan
    asc testflight testers import \
      --app "YOUR_APP_ID" \
      --input "./testers.csv" \
      --confirm
    ```

    `--confirm` is required unless `--dry-run` is set.
  </Step>

  <Step title="List and filter testers">
    List all testers for an app:

    ```bash  theme={null}
    asc testflight testers list --app "YOUR_APP_ID"
    ```

    Filter by beta group:

    ```bash  theme={null}
    asc testflight testers list --app "YOUR_APP_ID" --group "Beta Testers"
    ```

    Filter by build:

    ```bash  theme={null}
    asc testflight testers list --app "YOUR_APP_ID" --build-id "BUILD_ID"
    ```
  </Step>

  <Step title="Send invitations">
    Invite a tester to start testing:

    ```bash  theme={null}
    asc testflight testers invite \
      --app "YOUR_APP_ID" \
      --email "tester@example.com"
    ```

    Invite and auto-create if tester doesn't exist:

    ```bash  theme={null}
    asc testflight testers invite \
      --app "YOUR_APP_ID" \
      --email "newtester@example.com" \
      --group "Beta Testers"
    ```
  </Step>
</Steps>

## Distributing Builds to Groups

<Steps>
  <Step title="Find your build ID">
    List recent builds:

    ```bash  theme={null}
    asc builds list --app "YOUR_APP_ID" --sort "-uploadedDate" --limit 5
    ```

    Get a specific build:

    ```bash  theme={null}
    asc builds info --app "YOUR_APP_ID" --version "1.0" --build-number "42" --platform IOS
    ```
  </Step>

  <Step title="Add build to beta groups">
    Add a build to one or more groups:

    ```bash  theme={null}
    asc builds add-groups --build-id "BUILD_ID" --group "GROUP_ID"
    ```

    Add to multiple groups:

    ```bash  theme={null}
    asc builds add-groups --build-id "BUILD_ID" --group "GROUP_ID1,GROUP_ID2"
    ```
  </Step>

  <Step title="Verify distribution">
    Check which groups have access to a build:

    ```bash  theme={null}
    asc testflight distribution view --build-id "BUILD_ID"
    ```
  </Step>
</Steps>

## Feedback and Crash Reports

<Steps>
  <Step title="View crash submissions">
    Get crash submission details:

    ```bash  theme={null}
    asc testflight crashes view --submission-id "SUBMISSION_ID"
    ```

    Download the crash log:

    ```bash  theme={null}
    asc testflight crashes log --submission-id "SUBMISSION_ID"
    ```
  </Step>

  <Step title="View screenshot feedback">
    Get screenshot submission with annotations:

    ```bash  theme={null}
    asc testflight feedback view --submission-id "SUBMISSION_ID"
    ```
  </Step>

  <Step title="Retrieve crash logs">
    Get detailed crash logs for analysis:

    ```bash  theme={null}
    asc testflight crashes log --crash-log-id "CRASH_LOG_ID"
    ```
  </Step>

  <Step title="Clean up old feedback (optional)">
    Delete processed crash submissions:

    ```bash  theme={null}
    asc testflight crashes delete --submission-id "SUBMISSION_ID" --confirm
    ```

    Delete screenshot submissions:

    ```bash  theme={null}
    asc testflight feedback delete --submission-id "SUBMISSION_ID" --confirm
    ```
  </Step>
</Steps>

## Beta Tester Metrics

Track how testers engage with your builds:

```bash  theme={null}
asc testflight metrics app-testers --app "YOUR_APP_ID"
```

This provides insights into:

* Number of sessions per tester
* App version usage
* Install and session counts

## Complete Example Workflow

Here's a complete workflow for distributing a new build:

```bash  theme={null}
# 1. Set your app ID
export ASC_APP_ID="YOUR_APP_ID"

# 2. Find the latest build
asc builds list --app "$ASC_APP_ID" --sort "-uploadedDate" --limit 1 --output json
BUILD_ID="..." # Extract from output

# 3. Create or find your beta group
asc testflight groups list --app "$ASC_APP_ID"
GROUP_ID="..." # Use existing or create new

# 4. Add build to the beta group
asc builds add-groups --build-id "$BUILD_ID" --group "$GROUP_ID"

# 5. List testers in the group
asc testflight testers list --app "$ASC_APP_ID" --group "$GROUP_ID"

# 6. Add new testers if needed
asc testflight testers add \
  --app "$ASC_APP_ID" \
  --email "newtester@example.com" \
  --group "Beta Testers"

# 7. Send invitations
asc testflight testers invite --app "$ASC_APP_ID" --email "newtester@example.com"

# 8. Monitor beta tester activity
asc testflight metrics app-testers --app "$ASC_APP_ID"
```

## Troubleshooting

### Tester Not Receiving Invitation

1. Verify the tester is added to the group:
   ```bash  theme={null}
   asc testflight testers list --app "YOUR_APP_ID" --group "GROUP_ID"
   ```

2. Check if invitation was sent:
   ```bash  theme={null}
   asc testflight testers view --id "TESTER_ID"
   ```

3. Resend invitation:
   ```bash  theme={null}
   asc testflight testers invite --app "YOUR_APP_ID" --email "tester@example.com"
   ```

### Build Not Appearing for Testers

1. Verify build is assigned to the group:
   ```bash  theme={null}
   asc testflight distribution view --build-id "BUILD_ID"
   ```

2. Check build processing status:
   ```bash  theme={null}
   asc builds info --build-id "BUILD_ID"
   ```

3. Ensure build passed TestFlight review:
   ```bash  theme={null}
   asc testflight review submissions list --build-id "BUILD_ID"
   ```

### Managing Group Limits

Internal groups have a limit of 100 testers. External groups can have up to 10,000 testers.

To check current tester counts:

```bash  theme={null}
asc testflight groups view --id "GROUP_ID"
```

If you hit limits, create additional groups:

```bash  theme={null}
asc testflight groups create --app "YOUR_APP_ID" --name "Beta Testers Group 2"
```

## Best Practices

1. **Organize by testing focus**: Create separate groups for different testing purposes (QA, feature testing, general beta)

2. **Use CSV import for bulk operations**: Export your current tester list, make changes, and re-import

3. **Enable feedback collection**: Turn on feedback for external groups to collect valuable user input

4. **Monitor crash reports**: Regularly check crash submissions and address issues before App Store release

5. **Set public link limits**: For public beta links, set reasonable limits to control your beta population size

6. **Track metrics**: Use beta tester usage metrics to identify your most active testers

## Related Commands

* [App Store Submission](/guides/app-store-submission) - Submit builds for App Store review
* [Build Management](/commands/builds) - Manage builds in App Store Connect
* [Analytics and Reports](/guides/analytics-reports) - Download usage and crash analytics

---

### Architecture Apple Ads Api Support (`docs/architecture/apple-ads-api-support.md`)

# Apple Ads Campaign Management API v5 Support (Historical Scope)

Status: Implemented; retained as the v5 compatibility reference
Research date: May 31, 2026
Original target API: Apple Ads Campaign Management API 5.5
Historical command root: `asc ads`
Release target: 4.4.0
Current API addition: Apple Ads Platform API v1
Preferred API in CLI 4.4.0: Apple Ads Platform API v1 under direct `asc ads`

## Historical goal

This note records the existing Campaign Management API v5 surface and the
4.4.0 addition of the Apple Ads Platform API v1 surface. Direct `asc ads`
resource commands use v1. The incompatible, retiring v5 surface moves under
`asc ads v5`; the intermediate nested prototype is removed before
merge. Users should not need a raw HTTP client for supported workflows.

The v1 and v5 request contracts remain separate because they use different
hosts, contexts, payloads, and response envelopes. Existing v5 commands move
under the explicit `v5` group instead of being silently retargeted.

The original implementation preserved the local CLI style:

- `ffcli` commands with `shared.DefaultUsageFunc`
- explicit long flags
- TTY-aware output defaults through `shared.BindOutputFlags`
- non-interactive behavior only
- `--confirm` for deletes and bulk deletes
- `--paginate` for multi-page list/search commands
- JSON output that preserves Apple response envelopes for agents
- no new third-party dependencies

## Historical v5 sources

Apple sources used for the May 2026 v5 implementation:

- Apple Ads root: https://developer.apple.com/documentation/apple_ads
- Platform API v1: https://developer.apple.com/documentation/apple-ads-platform-api
- OAuth: https://developer.apple.com/documentation/apple_ads/implementing-oauth-for-the-apple-search-ads-api
- Calling the API: https://developer.apple.com/documentation/apple_ads/calling-the-apple-search-ads-api
- API functionality: https://developer.apple.com/documentation/apple_ads/using-apple-search-ads-api-functionality
- API 5 changelog: https://developer.apple.com/documentation/apple_ads/apple-search-ads-campaign-management-api-5

At the original research date, API 5 was Apple's current Campaign Management
API and API 5.5 had been released in February 2026. Apple later made Platform
API v1 available. CLI 4.4.0 implements that API under direct `asc ads` resource
paths and keeps the original v5 commands under `asc ads v5` as warning-producing compatibility paths until
Apple's January 26, 2027 retirement date.

At the research date, deprecated `Creative Sets` were not included as commands
because Apple's v5 documentation marked the collection as deprecated and
exposed no active v5 endpoint under that page. The
`includeDeletedCreativeSetAssets` query parameter on
`GET /v5/creatives/{creativeId}` was included.

AdServices Attribution API is out of scope. It is not part of the Apple Ads
Campaign Management API command surface and has different caller requirements.

## Platform API v1 in 4.4.0

The cumulative 4.4.0 implementation stack is intended to add all 99 documented
operations under direct `asc ads` resource paths: this foundation layer registers
13 operations, the campaign layer adds 41, Maps and assets add 21, and reports
and optimization add 24. At each intermediate PR, only the operations registered
by that layer are available; the cumulative stack is what provides the complete
surface.
Ad-account context is carried in
`X-AP-Context: adAccountId=<ad-account-id>;` for account-scoped requests. The
`--ad-account` flag, `ASC_ADS_AD_ACCOUNT_ID`, and the selected profile's
`ad_account_id` are independent of the legacy v5 `--org`/`org_id` context.

The `/v1/ad-accounts` collection is method-dependent:

| Operation | Context behavior |
| --- | --- |
| `POST /v1/ad-accounts` | Creates an account without `X-AP-Context`; the account context does not exist until the response supplies its ID. |
| `GET /v1/ad-accounts/{id}` | Requires `X-AP-Context: adAccountId=<id>;`; the header account must match the path ID. |
| `PUT /v1/ad-accounts/{id}` | Requires `X-AP-Context: adAccountId=<id>;`; the header account must match the path ID. |

Authentication commands use the Platform API v1 transport:

| Command | Validation/discovery request |
| --- | --- |
| `asc ads auth login --network` | OAuth token exchange, then Platform API v1 `GET /v1/me`. |
| `asc ads auth status --validate` | For each stored credential, OAuth token exchange, then Platform API v1 `GET /v1/me`. |
| `asc ads auth discover` | Platform API v1 `GET /v1/me` and `GET /v1/acls`, returning user and ad-account access. |

All three commands avoid an ad-account context for these requests. The token
exchange remains `POST https://appleid.apple.com/auth/oauth2/token` with the
`searchadsorg` scope; a supplied access token skips that exchange. Platform API
v1 users should call `asc ads me view` and `asc ads acls list`
when they need the v1 user or ACL resources.

## Historical v5 command placement

The original implementation added this command root; in 4.4.0 the historical
v5 leaves move beneath an explicit version group:

```text
asc ads v5 <subcommand> [flags]
```

Common endpoint flags:

- Every Apple Ads endpoint leaf command accepts `--ads-profile NAME`.
- Every org-scoped endpoint leaf command accepts `--org ORG_ID`.
- `asc ads v5 me view`, `asc ads v5 acls list`, and `asc ads auth ...` do not require
  `--org`.
- Examples in the endpoint matrix omit `--org` and `--ads-profile` unless those
  flags are materially relevant to the endpoint. The implementation still adds
  them to the command.
- Resource groups with a natural list endpoint execute list by default:
  `asc ads v5 campaigns`, `asc ads v5 budget-orders`, `asc ads v5 ad-groups`,
  `asc ads v5 creatives`, and `asc ads v5 impression-share-reports`.

These rules describe the deprecated v5 tree. Platform v1 commands live under
direct `asc ads`, use `--ad-account` instead of `--org`, and follow the v1
payload and response contracts documented in
`docs/design/apple-ads-platform-api-v1.md`.

Root help placement:

- Add `ads` to `cmd/root_usage.go` under `ANALYTICS & FINANCE COMMANDS` after
  `performance`. This is acquisition/marketing rather than App Store Connect
  resource management, but this is the closest existing command group.
- Register `ads.AdsCommand()` in `internal/cli/registry/registry.go`.
- Add `ads` to registry/root help tests.

Package layout:

```text
internal/appleads/
  auth.go
  auth_store.go
  client.go
  client_test.go
  endpoints.go
  endpoints_test.go
  errors.go
  pagination.go

internal/cli/ads/
  api.go
  root.go
  auth.go
  endpoints.go
  endpoints_test.go
  resolve.go
```

Use `internal/appleads`, not `internal/asc`, because the base URL, auth flow,
error envelope, and pagination model are different from App Store Connect.

Do not implement this as 73 bespoke command functions and 73 bespoke client
methods. Add one `EndpointSpec` table in `internal/appleads/endpoints.go` and
generic command/client builders that consume it.

`EndpointSpec` must include:

```go
type EndpointSpec struct {
	Name             string
	Method           string
	Path             string
	CommandPath      []string
	BodyKind         shared.JSONPayloadKind
	BodyType         string
	ResponseType     string
	RequiresOrg      bool
	RequiresConfirm  bool
	PathParams       []ParamSpec
	QueryParams      []ParamSpec
	SupportsPaginate bool
	DefaultListAlias bool
}

type ParamSpec struct {
	Name      string
	Flag      string
	Type      ParamType
	Required  bool
	Max       int
	Allowed   []string
}
```

Acceptance check: adding a newly documented Apple Ads endpoint requires one
spec row plus docs/tests, not a new hand-written request method.

## Authentication Contract

Apple Ads OAuth is separate from App Store Connect JWT auth.

OAuth facts from Apple:

- Token URL: `https://appleid.apple.com/auth/oauth2/token`
- Grant type: `client_credentials`
- Scope: `searchadsorg`
- Client secret: ES256 JWT
- JWT header: `alg=ES256`, `kid=<key-id>`
- JWT claims:
  - `iss=<team-id>`
  - `iat=<issued-at>`
  - `exp=<expires-at>`
  - `aud=https://appleid.apple.com`
  - `sub=<client-id>`
- Token response contains `access_token`, `token_type=Bearer`,
  `expires_in=3600`, and `scope=searchadsorg`.
- Generate the client secret on demand for each token refresh with a 10-minute
  lifetime and 30-second refresh skew. Do not persist the client secret.
- Send the token request as `application/x-www-form-urlencoded` form values:
  `grant_type`, `client_id`, `client_secret`, and `scope`.

Implement this command group:

```text
asc ads auth login --name NAME --client-id CLIENT_ID --team-id TEAM_ID --key-id KEY_ID --private-key PATH [--org ORG_ID] [--ad-account AD_ACCOUNT_ID] [--network] [--skip-validation] [--bypass-keychain] [--local]
asc ads auth status [--verbose] [--validate] [--output table|json]
asc ads auth discover [--ads-profile NAME] [--org ORG_ID] [--ad-account AD_ACCOUNT_ID] [--output table|json]
asc ads auth switch --name NAME
asc ads auth token --confirm [--output text|json]
asc ads auth doctor [--output text|json]
asc ads auth logout [--all --confirm | --name NAME]
```

Mirror the existing `asc auth` behavior:

- `--name`, `--key-id`, and `--private-key` use the same meaning and validation
  style as `asc auth login`.
- `--client-id` is Apple Ads OAuth `client_id`.
- `--team-id` is the JWT issuer (`iss`).
- `--org` stores a default Apple Ads org ID for API calls.
- `--ad-account` stores a default Platform API v1 ad-account ID and, on
  `auth discover`, marks that account as active in the output. It is separate
  from `--org`.
- `--private-key` accepts the EC P-256 PEM Apple documents for Ads. Reuse the
  existing private-key parsing helpers because they already support ES256 keys.
- `--network` requests an access token and calls Platform API v1 `GET /v1/me`.
- `--skip-validation` skips JWT and network validation.
- `--network` and `--skip-validation` are mutually exclusive.
- `--local` requires keychain bypass, exactly like `asc auth login`.
- Keychain is preferred; config fallback is allowed when bypassing keychain.
- `auth status` supports `--verbose` and `--validate`, matching `asc auth status`.
- `auth status --validate` validates each stored credential through Platform
  API v1 `GET /v1/me` and reports failures after rendering the status output.
- `auth discover` calls Platform API v1 `/v1/me` and `/v1/acls` to show the
  active Ads user and available ad accounts without printing access tokens.
  Each ACL row includes its honest `ad_account_id`; `--ad-account` controls
  which row is marked active.
- Platform API v1 user and ACL discovery is also available directly through
  `asc ads me view` and `asc ads acls list`.
- `auth logout` supports `--all` and `--name`. It requires one of those flags
  so bare `asc ads auth logout` does not clear every stored Ads profile, and
  `--all` requires `--confirm`.

Add config fields without changing existing App Store Connect credentials:

```go
type AdsCredential struct {
	Name           string `json:"name"`
	ClientID       string `json:"client_id"`
	TeamID         string `json:"team_id"`
	KeyID          string `json:"key_id"`
	PrivateKeyPath string `json:"private_key_path"`
	OrgID          string `json:"org_id,omitempty"`
	AdAccountID    string `json:"ad_account_id,omitempty"`
}

type AdsKeychainMetadata struct {
	Name       string `json:"name"`
	ClientID   string `json:"client_id"`
	TeamID     string `json:"team_id"`
	KeyID      string `json:"key_id"`
	OrgID      string `json:"org_id,omitempty"`
	AdAccountID string `json:"ad_account_id,omitempty"`
	ModifiedAt string `json:"modified_at,omitempty"`
}

type AdsConfig struct {
	DefaultKeyName   string                `json:"default_key_name,omitempty"`
	Keys             []AdsCredential       `json:"keys,omitempty"`
	KeychainMetadata []AdsKeychainMetadata `json:"keychain_metadata,omitempty"`
	OrgID            string                `json:"org_id,omitempty"`
	AdAccountID      string                `json:"ad_account_id,omitempty"`
}

type Config struct {
	// existing fields...
	Ads AdsConfig `json:"ads,omitempty"`
}
```

Do not reuse the existing App Store Connect keychain item prefixes. Ads storage
uses:

```text
asc:ads-credential:<name>
asc:ads-metadata:<name>
```

Reuse only private-key parsing/validation helpers from `internal/auth`.

Environment variables:

```text
ASC_ADS_ACCESS_TOKEN
ASC_ADS_CLIENT_ID
ASC_ADS_TEAM_ID
ASC_ADS_KEY_ID
ASC_ADS_PRIVATE_KEY_PATH
ASC_ADS_PRIVATE_KEY
ASC_ADS_PRIVATE_KEY_B64
ASC_ADS_ORG_ID
ASC_ADS_AD_ACCOUNT_ID
ASC_ADS_PROFILE
ASC_ADS_STRICT_AUTH
ASC_ADS_BYPASS_KEYCHAIN
```

Resolution order:

1. Explicit `--ads-profile` if present.
2. `ASC_ADS_PROFILE` if present.
3. `ASC_ADS_ACCESS_TOKEN` if no profile is selected; it bypasses token exchange but still needs
   `--org`, `ASC_ADS_ORG_ID`, or stored profile org for org-scoped calls.
4. Complete Ads env credential tuple.
5. Default Ads keychain/config profile.

If `ASC_ADS_STRICT_AUTH` is true, fail when Ads credentials are split across
multiple sources, matching the existing strict-auth principle.

If a profile is selected by `--ads-profile` or `ASC_ADS_PROFILE`, ignore
`ASC_ADS_ACCESS_TOKEN` unless `ASC_ADS_STRICT_AUTH` is true; in strict mode,
selected profile plus access token is a mixed-source error.

Org ID resolution is independent from token resolution:

1. `--org`
2. `ASC_ADS_ORG_ID`
3. selected Ads profile `org_id`
4. `ads.org_id` in config only when authentication is profile-less

Persist the org ID both on the selected credential and in `ads.org_id` when Ads
login receives an org ID. This lets
`ASC_ADS_ACCESS_TOKEN` users reuse a configured default org without storing Ads
private key material in the active environment. A named profile never inherits
either root Ads context when its own profile omits the value. Switching or
removing the default clears both root context values. Profile-less access-token
or environment authentication can still use the standalone root values.

Platform API v1 ad-account resolution is independent from org resolution:

1. `--ad-account`
2. `ASC_ADS_AD_ACCOUNT_ID`
3. selected Ads profile `ad_account_id`
4. `ads.ad_account_id` in config when authentication does not select a named profile

Persist an ad-account ID supplied to Ads login alongside the selected profile
and root config. A named profile never inherits the root ad-account ID from
another profile.

## Campaign Management API v5 HTTP Client Contract

This section applies only to the deprecated Campaign Management API v5
commands under `asc ads v5`. Platform API v1 has a separate transport contract
below.

Base URL:

```text
https://api.searchads.apple.com/api/
```

Headers:

```text
Authorization: Bearer <access_token>
Accept: application/json
Content-Type: application/json
X-AP-Context: orgId=<org-id>
```

Do not send `X-AP-Context` for these two endpoints:

- `GET /v5/acls`
- `GET /v5/me`

For all other endpoints, resolve org ID from `--org`, `ASC_ADS_ORG_ID`, the
selected Ads profile, or `ads.org_id` in config. If it is still empty, fail
with:

```text
Error: --org is required (or set ASC_ADS_ORG_ID or an Ads profile org_id)
```

Use `shared.ContextWithTimeout` for all Apple Ads API requests so `ASC_TIMEOUT`
continues to apply.

Use the existing retry/backoff approach for GET and HEAD requests and for 429
responses. Redact `Authorization`, `client_secret`, private keys, and access
tokens in `ASC_DEBUG=api` logging.

Apple Ads error envelope:

```json
{
  "error": {
    "errors": [
      {
        "field": "name",
        "message": "message",
        "messageCode": "CODE"
      }
    ]
  }
}
```

Add `appleads.APIError` with HTTP status, field, message, and messageCode. If
the response is not this envelope, sanitize and return the raw detail the same
way `internal/asc` does.

## Platform API v1 HTTP Client Contract

Base URL:

```text
https://api.ads.apple.com/v1/
```

Platform API v1 uses the same bearer authorization and JSON content headers as
v5. Account-scoped requests add:

```text
X-AP-Context: adAccountId=<ad-account-id>;
```

The ad-account ID is resolved independently from the legacy organization ID.
`--ad-account`, `ASC_ADS_AD_ACCOUNT_ID`, the selected profile's
`ad_account_id`, and root `ads.ad_account_id` are the supported sources. The
CLI rejects control characters and semicolons before authentication or network
work so an ID cannot inject additional context fields.

The following requests are context-free: `GET /v1/me`, `GET /v1/acls`,
`GET /v1/orgs/{id}`, `GET /v1/advertiser-resources`, and
`POST /v1/ad-accounts`. For `GET` and `PUT /v1/ad-accounts/{id}`, the context
account must match the path ID. Other
endpoint context requirements are declared by the v1 endpoint metadata.

Platform API v1 retains the shared timeout, retry, rate-limit, pagination, and
sanitized error handling behavior of the client, while selecting the v1 base
URL and endpoint-specific response envelope.

## Payload Contract

Every Apple Ads endpoint with an HTTP body uses `--file`.

Reason: Apple Ads request objects are large, nested, API-specific JSON payloads.
Using `--file` is already established in this repo for complex JSON payloads
such as Xcode Cloud workflows. It also preserves full API support without
mapping every Apple Ads nested object into unstable CLI flags.

Rules:

- The CLI sends the file content as the HTTP body without wrapping it.
- Object endpoints require a JSON object file.
- Array endpoints require a JSON array file.
- `(CustomProductPageCreative | DefaultProductPageCreative)` accepts a JSON
  object file.
- `[int64]` delete-bulk endpoints accept a JSON array of numbers.
- Do not implement `--file -` in this PR.

Extend `internal/cli/shared/json_payload.go`:

```go
type JSONPayloadKind string

const (
	JSONPayloadObject JSONPayloadKind = "object"
	JSONPayloadArray  JSONPayloadKind = "array"
	JSONPayloadAny    JSONPayloadKind = "any"
)

func ReadJSONFilePayloadKind(path string, kind JSONPayloadKind) (json.RawMessage, error)
```

Keep `ReadJSONFilePayload(path)` as the object-only compatibility wrapper.

Required errors:

```text
payload path must be a file
payload file is empty
invalid JSON: ...
payload must be a JSON object
payload must be a JSON array
```

## Pagination Contract

Apple Ads uses offset pagination, not App Store Connect `links.next`.

List/search commands with `limit`/`offset` support:

```text
--limit INT
--offset INT
--paginate
```

Do not add `--next` to Apple Ads commands. Validate:

- `--limit` must be `1..1000` except `GET v5/custom-reports`, where it must be
  `1..50`.
- `--offset` must be `>=0`.
- `--paginate` starts at `--offset` when provided.
- `--paginate` uses page size `--limit` when provided, otherwise `1000`
  except `GET v5/custom-reports`, where the default page size is `50`.
- Continue until `pagination.startIndex + pagination.itemsPerPage >= pagination.totalResults` or a page returns no data.

Do not add `--paginate` to `find` or report endpoints that carry pagination
inside a `Selector` or `ReportingRequest` JSON body. Those endpoints keep
pagination fully manual in the payload file. The CLI must not mutate payload
files or rewrite body JSON to advance selector pagination in this PR.

The generic paginated response shape must preserve the Apple envelope:

```json
{
  "data": [],
  "pagination": {
    "itemsPerPage": 1000,
    "startIndex": 0,
    "totalResults": 0
  }
}
```

## Output Contract

All endpoint commands bind:

```go
output := shared.BindOutputFlags(fs)
```

Return Apple Ads response JSON exactly as Apple returns it, including `data`,
`pagination`, and `error` envelopes. JSON is the canonical agent output.

Use the existing output-registry fallback for Apple Ads raw response types:
`table` and `markdown` print the same JSON envelope as `json` until a dedicated
renderer is added in a later PR. Do not add custom Apple Ads table/markdown
renderers in this PR.

Represent successful Apple Ads responses as `json.RawMessage` or a dedicated
raw envelope type that is not registered with the output registry.

## Historical v5 endpoint-to-command matrix

This matrix records the 100% v5 coverage implemented from Apple's May 2026
documentation. Every row remains a runnable deprecated compatibility command;
it is not the preferred Platform v1 command inventory.

| CLI command | HTTP endpoint | Body | Notes |
| --- | --- | --- | --- |
| `asc ads v5 acls list` | `GET v5/acls` | none | No `--org` header. |
| `asc ads v5 me view` | `GET v5/me` | none | No `--org` header. |
| `asc ads v5 apps search --query QUERY [--limit N --offset N --paginate --return-owned-apps]` | `GET v5/search/apps` | none | `query` is required. |
| `asc ads v5 apps view --adam-id ADAM_ID` | `GET v5/apps/{adamId}` | none | `adamId` is int64. |
| `asc ads v5 apps localized-details --adam-id ADAM_ID` | `GET v5/apps/{adamId}/locale-details` | none |  |
| `asc ads v5 apps eligibility find --adam-id ADAM_ID --file selector.json` | `POST v5/apps/{adamId}/eligibilities/find` | `Selector` object |  |
| `asc ads v5 apps assets find --adam-id ADAM_ID --file selector.json` | `POST v5/apps/{adamId}/assets/find` | `Selector` object |  |
| `asc ads v5 product-pages list --adam-id ADAM_ID [--name NAME --states STATES]` | `GET v5/apps/{adamId}/product-pages` | none | Forward `states` as raw query string. |
| `asc ads v5 product-pages view --adam-id ADAM_ID --product-page PRODUCT_PAGE_ID` | `GET v5/apps/{adamId}/product-pages/{productPageId}` | none |  |
| `asc ads v5 product-pages locales list --adam-id ADAM_ID --product-page PRODUCT_PAGE_ID [--device-classes VALUE --language-codes VALUE --languages VALUE --expand]` | `GET v5/apps/{adamId}/product-pages/{productPageId}/locale-details` | none | Forward query strings exactly. |
| `asc ads v5 product-pages countries list [--countries-or-regions VALUE]` | `GET v5/countries-or-regions` | none |  |
| `asc ads v5 product-pages devices list` | `GET v5/creativeappmappings/devices` | none |  |
| `asc ads v5 budget-orders list [--limit N --offset N --paginate]` | `GET v5/budgetorders` | none |  |
| `asc ads v5 budget-orders create --file budget-order-create.json --confirm` | `POST v5/budgetorders` | `BudgetOrderCreate` object | Require `--confirm`. |
| `asc ads v5 budget-orders view --budget-order BUDGET_ORDER_ID` | `GET v5/budgetorders/{boId}` | none |  |
| `asc ads v5 budget-orders update --budget-order BUDGET_ORDER_ID --file budget-order-update.json --confirm` | `PUT v5/budgetorders/{boId}` | `BudgetOrderUpdate` object | Require `--confirm`. |
| `asc ads v5 campaigns list [--limit N --offset N --paginate]` | `GET v5/campaigns` | none | `asc ads v5 campaigns` aliases list. |
| `asc ads v5 campaigns find --file selector.json` | `POST v5/campaigns/find` | `Selector` object |  |
| `asc ads v5 campaigns view --campaign CAMPAIGN_ID` | `GET v5/campaigns/{campaignId}` | none |  |
| `asc ads v5 campaigns create --file campaign.json --confirm` | `POST v5/campaigns` | `Campaign` object | Require `--confirm`. |
| `asc ads v5 campaigns update --campaign CAMPAIGN_ID --file campaign-update.json --confirm` | `PUT v5/campaigns/{campaignId}` | `UpdateCampaignRequest` object | Require `--confirm`; campaign update uses Apple's campaign envelope. |
| `asc ads v5 campaigns delete --campaign CAMPAIGN_ID --confirm` | `DELETE v5/campaigns/{campaignId}` | none | Require `--confirm`. |
| `asc ads v5 ad-groups list --campaign CAMPAIGN_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/adgroups` | none | `asc ads v5 ad-groups` aliases list. |
| `asc ads v5 ad-groups find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/adgroups/find` | `Selector` object |  |
| `asc ads v5 ad-groups find-org --file selector.json` | `POST v5/adgroups/find` | `Selector` object | Org-level find. |
| `asc ads v5 ad-groups view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}` | none |  |
| `asc ads v5 ad-groups create --campaign CAMPAIGN_ID --file ad-group.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups` | `AdGroup` object | Require `--confirm`. |
| `asc ads v5 ad-groups update --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file ad-group-update.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}` | `AdGroupUpdate` object | Require `--confirm`. |
| `asc ads v5 ad-groups delete --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --confirm` | `DELETE v5/campaigns/{campaignId}/adgroups/{adgroupId}` | none | Require `--confirm`. |
| `asc ads v5 ads list --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads` | none |  |
| `asc ads v5 ads find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/ads/find` | `Selector` object | Campaign-level find. |
| `asc ads v5 ads find-org --file selector.json` | `POST v5/ads/find` | `Selector` object | Org-level find. |
| `asc ads v5 ads view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --ad AD_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads/{adId}` | none |  |
| `asc ads v5 ads create --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file ad-create.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads` | `AdCreate` object | Require `--confirm`. |
| `asc ads v5 ads update --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --ad AD_ID --file ad-update.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads/{adId}` | `AdUpdate` object | Require `--confirm`. |
| `asc ads v5 ads delete --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --ad AD_ID --confirm` | `DELETE v5/campaigns/{campaignId}/adgroups/{adgroupId}/ads/{adId}` | none | Require `--confirm`. |
| `asc ads v5 targeting-keywords list --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords` | none |  |
| `asc ads v5 targeting-keywords find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/adgroups/targetingkeywords/find` | `Selector` object | Campaign-level find across ad groups. |
| `asc ads v5 targeting-keywords view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --keyword KEYWORD_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/{keywordId}` | none |  |
| `asc ads v5 targeting-keywords create-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keywords.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/bulk` | `[Keyword]` array | Require `--confirm`. |
| `asc ads v5 targeting-keywords update-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keywords-update.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/bulk` | `[KeywordUpdateRequest]` array | Require `--confirm`. |
| `asc ads v5 targeting-keywords delete --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --keyword KEYWORD_ID --confirm` | `DELETE v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/{keywordId}` | none | Require `--confirm`. |
| `asc ads v5 targeting-keywords delete-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keyword-ids.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords/delete/bulk` | `[int64]` array | Require `--confirm`. |
| `asc ads v5 campaign-negative-keywords list --campaign CAMPAIGN_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/negativekeywords` | none |  |
| `asc ads v5 campaign-negative-keywords find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/negativekeywords/find` | `Selector` object |  |
| `asc ads v5 campaign-negative-keywords view --campaign CAMPAIGN_ID --negative-keyword KEYWORD_ID` | `GET v5/campaigns/{campaignId}/negativekeywords/{keywordId}` | none |  |
| `asc ads v5 campaign-negative-keywords create-bulk --campaign CAMPAIGN_ID --file negative-keywords.json --confirm` | `POST v5/campaigns/{campaignId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. |
| `asc ads v5 campaign-negative-keywords update-bulk --campaign CAMPAIGN_ID --file negative-keywords.json --confirm` | `PUT v5/campaigns/{campaignId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. |
| `asc ads v5 campaign-negative-keywords delete-bulk --campaign CAMPAIGN_ID --file keyword-ids.json --confirm` | `POST v5/campaigns/{campaignId}/negativekeywords/delete/bulk` | `[int64]` array | Require `--confirm`. |
| `asc ads v5 ad-group-negative-keywords list --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID [--limit N --offset N --paginate]` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords` | none |  |
| `asc ads v5 ad-group-negative-keywords find --campaign CAMPAIGN_ID --file selector.json` | `POST v5/campaigns/{campaignId}/adgroups/negativekeywords/find` | `Selector` object | Campaign-level find across ad groups. |
| `asc ads v5 ad-group-negative-keywords view --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --negative-keyword KEYWORD_ID` | `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/{keywordId}` | none |  |
| `asc ads v5 ad-group-negative-keywords create-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file negative-keywords.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. |
| `asc ads v5 ad-group-negative-keywords update-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file negative-keywords.json --confirm` | `PUT v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/bulk` | `[NegativeKeyword]` array | Require `--confirm`. |
| `asc ads v5 ad-group-negative-keywords delete-bulk --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file keyword-ids.json --confirm` | `POST v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords/delete/bulk` | `[int64]` array | Require `--confirm`. |
| `asc ads v5 geo search [--country-code CC --entity ENTITY --query QUERY --limit N --offset N --paginate]` | `GET v5/search/geo` | none | API default query is `*:*`. |
| `asc ads v5 geo resolve --file geo-requests.json [--limit N --offset N --paginate]` | `POST v5/search/geo` | `[GeoRequest]` array |  |
| `asc ads v5 creatives list [--limit N --offset N --paginate]` | `GET v5/creatives` | none |  |
| `asc ads v5 creatives find --file selector.json` | `POST v5/creatives/find` | `Selector` object |  |
| `asc ads v5 creatives view --creative CREATIVE_ID [--include-deleted-creative-set-assets]` | `GET v5/creatives/{creativeId}` | none | Include deprecated creative set assets query. |
| `asc ads v5 creatives create --file creative.json` | `POST v5/creatives` | `CustomProductPageCreative` or `DefaultProductPageCreative` object |  |
| `asc ads v5 rejection-reasons find --file selector.json` | `POST v5/product-page-reasons/find` | `Selector` object |  |
| `asc ads v5 rejection-reasons view --reason PRODUCT_PAGE_REASON_ID` | `GET v5/product-page-reasons/{productPageReasonId}` | none |  |
| `asc ads v5 reports campaigns --file reporting-request.json` | `POST v5/reports/campaigns` | `ReportingRequest` object |  |
| `asc ads v5 reports ad-groups --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/adgroups` | `ReportingRequest` object |  |
| `asc ads v5 reports keywords --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/keywords` | `ReportingRequest` object |  |
| `asc ads v5 reports search-terms --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/searchterms` | `ReportingRequest` object |  |
| `asc ads v5 reports ads --campaign CAMPAIGN_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/ads` | `ReportingRequest` object |  |
| `asc ads v5 reports ad-group-keywords --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/adgroups/{adgroupId}/keywords` | `ReportingRequest` object |  |
| `asc ads v5 reports ad-group-search-terms --campaign CAMPAIGN_ID --ad-group AD_GROUP_ID --file reporting-request.json` | `POST v5/reports/campaigns/{campaignId}/adgroups/{adgroupId}/searchterms` | `ReportingRequest` object |  |
| `asc ads v5 impression-share-reports list [--field FIELD --sort-order ORDER --limit N --offset N --paginate]` | `GET v5/custom-reports` | none |  |
| `asc ads v5 impression-share-reports create --file custom-report-request.json` | `POST v5/custom-reports` | `CustomReportRequest` object |  |
| `asc ads v5 impression-share-reports view --report REPORT_ID` | `GET v5/custom-reports/{reportId}` | none |  |

The `EndpointSpec` query parameter metadata matched Apple's v5 documentation at
the research date for every row. Required query parameters were:

| HTTP endpoint | Query flags |
| --- | --- |
| `GET v5/search/apps` | `--query` required, `--limit`, `--offset`, `--return-owned-apps` |
| `GET v5/search/geo` | `--country-code`, `--entity`, `--query`, `--limit`, `--offset` |
| `POST v5/search/geo` | `--limit`, `--offset` |
| `GET v5/campaigns` | `--limit`, `--offset` |
| `GET v5/budgetorders` | `--limit`, `--offset` |
| `GET v5/campaigns/{campaignId}/adgroups` | `--limit`, `--offset` |
| `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/targetingkeywords` | `--limit`, `--offset` |
| `GET v5/campaigns/{campaignId}/adgroups/{adgroupId}/negativekeywords` | `--limit`, `--offset` |
| `GET v5/campaigns/{campaignId}/negativekeywords` | `--limit`, `--offset` |
| `GET v5/creatives` | `--limit`, `--offset` |
| `GET v5/custom-reports` | `--field`, `--sort-order`, `--limit`, `--offset` |
| `GET v5/apps/{adamId}/product-pages` | `--name`, `--states` |
| `GET v5/apps/{adamId}/product-pages/{productPageId}/locale-details` | `--device-classes`, `--expand`, `--language-codes`, `--languages` |
| `GET v5/countries-or-regions` | `--countries-or-regions` |
| `GET v5/creatives/{creativeId}` | `--include-deleted-creative-set-assets` |

Apple's general partial-fetch `fields` query parameter was not exposed in the
original v5 implementation because those endpoint pages did not list
endpoint-specific `fields[...]` query parameters. Add it only if the
compatibility implementation extracts a documented endpoint-specific query
parameter from Apple docs.

Add this debug/forward-compatibility command after the named endpoints:

```text
asc ads v5 api request --method METHOD --path v5/... [--file payload.json] [--org ORG_ID] [--confirm]
```

Rules:

- `--path` accepts only relative `v5/...` paths or full URLs under
  `https://api.searchads.apple.com/api/`.
- Full URLs for other hosts are rejected.
- `DELETE` requires `--confirm`.
- This command is not a substitute for any named endpoint above; all rows above
  must still exist.

## Request Body Reference

The following body types are accepted through `--file` and must be validated
only for JSON shape, not for every semantic field. Semantic validation belongs
to Apple Ads.

| Body type | JSON shape | Required top-level fields from Apple docs |
| --- | --- | --- |
| `Selector` | object | none; common fields are `conditions`, `fields`, `orderBy`, `pagination` |
| `Condition` | object | none; common fields are `field`, `operator`, `values`, `ignoreCase` |
| `BudgetOrderCreate` | object | none documented as top-level required |
| `BudgetOrderUpdate` | object | none documented as top-level required |
| `Campaign` | object | `adamId`, `adChannelType`, `billingEvent`, `countriesOrRegions`, `dailyBudgetAmount`, `name`, `supplySources` |
| `UpdateCampaignRequest` | object | none documented as top-level required; payload uses `campaign` envelope |
| `AdGroup` | object | `defaultBidAmount`, `name`, `pricingModel`, `startTime` |
| `AdGroupUpdate` | object | none documented as top-level required |
| `Keyword` | object array item | `bidAmount`, `matchType`, `text` |
| `KeywordUpdateRequest` | object array item | `matchType`, `text` |
| `NegativeKeyword` | object array item | `matchType`, `text` |
| `AdCreate` | object | `creativeId`, `name`, `status` |
| `AdUpdate` | object | none documented as top-level required |
| `CustomProductPageCreative` | object | `productPageId`, `adamId`, `name`, `type` |
| `DefaultProductPageCreative` | object | `adamId`, `name`, `type` |
| `ReportingRequest` | object | `startTime`, `endTime`, `selector` |
| `CustomReportRequest` | object | `name` |
| `GeoRequest` | object array item | `entity`, `id` |
| `[int64]` | array | integer IDs |

Payload examples:

```json
{
  "conditions": [
    {
      "field": "campaignId",
      "operator": "EQUALS",
      "values": ["1234567890"]
    }
  ],
  "pagination": {
    "limit": 1000,
    "offset": 0
  }
}
```

```json
{
  "adamId": 123456789,
  "adChannelType": "SEARCH",
  "billingEvent": "TAPS",
  "countriesOrRegions": ["US"],
  "dailyBudgetAmount": {
    "amount": "25",
    "currency": "USD"
  },
  "name": "US Search Campaign",
  "status": "ENABLED",
  "supplySources": ["APPSTORE_SEARCH_RESULTS"]
}
```

```json
[
  {
    "text": "example keyword",
    "matchType": "BROAD",
    "bidAmount": {
      "amount": "1.25",
      "currency": "USD"
    },
    "status": "ACTIVE"
  }
]
```

```json
{
  "startTime": "2026-05-01",
  "endTime": "2026-05-31",
  "granularity": "DAILY",
  "selector": {
    "pagination": {
      "limit": 1000,
      "offset": 0
    }
  },
  "timeZone": "UTC"
}
```

## Implementation Sequence

The PR implemented the work in this order:

1. Added `internal/appleads` auth, token, client, error, and pagination tests.
2. Extended `shared.ReadJSONFilePayload` with array/object/any shape support
   while preserving the existing object-only wrapper.
3. Added `internal/cli/ads` root, auth commands, and registry/root help wiring.
4. Added read-only endpoints first: `me`, `acls`, app search/details, product
   pages, countries, devices, list/view commands.
5. Added request-body commands with `--file` and shape validation.
6. Added deletes and bulk deletes with `--confirm`.
7. Added reports and impression-share reports.
8. Added `ads api request`.
9. Added command docs and generated command docs.

Do not add ergonomic create/update flags in the first PR. They would duplicate
large Apple request schemas and weaken the guarantee that every API field can be
used by agents immediately. Ergonomic wrappers can be a later additive PR.

## Test Plan

Use TDD for implementation. Start with failing tests.

Required unit tests:

- Apple Ads client secret JWT includes `alg=ES256`, `kid`, `iss`, `iat`, `exp`,
  `aud=https://appleid.apple.com`, and `sub=<client-id>`.
- Token request uses `POST https://appleid.apple.com/auth/oauth2/token` with
  `grant_type=client_credentials`, `client_id`, `client_secret`, and
  `scope=searchadsorg`.
- Access token cache refreshes before expiry.
- `ASC_ADS_ACCESS_TOKEN` bypasses token exchange.
- `X-AP-Context` is present for org-scoped endpoints and absent for `me`/`acls`.
- Missing org fails before auth resolution.
- Apple Ads error envelopes parse into `appleads.APIError`.
- Offset pagination aggregates `data` and stops at `totalResults`.
- JSON payload helper accepts object, array, and any modes; rejects wrong shapes.

Required CLI tests under `internal/cli/cmdtest`:

- Every command in the matrix is registered and has help output.
- Every leaf command validates missing required flags with exit code `2` and a
  concrete stderr message.
- Every command with `--file` rejects missing file, empty file, invalid JSON,
  and wrong object/array shape.
- Every delete and bulk delete rejects missing `--confirm` with exit code `2`.
- Every list/search command rejects invalid `--limit`, invalid `--offset`, and
  invalid boolean flags.
- Query parameters are encoded exactly as documented.
- Body payloads are forwarded byte-for-byte except for JSON validation.
- `--output json` returns parseable JSON for representative success responses.
- `--pretty` is accepted only for JSON.
- `--paginate` makes multiple requests with increasing `offset`.
- `ads api request` rejects non-Apple Ads hosts and requires `--confirm` for
  `DELETE`.

Required client tests:

- One table-driven test row for every `EndpointSpec` row that asserts method,
  path, path variables, query variables, body shape, and response passthrough.
- One generated inventory test that compares each `EndpointSpec` path, method,
  body kind, and query parameter names against the Apple docs snapshot used in
  the test fixture.
- 401, 403, 404, 429, and 500 response handling.
- Token endpoint errors do not print secrets.

Black-box verification:

```bash
go build -o /tmp/asc .
ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads --help
ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads v5 campaigns list --org 123 --output json
ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads v5 campaigns delete --campaign 1 --org 123
ASC_BYPASS_KEYCHAIN=1 /tmp/asc ads v5 campaigns delete --campaign 1 --org 123 --confirm
```

Repository checks before PR:

```bash
make format
make check-command-docs
make lint
ASC_BYPASS_KEYCHAIN=1 make test
```

Live Apple Ads smoke tests are not required for PR completion when Ads
credentials are unavailable. When Ads credentials are configured locally, run
only these read-only smoke tests:

```bash
ASC_BYPASS_KEYCHAIN=1 asc ads v5 me view --output json
ASC_BYPASS_KEYCHAIN=1 asc ads v5 acls list --output json
ASC_BYPASS_KEYCHAIN=1 asc ads v5 campaigns list --org "$ASC_ADS_ORG_ID" --limit 1 --output json
```

Do not create spend-bearing Apple Ads campaigns in live smoke tests unless the
user explicitly approves the exact org and payload.

## Documentation Updates

Updated:

- `commands/ads.mdx`
- `authentication.mdx` with Apple Ads auth subsection
- `configuration/environment-variables.mdx` with `ASC_ADS_*` variables
- `docs.json` navigation for the new command page
- `docs/COMMANDS.md` via `make generate-command-docs`
- `README.md` feature list

Docs teach `--file` payloads with copy-pasteable JSON examples and state that
Apple Ads credentials are separate from App Store Connect API keys.

### Docs placement

`commands/ads.mdx` is the user guide. Keep it focused on auth, org context,
payload files, pagination rules, endpoint groups, and raw requests.

`configuration/environment-variables.mdx` owns the full `ASC_ADS_*` reference
and credential precedence.

This architecture note owns endpoint coverage, auth internals, generated
command strategy, test requirements, and live-smoke limits.

## Definition of Done

- All 73 current v5 endpoints in the matrix have named commands.
- `asc ads v5 api request` exists for debugging and newly added Apple fields.
- Apple Ads OAuth login/status/token/doctor/logout are implemented.
- All org-scoped commands require/respect `--org`.
- All body endpoints use `--file` and support the correct JSON shape.
- Deletes and bulk deletes require `--confirm`.
- Offset pagination works with `--paginate`.
- JSON output preserves Apple response envelopes.
- Generated command docs are updated.
- `make format`, `make check-command-docs`, `make lint`, and
  `ASC_BYPASS_KEYCHAIN=1 make test` pass.

---

--- METRICS ---
- Files Extracted: 50
- Estimated Token Budget: ~130708 tokens
- Canonical Reference: https://codewiki.google/github.com/rorkai/App-Store-Connect-CLI
