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
Install
npx skills add https://github.com/wei18/apple-dev-skills/tree/main/apple-dev-skills/skills/app-store-review-rejections
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wei18-apple-dev-skills@llmmart
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-automationgot 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")
- Clean-device + oldest-OS smoke run; no crash, no blank ad frame, no debug surface.
- L10n gate green for every shipped locale — 0
<TRANSLATE>(default set →ai-translated-localization; rejection-grade for 2.1/2.3). - ATT:
NSUserTrackingUsageDescriptionlocalized for every shipped locale; sim-verify the ATT prompt actually fires on a fresh install (drive it withinteractive-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. - Restore-Purchases button visible; Remove-Ads restores on reinstall.
- ASC App Privacy answers ==
PrivacyInfo.xcprivacy== AdMob's declared data use (manifest pre-submit checklist inreferences/privacy-manifest.md). - Screenshots are real captures at spec dimensions (not snapshot baselines).
- Privacy-policy URL set and reachable; no other-platform mentions in any locale's metadata.
- Per-app distinct name/icon/screenshots; differentiation visible in the first screenshot (4.3).
- 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.
Reviews (0)
No reviews yet.
No comments yet.