Claude Skill

app-store-review-rejections

Use when preparing an App Store submission, reading an App Review rejection or Resolution Center message citing a guideline number (2.1, 2.3.x, 3.1.1, 4.3, 5.1.1, 5.1.2), or a privacy-manifest upload email (ITMS-91061 / ITMS-91056, missing required-reason API), or auditing an ads

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wei18-apple-dev-skills-apple-dev-skills_skills_app-store-review-rejections-7ea7e61.zip · 14 KB
Part of wei18/apple-dev-skills — 37 skills

Install

skills CLI npx skills add https://github.com/wei18/apple-dev-skills/tree/main/apple-dev-skills/skills/app-store-review-rejections
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wei18-apple-dev-skills@llmmart
Git git clone https://github.com/wei18/apple-dev-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wei18/apple-dev-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

App Store Review Rejections

Overview

App Review rejects on a small set of recurring guideline violations. Per Apple's own transparency reporting, Guideline 2 Performance (which includes 2.1) is the #1 cause, and Legal (Guideline 5, which includes 5.1.x) is the leading policy cause — Apple's Transparency Report only breaks rejections down to the top-level guideline, not to 2.1 or 5.1.x specifically. This skill maps the rejection classes that a free game/app with banner ads + a Remove-Ads IAP + CloudKit + Game Center realistically hits, to a concrete pre-submission fix. These examples assume a free app with banner ads + a Remove-Ads IAP + CloudKit + Game Center; the guideline classes apply broadly — skip sections that don't fit your app's feature set.

It is the content companion to asc-api-automation (which drives who/how each ASC call gets made); this says why a build gets bounced and how to pre-empt it.

When to use

  • Before any TestFlight→App Store submission (asc-api-automation got the build up; this gates whether it passes review).
  • A Resolution Center message arrived citing "Guideline X.Y" — find the row, apply the fix.
  • Auditing a new app/game for submission-readiness.
  • NOT for the mechanics of submitting or distributing (that's asc-api-automation).

Quick reference — rejection class → fix (weighted to these apps)

Guideline Why it bounces a puzzle-game-with-ads Pre-submit fix here
2.1 App Completeness Crash on reviewer's device/OS, dead-end flow, placeholder content, ad fails to load → blank space, test/placeholder ad creative shown in the submitted build Run the build on a clean device + the oldest supported OS. Ensure ads degrade gracefully (no empty frame on no-fill). The build App Review sees must read the production ad unit ID (monetization-sdk-integration's #if DEBUG/Release split already does this) — a test creative reads as placeholder content under 2.1. TestFlight archives are Release builds too, so the same prod ID ships there; the risk is the opposite direction — clicking too many production ads without being in Google's test mode risks the AdMob account being flagged for invalid traffic (Google's own guidance). Fix it one of two ways: register each tester's device as a Google test device (AdMob console → Test devices) so it gets test ads on the prod ID, or — when registering every external tester isn't feasible — detect TestFlight at runtime via StoreKit 2 — if case .verified(let tx) = try await AppTransaction.shared, tx.environment == .sandbox { /* TestFlight or sandbox */ } (AppTransaction.shared / AppStore.Environment, iOS 16+; TestFlight builds always run in the sandbox environment) — and switch to the test ad unit for that session only. Do not use the receipt path (Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"): appStoreReceiptURL is deprecated since iOS 18 / macOS 15, below this catalog's iOS 26 floor. Verify ads actually render, not a blank no-fill frame. No <TRANSLATE> / lorem strings.
2.3.1 Hidden features Shipping dormant code / hidden toggles reviewers can reach No DEBUG-only surfaces (debug menus) reachable in Release.
2.3.3 Screenshots Screenshots don't match the actual app, wrong dimensions, contain device frames Apple disallows, alpha channel; the marketing copy or version number rendered inside the screenshot is itself metadata Do not upload snapshot-test baselines directly — they have an alpha channel + wrong dims. Use real device/sim captures at exact spec sizes, regenerated fresh per release. Also check the text baked into the image: a claim like "Zero tracking" or "No third-party SDKs" is a metadata claim App Review can compare against PrivacyInfo.xcprivacy (e.g. NSPrivacyTracking: true contradicts it) — false claims are rejectable independent of the app's actual behavior; a version number rendered in the screenshot can also simply go stale.
2.3.10 Irrelevant metadata Mentioning Android / "also on Google Play", other platform names in description/keywords Strip platform references from every shipped locale's metadata.
3.1.1 In-App Purchase Remove-Ads unlock sold outside IAP; no "Restore Purchases" control; price/benefit unclear Remove-Ads must be a StoreKit IAP with a visible Restore Purchases control (implementation → storekit2-iap-defaults). Apple's wording is "you should have a restore mechanism," but it is enforced in practice as mandatory — non-consumable must restore on reinstall.
4.3(a)/(b) Spam / saturation Highest latent risk — saturated genres (puzzle, utility) face 4.3(b) "indistinguishable from what's already available" risk. (Apple's 4.3(b) text names two groups: saturated kinds — dating, flashlight, sound effects, wallpaper, simple timers, fortune telling — accepted only with a meaningfully different or improved experience; and low-effort kinds — drinking games, Kama Sutra, fart, burp — where repeated submissions may lead to removal from the Apple Developer Program. Puzzle games are in neither; the risk is inferred from saturation, not an explicit callout.) A thin clone gets bounced as spam. Lead with genuine differentiation (design system, content hub, Game Center, cross-platform). Distinct app name/icon/screenshots per app; never ship two near-identical binaries under different names (4.3(a) = same app under multiple Bundle IDs).
4.2 Minimum Functionality Too simple, feels like a web wrapper or template. (A rejection citing "Guideline 4.0 - Design" points at the section-4 preamble — the Guidelines have no numbered 4.0 rule — not at this row.) Native features (haptics, Game Center, iCloud resume, widgets if any) demonstrate platform depth.
5.1.1(v) Account deletion / data App "supports account creation" but offers no in-app deletion path Apple's text triggers the deletion requirement only for apps that "support account creation". If the app has no app-specific account-creation flow (identity via iCloud / Game Center), 5.1.1(v) likely doesn't apply — confirm for your app. But Apple publishes NO explicit iCloud/Game-Center exemption, so don't assert one as fact. Still: provide a way to clear the user's CloudKit data + a reachable privacy-policy URL. If rejected on 5.1.1(v), reply to Review that the app creates no app-specific account and data-clearing is available, rather than claiming a blanket exemption.
5.1.1 Privacy policy Missing/unreachable privacy policy URL in ASC + in-app Privacy policy URL set in App Privacy + reachable; covers ads (AdMob) + analytics data.
5.1.2 Data Use — ATT The AdMob trap. App accesses ASIdentifierManager.advertisingIdentifier (IDFA) or presents the ATT prompt via UMP but lacks NSUserTrackingUsageDescription, or never shows the prompt at all ATT is required when the app accesses IDFA (ASIdentifierManager.advertisingIdentifier) or presents the ATT prompt via UMP. AdMob can serve limited/non-personalized ads without accessing IDFA — in that case ATT is not required (though UMP/GDPR consent may still apply). When ATT is required: NSUserTrackingUsageDescription present in every shipped locale, ATT prompt shown via UMP before personalized ads. Ensure each new app in a multi-app repo has its own pre-prompt explainer shown before the ATT system prompt — it is a submission blocker if missing. If you do NOT access IDFA, declare so and don't link to it. Note: reviewers sometimes cite 2.1 (Information Needed) instead of 5.1.2 when they simply can't find where the prompt fires — the sim-verify checklist item below covers both.
Privacy-label parity App Privacy "nutrition" answers in ASC contradict PrivacyInfo.xcprivacy / actual SDK behavior (AdMob collects identifiers + usage data) ASC App Privacy answers must match the committed PrivacyInfo.xcprivacy and AdMob's declared collection. Keep them in sync (apple-three-piece-analytics). For the manifest keys, the required-reason API reason codes, the SDK-manifest rules, the upload-time ITMS- emails and the purpose-string gate, read references/privacy-manifest.md.
Age rating / 2.3.6 Ads can serve mature content but rating says 4+ (1.3 is the Kids Category, not age-rating honesty) Set AdMob max ad content rating appropriately; age rating must cover ad content.
Export compliance (ASC upload step, not a Guideline number) Build marked Missing Compliance until its encryption use is specified — the Guidelines text has no "export" section; 2.5 is Software Requirements, unrelated to this Default: set ITSAppUsesNonExemptEncryption = NO in Info.plist when the app uses no encryption or only exempt encryption, so ASC never asks. Remedy for an already-uploaded build: answer the questions in ASC, or PATCH /v1/builds/{id} with usesNonExemptEncryption (asc-api-automation covers what the API can drive). A human step is required only when non-exempt encryption needs documentation uploaded.

Pre-submission checklist (run before flipping a build to "Submit for Review")

  1. Clean-device + oldest-OS smoke run; no crash, no blank ad frame, no debug surface.
  2. L10n gate green for every shipped locale — 0 <TRANSLATE> (default set → ai-translated-localization; rejection-grade for 2.1/2.3).
  3. ATT: NSUserTrackingUsageDescription localized for every shipped locale; sim-verify the ATT prompt actually fires on a fresh install (drive it with interactive-simulator-ux-audit's idb / simctl tap + screenshot flow — reviewers reject on the runtime prompt being absent, not just the Info.plist key); pre-prompt explainer present in every shipped app.
  4. Restore-Purchases button visible; Remove-Ads restores on reinstall.
  5. ASC App Privacy answers == PrivacyInfo.xcprivacy == AdMob's declared data use (manifest pre-submit checklist in references/privacy-manifest.md).
  6. Screenshots are real captures at spec dimensions (not snapshot baselines).
  7. Privacy-policy URL set and reachable; no other-platform mentions in any locale's metadata.
  8. Per-app distinct name/icon/screenshots; differentiation visible in the first screenshot (4.3).
  9. Age rating questionnaire answered for this version, including the social media capability questions required for submissions since September 2026.

Common mistakes

  • Trusting "it compiled / TF accepted it." Upload success ≠ review pass; the gates above are orthogonal to a green build.
  • Re-using snapshot PNGs as store screenshots — wrong dims + alpha → 2.3.3.
  • Adding the ad SDK but skipping ATT — the single most common ads-app 5.1.2 bounce.
  • Filling App Privacy by guesswork — must mirror the actual SDK + PrivacyInfo.xcprivacy, or it's a 5.1.x mismatch.
  • Shipping a sibling app as a near-clone — 4.3 spam; sharing code is fine, but each product must have a distinct identity.

Sources

Apple App Store Review Guidelines (developer.apple.com) + Apple App Store Transparency Report (performance = top rejection cause); Google AdMob iOS privacy strategies (ATT / UMP / NSUserTrackingUsageDescription). Re-verify guideline numbers against the live Guidelines before quoting them in a Resolution Center reply — Apple renumbers.

Related skills

  • asc-api-automation — how to submit via the ASC REST API once a build exists; this skill covers what content has to be true for that submission to pass review.
  • storekit2-iap-defaults — 3.1.1 implementation: the Remove-Ads IAP and its Restore Purchases control.
  • monetization-sdk-integration — 5.1.2 ATT / PrivacyInfo consequences of shipping AdMob.
  • ai-translated-localization — the L10n flow that keeps every shipped locale's metadata and ATT string complete.
  • Official sources: when verifying or updating a factual or version-sensitive claim, read references/official-docs.md.
Files (apple-dev-skills)
  • references
    • official-docs.md 1.8 KB
      Official pages backing this skill's claims; read when verifying or updating a factual or version-sensitive claim.
      
      | Page | URL | Backs |
      |---|---|---|
      | App Review Guidelines | https://developer.apple.com/app-store/review/guidelines/ | Quick reference rows (Guideline 4.0 is a section header, not a numbered item) |
      | 2024 App Store Transparency Report | https://www.apple.com/legal/more-resources/docs/2024-App-Store-Transparency-Report.pdf | Overview rejection-rate figures |
      | Complying with Encryption Export Regulations | https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations | Export compliance row: bypassing the questionnaire via Info.plist |
      | ITSAppUsesNonExemptEncryption | https://developer.apple.com/documentation/bundleresources/information-property-list/itsappusesnonexemptencryption | Export compliance row: the questionnaire triggers on every upload without this key |
      | Provide export compliance information for beta builds | https://developer.apple.com/help/app-store-connect/test-a-beta-version/provide-export-compliance-information-for-beta-builds/ | The "Missing Compliance" status name |
      | BuildUpdateRequest.Data.Attributes | https://developer.apple.com/documentation/appstoreconnectapi/buildupdaterequest/data-data.dictionary/attributes-data.dictionary | `usesNonExemptEncryption` attribute (only `expired` / `usesNonExemptEncryption` exist) |
      | Screenshot specifications | https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications | 2.3.3 row: no alpha channel/transparency, per-size pixel dimensions |
      | environment | https://developer.apple.com/documentation/storekit/apptransaction/environment | 2.1 row: `AppTransaction` environment check, iOS 16.0 / macOS 13.0 |
      | Privacy | https://developer.apple.com/design/human-interface-guidelines/privacy | 5.1.2 row: pre-permission priming screen |
      
    • privacy-manifest.md 21.8 KB
      # Privacy manifest (`PrivacyInfo.xcprivacy`)
      
      The upload-time and review-time rules around the privacy manifest, condensed from
      Apple's documentation. Every falsifiable claim carries its source in §Sources;
      reason codes are quoted from Apple's page, not paraphrased — copy them exactly.
      
      ## Contents
      
      - [What the file is and where it lives](#what-the-file-is-and-where-it-lives)
      - [The four top-level keys](#the-four-top-level-keys)
      - [Required-reason APIs and their reason codes](#required-reason-apis-and-their-reason-codes)
      - [Third-party SDKs](#third-party-sdks)
      - [Reading the Xcode privacy report](#reading-the-xcode-privacy-report)
      - [Upload-time emails](#upload-time-emails)
      - [Parity with the App Privacy label](#parity-with-the-app-privacy-label)
      - [Info.plist purpose strings](#infoplist-purpose-strings)
      - [Pre-submit checklist](#pre-submit-checklist)
      - [Sources](#sources)
      
      ## What the file is and where it lives
      
      - A property list named exactly `PrivacyInfo.xcprivacy` ("the required file name for
        bundled privacy manifests"). Create it in Xcode via File > New File > Resource >
        **App Privacy** File type, and check the target so it lands in the target's
        resources — "You need to add the privacy manifest file to your target's resources
        for Xcode to use it when you generate a privacy report." [S1]
      - Location: iOS / iPadOS / tvOS / visionOS / watchOS apps place it at the **root of
        the app bundle** (`Sample.app/PrivacyInfo.xcprivacy`); macOS and Mac Catalyst apps
        at `Contents/Resources/`. Frameworks follow the same split (root vs
        `Versions/A/Resources/`). Xcode places it correctly when the file is a target
        resource. [S2]
      - Swift packages: Xcode "doesn't recognize privacy manifest files as resources by
        default" — declare it explicitly, e.g. `resources: [.process("PrivacyInfo.xcprivacy")]`. [S2]
      - A `.a` static library "doesn't support resources such as the privacy manifest";
        ship a static *framework* target instead. [S2]
      - "All privacy manifest files in your app must be valid, containing only the expected
        keys and values." App Store Connect rejects submissions with invalid manifests. [S2]
      
      ## The four top-level keys
      
      | Key | Type | When it is required |
      |---|---|---|
      | `NSPrivacyTracking` | Boolean | "indicates whether your app or third-party SDK uses data for tracking as defined under the App Tracking Transparency framework. When set to `true` you need to provide a list of internet domains in `NSPrivacyTrackingDomains`." [S1] |
      | `NSPrivacyTrackingDomains` | Array of String | "lists the internet domains your app or third-party SDK connects to that engage in tracking. If the user has not granted tracking permission through the App Tracking Transparency framework, network requests to these domains fail and your app receives an error. To provide a list of internet domains in `NSPrivacyTrackingDomains`, set `NSPrivacyTracking` to `true`." [S1] Each entry: top-level domain or subdomain, no path/query, no trailing slash. `NSPrivacyTracking: false` with a non-empty domains array is *invalid*; `true` with an empty array is *invalid*. [S7] |
      | `NSPrivacyCollectedDataTypes` | Array of Dictionary | The data types collected — required "on all platforms". Each dictionary has `NSPrivacyCollectedDataType`, `NSPrivacyCollectedDataTypeLinked` (Bool), `NSPrivacyCollectedDataTypeTracking` (Bool), `NSPrivacyCollectedDataTypePurposes` (Array). Only Apple's listed type and purpose values are accepted: "Xcode won't generate a privacy report correctly if you define your own collected data types". [S1][S3] |
      | `NSPrivacyAccessedAPITypes` | Array of Dictionary | The required-reason API categories used — required "on iOS, iPadOS, tvOS, visionOS, and watchOS". Each dictionary has exactly `NSPrivacyAccessedAPIType` (String) and `NSPrivacyAccessedAPITypeReasons` (Array of String). [S1][S4][S8] |
      
      Omission rules (from TN3181): if the app contacts no tracking domains, either remove
      both tracking keys or set `NSPrivacyTracking` to `false` and remove
      `NSPrivacyTrackingDomains`; if `NSPrivacyAccessedAPITypes` would be empty, remove
      the key rather than ship an empty array. [S7]
      
      ## Required-reason APIs and their reason codes
      
      Why this exists: "Regardless of whether a user gives your app permission to track,
      fingerprinting is not allowed." Since May 1, 2024 "apps that don't describe their use
      of required reason API in their privacy manifest file aren't accepted by App Store
      Connect." The app declares APIs used in *its own* code; an SDK declares its own —
      "Your third-party SDK can't rely on the privacy manifest files for apps that link the
      third-party SDK". "For each executable or dynamic library in an app that uses a
      required reason API, the bundle that includes the executable or dynamic library needs
      to include a privacy manifest file that reports the API." [S4]
      
      The five `NSPrivacyAccessedAPIType` values and the reason codes each accepts. The
      reason text is the first sentence of Apple's definition; every reason also carries
      an off-device / usage restriction paragraph on the source page — read it before
      declaring. [S5][S8]
      
      ### `NSPrivacyAccessedAPICategoryFileTimestamp`
      
      APIs: `creationDate`, `modificationDate`, `fileModificationDate`,
      `contentModificationDateKey`, `creationDateKey`, `getattrlist`, `getattrlistbulk`,
      `fgetattrlist`, `stat`, `fstat`, `fstatat`, `lstat`, `getattrlistat`.
      
      | Code | Reason |
      |---|---|
      | `DDA9.1` | "Declare this reason to display file timestamps to the person using the device." |
      | `C617.1` | "Declare this reason to access the timestamps, size, or other metadata of files inside the app container, app group container, or the app's CloudKit container." |
      | `3B52.1` | "Declare this reason to access the timestamps, size, or other metadata of files or directories that the user specifically granted access to, such as using a document picker view controller." |
      | `0A2A.1` | "Declare this reason if your third-party SDK is providing a wrapper function around file timestamp API(s) for the app to use, and you only access the file timestamp APIs when the app calls your wrapper function. This reason may only be declared by third-party SDKs." |
      
      ### `NSPrivacyAccessedAPICategorySystemBootTime`
      
      APIs: `systemUptime`, `mach_absolute_time()`.
      
      | Code | Reason |
      |---|---|
      | `35F9.1` | "Declare this reason to access the system boot time in order to measure the amount of time that has elapsed between events that occurred within the app or to perform calculations to enable timers." |
      | `8FFB.1` | "Declare this reason to access the system boot time to calculate absolute timestamps for events that occurred within your app, such as events related to the UIKit or AVFAudio frameworks." |
      | `3D61.1` | "Declare this reason to include system boot time information in an optional bug report that the person using the device chooses to submit." |
      
      ### `NSPrivacyAccessedAPICategoryDiskSpace`
      
      APIs: `volumeAvailableCapacityKey`, `volumeAvailableCapacityForImportantUsageKey`,
      `volumeAvailableCapacityForOpportunisticUsageKey`, `volumeTotalCapacityKey`,
      `systemFreeSize`, `systemSize`, `statfs`, `statvfs`, `fstatfs`, `fstatvfs`,
      `getattrlist`, `fgetattrlist`, `getattrlistat`.
      
      | Code | Reason |
      |---|---|
      | `85F4.1` | "Declare this reason to display disk space information to the person using the device." |
      | `E174.1` | "Declare this reason to check whether there is sufficient disk space to write files, or to check whether the disk space is low so that the app can delete files when the disk space is low." |
      | `7D9E.1` | "Declare this reason to include disk space information in an optional bug report that the person using the device chooses to submit." |
      | `B728.1` | "Declare this reason if your app is a health research app, and you access this API category to detect and inform research participants about low disk space impacting the research data collection." |
      
      ### `NSPrivacyAccessedAPICategoryActiveKeyboards`
      
      API: `activeInputModes`.
      
      | Code | Reason |
      |---|---|
      | `3EC4.1` | "Declare this reason if your app is a custom keyboard app, and you access this API category to determine the keyboards that are active on the device." |
      | `54BD.1` | "Declare this reason to access active keyboard information to present the correct customized user interface to the person using the device." |
      
      ### `NSPrivacyAccessedAPICategoryUserDefaults`
      
      API: `UserDefaults`. This is the category almost every app hits — any
      `UserDefaults.standard` read or write needs a declaration.
      
      | Code | Reason |
      |---|---|
      | `CA92.1` | "Declare this reason to access user defaults to read and write information that is only accessible to the app itself." |
      | `1C8F.1` | "Declare this reason to access user defaults to read and write information that is only accessible to the apps, app extensions, and App Clips that are members of the same App Group as the app itself." |
      | `C56D.1` | "Declare this reason if your third-party SDK is providing a wrapper function around user defaults API(s) for the app to use, and you only access the user defaults APIs when the app calls your wrapper function. This reason may only be declared by third-party SDKs." |
      | `AC6B.1` | "Declare this reason to access user defaults to read the `com.apple.configuration.managed` key to retrieve the managed app configuration set by MDM, or to set the `com.apple.feedback.managed` key to store feedback information to be queried over MDM". |
      
      Apple "continually reviews the list of required reason APIs and reasons for usage" —
      re-check the source page when a new category or code appears in an upload email. [S4]
      
      ## Third-party SDKs
      
      - Apple's list of "SDKs that require a privacy manifest and signature" (Abseil,
        AFNetworking, Alamofire, Firebase modules, FBSDK*, GoogleUtilities, Sentry, …) lives
        at [S6]. "You must include the privacy manifest for any SDK listed below when you
        submit new apps in App Store Connect that include those SDKs, or when you submit an
        app update that adds one of the listed SDKs as part of the update." "Signatures are
        also required in these cases where the listed SDKs are used as binary dependencies.
        Any version of a listed SDK, as well as any SDKs that repackage those on the list,
        are included in the requirement." [S6]
      - Unlisted SDKs still need a manifest "if it uses a required reasons API, collects
        data about the person using apps that include the third-party SDK, enables the app
        to collect data about people using the app, or contacts tracking domains". [S1]
      - **Do not duplicate an SDK's declarations in the app manifest.** "Third-party SDKs
        need to provide their own privacy manifest files that record the types of data they
        collect. Your app's privacy manifest file doesn't need to cover data collected by
        third-party SDKs that your app links to." [S3] DTS repeats it for the ITMS-91061
        case: the app manifest must "**only** describe the privacy practices of your app.
        Do not add the privacy practices of the SDK to your app's privacy manifest." [S10]
      - Enforcement dates: required-reason declarations since May 1, 2024 [S4][S9]; a valid
        manifest for every file since November 12, 2024 [S7]; a manifest inside each listed
        SDK since February 12, 2025 [S2][S10].
      - Escape hatch when an SDK ships an *invalid* manifest and the vendor is slow: locate
        it in the archive (Organizer → Archives → Show in Finder → Show Package Contents),
        delete or fix it, and distribute from the Archives organizer so Xcode re-signs. [S2]
      
      ## Reading the Xcode privacy report
      
      Product > Archive → in the Organizer, Control-click the archive → **Generate Privacy
      Report** → save and open the PDF. "Xcode can create a privacy report by aggregating
      the privacy manifests from your app and the third-party SDKs it links to." "The
      privacy report is organized in a similar way to Privacy Nutrition Labels. Refer to
      this report when you provide your app's privacy details in App Store Connect." [S3]
      Use it as the source of truth for the App Privacy questionnaire (see parity below),
      and as the diff when an SDK bump changes what is collected.
      
      ## Upload-time emails
      
      Apple identifies each upload-time problem with an `ITMS-` code in the email. The codes
      below have an Apple-authored source; other codes circulating in forum posts are quoted
      from developer emails only, so they are not listed here.
      
      | Code | Meaning | Fix |
      |---|---|---|
      | `ITMS-91056: Invalid privacy manifest` | "The PrivacyInfo.xcprivacy file from the following path is invalid … Keys and values in your app's privacy manifests must be valid." Either a malformed plist or a valid plist with unexpected keys/values (wrong types, empty arrays, reason strings that don't match the category, a `false` tracking flag with domains listed). [S7] | `plutil -lint /path/to/PrivacyInfo.xcprivacy`, then walk TN3181's reason/solution tables. If the path in the email is inside an SDK's bundle, update the SDK. [S7] |
      | `ITMS-91061: Missing privacy manifest` | "Your app includes "<path/to/SDK>", which includes <SDK>, an SDK that was identified in the documentation as a privacy-impacting third-party SDK. Starting February 12, 2025, if a new app includes a privacy-impacting SDK, or an app update adds a new privacy-impacting SDK, the SDK must include a privacy manifest file." [S10] | Get an SDK version that bundles its own manifest at the expected location; never paper over it by adding the SDK's practices to the app manifest. [S10] |
      | Missing-reason email (required-reason API) | "If you upload an app to App Store Connect that uses required reason API without describing the reason in its privacy manifest file, Apple sends you an email reminding you to add the reason to the app's privacy manifest." Since May 1, 2024 the upload is not accepted. [S4] | Add the category + reason dictionary to whichever bundle's code uses the API (app or SDK). The email names the offending file, so a category reported for the app binary is yours to declare; one reported for `Frameworks/X.framework` is the SDK's. [S4] |
      | `ITMS-90683: Missing purpose string in Info.plist` | "Your app's code references one or more APIs that access sensitive user data, or the app has one or more entitlements that permit such access. The Info.plist file for the "{app-bundle-path}" bundle should contain a NSLocationWhenInUseUsageDescription key with a user-facing purpose string". [S11] | Add the `…UsageDescription` key for every protected resource the code (including SDK code) can reach — see purpose strings below. |
      
      ## Parity with the App Privacy label
      
      The `SKILL.md` "Privacy-label parity" row is the rule; this is the mechanism. App Store
      Connect's App Privacy answers, the aggregated privacy report, and the actual SDK
      behavior must describe the same collection. Apple ties them together explicitly: the
      privacy report is "organized in a similar way to Privacy Nutrition Labels" and is
      what you "refer to … when you provide your app's privacy details in App Store
      Connect" [S3]. So the workflow is one-directional — regenerate the privacy report
      after every SDK change, then update the App Privacy questionnaire from it, never the
      other way round.
      
      ## Info.plist purpose strings
      
      Purpose strings are a separate mechanism from the manifest, but they fail at the same
      two gates: at runtime and at upload/review.
      
      - **Runtime.** "Always provide a valid purpose string in the Signing and Capabilities
        editor if your app uses a protected resource. If you don't, attempts to access the
        resource fail, and might cause your app to crash." The crash report's
        `Termination Reason` field can carry this — Apple lists "accessing privacy sensitive
        information without a purpose string" among its example messages. [S11][S13]
      - **Upload / review.** "App Review checks for the use of protected resources, and
        rejects apps that contain code accessing those resources without a purpose string"
        (ITMS-90683 above). A *vague* string is a Guideline 5.1.1(ii) problem: "Ensure your
        purpose strings clearly and completely describe your use of the data." Apple's
        validity rules: not blank / whitespace, under 4,000 bytes, correct type, and "a
        description that's accurate, meaningful, and specific about why the app needs to
        access the protected resource" — for every localization. [S11][S12]
      - Localize them in `InfoPlist.xcstrings`; Xcode writes them as
        `INFOPLIST_KEY_<KeyName>` build settings. [S11]
      
      Common keys (current, non-deprecated names): [S14]
      
      | Resource | Key |
      |---|---|
      | Camera | `NSCameraUsageDescription` |
      | Microphone | `NSMicrophoneUsageDescription` |
      | Photos (read) / (add only) | `NSPhotoLibraryUsageDescription` / `NSPhotoLibraryAddUsageDescription` |
      | Location (when in use) / (always) | `NSLocationWhenInUseUsageDescription` / `NSLocationAlwaysAndWhenInUseUsageDescription` (`NSLocationAlwaysUsageDescription` is deprecated) |
      | Contacts | `NSContactsUsageDescription` |
      | Calendars (full) / (write-only) | `NSCalendarsFullAccessUsageDescription` / `NSCalendarsWriteOnlyAccessUsageDescription` |
      | Reminders | `NSRemindersFullAccessUsageDescription` |
      | Bluetooth | `NSBluetoothAlwaysUsageDescription` (`NSBluetoothPeripheralUsageDescription` is deprecated) |
      | Face ID | `NSFaceIDUsageDescription` |
      | Motion | `NSMotionUsageDescription` |
      | Health (read) / (write) | `NSHealthShareUsageDescription` / `NSHealthUpdateUsageDescription` |
      | Local network | `NSLocalNetworkUsageDescription` |
      | Speech recognition | `NSSpeechRecognitionUsageDescription` |
      | Tracking (ATT) | `NSUserTrackingUsageDescription` |
      | Game Center friends | `NSGKFriendListUsageDescription` |
      
      HIG rules for the prompt itself: "Request permission only when your app clearly needs
      access to the data or resource"; "Avoid requesting permission at launch unless the
      data or resource is required for your app to function"; write "a brief, complete
      sentence that's straightforward, specific, and easy to understand" in sentence case,
      active voice, ending with a period. Apple's own good/bad pair: "The app records during
      the night to detect snoring sounds." vs "Microphone access is needed for a better
      experience." If you show a pre-alert screen, it must have exactly one button labeled
      like "Continue" / "Next" that opens the system alert, no cancel/close, and must never
      imitate or annotate the system alert; for tracking, incentives or look-alike screens
      "will lead to rejection by App Store review". [S15]
      
      ## Pre-submit checklist
      
      1. `PrivacyInfo.xcprivacy` is a target resource of the app (and of every first-party
         framework / package target that uses a required-reason API); for packages the
         `resources:` entry exists. [S1][S2]
      2. `plutil -lint` passes on the app manifest **and** every manifest under
         `Frameworks/` in the archive. [S7]
      3. Every `NSPrivacyAccessedAPITypes` entry has a category string from the list of five
         and at least one reason code copied verbatim from the table above; no empty arrays;
         `UserDefaults` usage is declared (`CA92.1` for app-private defaults). [S5][S7]
      4. Tracking keys are consistent: both present with `true` + non-empty, well-formed
         domains, or `NSPrivacyTracking: false` with no domains key. [S7]
      5. Every SDK on Apple's list is at a version that bundles its own manifest (and a
         signature when consumed as a binary); the app manifest does not restate SDK
         practices. [S6][S10]
      6. Product > Archive → Generate Privacy Report; the report matches the App Privacy
         answers in App Store Connect and the ATT stance in `SKILL.md`'s 5.1.2 row. [S3]
      7. Every protected resource reachable from app or SDK code has its
         `…UsageDescription` key, localized in every shipped locale, with a specific
         sentence (not "needed for a better experience"). [S11][S12][S15]
      8. Permission prompts fire in context, not at launch; any pre-alert screen follows
         the single-"Continue"-button rule. [S15]
      
      ## Sources
      
      - [S1] Privacy manifest files — https://developer.apple.com/documentation/bundleresources/privacy-manifest-files
      - [S2] Adding a privacy manifest to your app or third-party SDK — https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk
      - [S3] Describing data use in privacy manifests — https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests
      - [S4] Describing use of required reason API — https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api
      - [S5] `NSPrivacyAccessedAPIType` (categories, APIs, reason codes) — https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype
      - [S6] Upcoming third-party SDK requirements (SDK list) — https://developer.apple.com/support/third-party-SDK-requirements/
      - [S7] TN3181: Debugging an invalid privacy manifest — https://developer.apple.com/documentation/technotes/tn3181-debugging-invalid-privacy-manifest
      - [S8] TN3183: Adding required reason API entries to your privacy manifest — https://developer.apple.com/documentation/technotes/tn3183-adding-required-reason-api-entries-to-your-privacy-manifest
      - [S9] Privacy updates for App Store submissions (Feb 29, 2024) — https://developer.apple.com/news/?id=3d8a9yyh
      - [S10] Handling ITMS-91061: Missing privacy manifest (DTS Engineer) — https://developer.apple.com/forums/thread/774960
      - [S11] Requesting access to protected resources — https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources
      - [S12] App Store Review Guidelines 5.1.1 — https://developer.apple.com/app-store/review/guidelines/#privacy
      - [S13] Examining the fields in a crash report (Termination Reason) — https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report
      - [S14] Protected resources (Info.plist keys) — https://developer.apple.com/documentation/bundleresources/protected-resources
      - [S15] Human Interface Guidelines: Privacy — https://developer.apple.com/design/human-interface-guidelines/privacy
      - Also relevant: TN3182 (tracking keys) — https://developer.apple.com/documentation/technotes/tn3182-adding-privacy-tracking-keys-to-your-privacy-manifest ; TN3184 (data collection keys) — https://developer.apple.com/documentation/technotes/tn3184-adding-data-collection-details-to-your-privacy-manifest
      
  • SKILL.md 12.5 KB
    ---
    name: app-store-review-rejections
    description: 'Use when preparing an App Store submission, reading an App Review rejection or Resolution Center message citing a guideline number (2.1, 2.3.x, 3.1.1, 4.3, 5.1.1, 5.1.2), or a privacy-manifest upload email (ITMS-91061 / ITMS-91056, missing required-reason API), or auditing an ads + Remove-Ads IAP + CloudKit + Game Center app for the rejection classes it realistically hits: the ATT-vs-AdMob trap, privacy-label parity with `PrivacyInfo.xcprivacy`, Restore Purchases, screenshot metadata, export compliance. Maps each class to a pre-submit fix. Not the submission mechanics (asc-api-automation) nor StoreKit implementation (storekit2-iap-defaults).'
    ---
    
    # App Store Review Rejections
    
    ## Overview
    
    App Review rejects on a small set of recurring guideline violations. Per Apple's
    own transparency reporting, **Guideline 2 Performance (which includes 2.1) is the #1 cause**, and
    **Legal (Guideline 5, which includes 5.1.x)** is the leading policy cause — Apple's
    Transparency Report only breaks rejections down to the top-level guideline, not
    to 2.1 or 5.1.x specifically. This skill maps the rejection
    classes that **a free game/app with banner ads + a Remove-Ads IAP + CloudKit +
    Game Center** realistically hits, to a concrete pre-submission fix. These examples
    assume a free app with banner ads + a Remove-Ads IAP + CloudKit + Game Center; the
    guideline classes apply broadly — skip sections that don't fit your app's feature set.
    
    It is the *content* companion to `asc-api-automation` (which drives **who**/**how** each ASC
    call gets made); this says **why a build gets bounced and how to pre-empt it**.
    
    ## When to use
    
    - Before any TestFlight→App Store submission (`asc-api-automation` got the build up; this gates whether it passes review).
    - A Resolution Center message arrived citing "Guideline X.Y" — find the row, apply the fix.
    - Auditing a new app/game for submission-readiness.
    - NOT for the *mechanics* of submitting or distributing (that's `asc-api-automation`).
    
    ## Quick reference — rejection class → fix (weighted to these apps)
    
    | Guideline | Why it bounces a puzzle-game-with-ads | Pre-submit fix here |
    |---|---|---|
    | **2.1 App Completeness** | Crash on reviewer's device/OS, dead-end flow, placeholder content, ad fails to load → blank space, test/placeholder ad creative shown in the submitted build | Run the build on a *clean* device + the oldest supported OS. Ensure ads degrade gracefully (no empty frame on no-fill). The build App Review sees must read the **production** ad unit ID (`monetization-sdk-integration`'s `#if DEBUG`/Release split already does this) — a test creative reads as placeholder content under 2.1. TestFlight archives are Release builds too, so the same prod ID ships there; the risk is the opposite direction — clicking too many *production* ads without being in Google's test mode risks the AdMob account being flagged for invalid traffic (Google's own guidance). Fix it one of two ways: register each tester's device as a Google test device (AdMob console → Test devices) so it gets test ads on the prod ID, or — when registering every external tester isn't feasible — detect TestFlight at runtime via StoreKit 2 — `if case .verified(let tx) = try await AppTransaction.shared, tx.environment == .sandbox { /* TestFlight or sandbox */ }` (`AppTransaction.shared` / `AppStore.Environment`, iOS 16+; TestFlight builds always run in the sandbox environment) — and switch to the test ad unit for that session only. Do not use the receipt path (`Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"`): `appStoreReceiptURL` is deprecated since iOS 18 / macOS 15, below this catalog's iOS 26 floor. Verify ads actually render, not a blank no-fill frame. No `<TRANSLATE>` / lorem strings. |
    | **2.3.1 Hidden features** | Shipping dormant code / hidden toggles reviewers can reach | No DEBUG-only surfaces (debug menus) reachable in Release. |
    | **2.3.3 Screenshots** | Screenshots don't match the actual app, wrong dimensions, contain device frames Apple disallows, alpha channel; the marketing copy or version number *rendered inside* the screenshot is itself metadata | Do **not** upload snapshot-test baselines directly — they have an alpha channel + wrong dims. Use real device/sim captures at exact spec sizes, regenerated fresh per release. Also check the text baked into the image: a claim like "Zero tracking" or "No third-party SDKs" is a metadata claim App Review can compare against `PrivacyInfo.xcprivacy` (e.g. `NSPrivacyTracking: true` contradicts it) — false claims are rejectable independent of the app's actual behavior; a version number rendered in the screenshot can also simply go stale. |
    | **2.3.10 Irrelevant metadata** | Mentioning Android / "also on Google Play", other platform names in description/keywords | Strip platform references from every shipped locale's metadata. |
    | **3.1.1 In-App Purchase** | Remove-Ads unlock sold outside IAP; **no "Restore Purchases" control**; price/benefit unclear | Remove-Ads must be a StoreKit IAP with a visible Restore Purchases control (implementation → `storekit2-iap-defaults`). Apple's wording is "you *should* have a restore mechanism," but it is enforced in practice as mandatory — non-consumable must restore on reinstall. |
    | **4.3(a)/(b) Spam / saturation** | **Highest latent risk** — saturated genres (puzzle, utility) face 4.3(b) "indistinguishable from what's already available" risk. (Apple's 4.3(b) text names two groups: saturated kinds — dating, flashlight, sound effects, wallpaper, simple timers, fortune telling — accepted only with a meaningfully different or improved experience; and low-effort kinds — drinking games, Kama Sutra, fart, burp — where repeated submissions may lead to removal from the Apple Developer Program. Puzzle games are in neither; the risk is inferred from saturation, not an explicit callout.) A thin clone gets bounced as spam. | Lead with genuine differentiation (design system, content hub, Game Center, cross-platform). Distinct app name/icon/screenshots per app; never ship two near-identical binaries under different names (4.3(a) = same app under multiple Bundle IDs). |
    | **4.2 Minimum Functionality** | Too simple, feels like a web wrapper or template. (A rejection citing "Guideline 4.0 - Design" points at the section-4 preamble — the Guidelines have no numbered 4.0 rule — not at this row.) | Native features (haptics, Game Center, iCloud resume, widgets if any) demonstrate platform depth. |
    | **5.1.1(v) Account deletion / data** | App "supports account creation" but offers no in-app deletion path | Apple's text triggers the deletion requirement only for apps that **"support account creation"**. If the app has no app-specific account-creation flow (identity via iCloud / Game Center), 5.1.1(v) likely doesn't apply — confirm for your app. But **Apple publishes NO explicit iCloud/Game-Center exemption**, so don't assert one as fact. Still: provide a way to clear the user's CloudKit data + a reachable privacy-policy URL. If rejected on 5.1.1(v), reply to Review that the app creates no app-specific account and data-clearing is available, rather than claiming a blanket exemption. |
    | **5.1.1 Privacy policy** | Missing/unreachable privacy policy URL in ASC + in-app | Privacy policy URL set in App Privacy + reachable; covers ads (AdMob) + analytics data. |
    | **5.1.2 Data Use — ATT** | **The AdMob trap.** App accesses `ASIdentifierManager.advertisingIdentifier` (IDFA) or presents the ATT prompt via UMP but lacks `NSUserTrackingUsageDescription`, or never shows the prompt at all | ATT is required when the app accesses IDFA (`ASIdentifierManager.advertisingIdentifier`) or presents the ATT prompt via UMP. **AdMob can serve limited/non-personalized ads without accessing IDFA — in that case ATT is not required** (though UMP/GDPR consent may still apply). When ATT is required: `NSUserTrackingUsageDescription` present in **every shipped locale**, ATT prompt shown via UMP before personalized ads. Ensure each new app in a multi-app repo has its own pre-prompt explainer shown before the ATT system prompt — it is a submission blocker if missing. If you do NOT access IDFA, declare so and don't link to it. Note: reviewers sometimes cite **2.1 (Information Needed)** instead of 5.1.2 when they simply can't *find* where the prompt fires — the sim-verify checklist item below covers both. |
    | **Privacy-label parity** | App Privacy "nutrition" answers in ASC contradict `PrivacyInfo.xcprivacy` / actual SDK behavior (AdMob collects identifiers + usage data) | ASC App Privacy answers must match the committed `PrivacyInfo.xcprivacy` and AdMob's declared collection. Keep them in sync (`apple-three-piece-analytics`). For the manifest keys, the required-reason API reason codes, the SDK-manifest rules, the upload-time `ITMS-` emails and the purpose-string gate, read `references/privacy-manifest.md`. |
    | **Age rating / 2.3.6** | Ads can serve mature content but rating says 4+ (1.3 is the Kids Category, not age-rating honesty) | Set AdMob max ad content rating appropriately; age rating must cover ad content. |
    | **Export compliance (ASC upload step, not a Guideline number)** | Build marked **Missing Compliance** until its encryption use is specified — the Guidelines text has no "export" section; 2.5 is Software Requirements, unrelated to this | Default: set `ITSAppUsesNonExemptEncryption` = `NO` in Info.plist when the app uses no encryption or only exempt encryption, so ASC never asks. Remedy for an already-uploaded build: answer the questions in ASC, or `PATCH /v1/builds/{id}` with `usesNonExemptEncryption` (`asc-api-automation` covers what the API *can* drive). A human step is required only when non-exempt encryption needs documentation uploaded. |
    
    ## Pre-submission checklist (run before flipping a build to "Submit for Review")
    
    1. Clean-device + oldest-OS smoke run; no crash, no blank ad frame, no debug surface.
    2. L10n gate green for every shipped locale — 0 `<TRANSLATE>` (default set → `ai-translated-localization`; rejection-grade for 2.1/2.3).
    3. ATT: `NSUserTrackingUsageDescription` localized for every shipped locale; **sim-verify the ATT prompt actually fires** on a fresh install (drive it with `interactive-simulator-ux-audit`'s idb / simctl tap + screenshot flow — reviewers reject on the *runtime* prompt being absent, not just the Info.plist key); pre-prompt explainer present in every shipped app.
    4. Restore-Purchases button visible; Remove-Ads restores on reinstall.
    5. ASC App Privacy answers == `PrivacyInfo.xcprivacy` == AdMob's declared data use (manifest pre-submit checklist in `references/privacy-manifest.md`).
    6. Screenshots are real captures at spec dimensions (not snapshot baselines).
    7. Privacy-policy URL set and reachable; no other-platform mentions in any locale's metadata.
    8. Per-app distinct name/icon/screenshots; differentiation visible in the first screenshot (4.3).
    9. Age rating questionnaire answered for this version, including the social media capability questions required for submissions since September 2026.
    
    ## Common mistakes
    
    - **Trusting "it compiled / TF accepted it."** Upload success ≠ review pass; the gates above are orthogonal to a green build.
    - **Re-using snapshot PNGs as store screenshots** — wrong dims + alpha → 2.3.3.
    - **Adding the ad SDK but skipping ATT** — the single most common ads-app 5.1.2 bounce.
    - **Filling App Privacy by guesswork** — must mirror the actual SDK + `PrivacyInfo.xcprivacy`, or it's a 5.1.x mismatch.
    - **Shipping a sibling app as a near-clone** — 4.3 spam; sharing code is fine, but each product must have a distinct identity.
    
    ## Sources
    
    Apple App Store Review Guidelines (developer.apple.com) + Apple App Store
    Transparency Report (performance = top rejection cause); Google AdMob iOS privacy
    strategies (ATT / UMP / `NSUserTrackingUsageDescription`). Re-verify guideline
    numbers against the live Guidelines before quoting them in a Resolution Center
    reply — Apple renumbers.
    
    ## Related skills
    
    - `asc-api-automation` — *how* to submit via the ASC REST API once a build exists; this skill covers *what* content has to be true for that submission to pass review.
    - `storekit2-iap-defaults` — 3.1.1 implementation: the Remove-Ads IAP and its Restore Purchases control.
    - `monetization-sdk-integration` — 5.1.2 ATT / PrivacyInfo consequences of shipping AdMob.
    - `ai-translated-localization` — the L10n flow that keeps every shipped locale's metadata and ATT string complete.
    - Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related