You uploaded a build, the ingestion email landed in your inbox, and the body cites ITMS-91053 with a Missing API declaration line that names UserDefaults. The build sits in App Store Connect with status Invalid Binary, and no error message tells you which reason code to add. The decision sounds small until you stare at the three approved codes and try to pick the right one for an app that mixes a main bundle, a widget, and four analytics SDKs.
Short answer
The short answer is that App Store Connect's ingestion audit found a call to UserDefaults or NSUserDefaults in your linked binaries with no matching declaration in PrivacyInfo.xcprivacy. The fix is one dictionary inside NSPrivacyAccessedAPITypes that names NSPrivacyAccessedAPICategoryUserDefaults and lists at least one approved reason code: CA92.1 for in-app storage, 1C8F.1 for App Group sharing, or C56D.1 for third-party SDK wrappers. Apple has enforced this rule for App Store and TestFlight submissions since 1 May 2024, per Apple's privacy manifest documentation.
What you should know
- The rejection is a manifest gap, not a code gap. UserDefaults itself is legal; the audit fails when the binary calls it without a declaration that matches the category.
- One reason is enough when it fits the call. Apple accepts a single approved reason code per category; the audit does not require you to list every possible scenario.
- The right code depends on call pattern, not call frequency. Three reads from UserDefaults.standard and three thousand still map to CA92.1, as long as no App Group or SDK wrapping is in play.
- Reason codes are case-sensitive and exact. CA92.1 and ca92.1 are not treated as equivalent; a typo blocks ingestion the same as a missing entry.
- A new build number is required after every fix. App Store Connect deduplicates by version plus build, so a re-upload reusing the rejected build number does not replace the bad binary.
- The manifest does not change runtime behavior. No prompt appears, no permission dialog shows; the audit is purely a paperwork check at ingestion.
Which reason code matches my UserDefaults calls?
The short answer is that the call pattern decides the code, not the data type stored. Apple defines three approved reason codes for the UserDefaults category, and each maps to a distinct storage scope. The pattern reported on the Apple Developer Forums thread on 1C8F.1 is that developers reach for the broadest code by default, which is not always correct.
CA92.1 fits the case where every UserDefaults call sits inside a single app bundle. The data never leaves the app; no widget, no App Clip, no watchOS companion reads it. Most early-stage iOS apps and most builds shipped by no-code platforms land here.
1C8F.1 fits the case where UserDefaults data is shared across an App Group. A widget reading a token written by the host app, a watch companion reading a feature flag, or any access through UserDefaults(suiteName:) with an App Group identifier needs this code. According to a clarifying reply on the Apple Developer Forums thread comparing CA92.1 and 1C8F.1, 1C8F.1 is a superset of CA92.1 in scope. The reply suggests declaring 1C8F.1 when any extension or Clip is involved, and CA92.1 only when the build is genuinely a single bundle.
C56D.1 is reserved for third-party SDKs that wrap UserDefaults inside their own API surface. A library exposing a function like Analytics.setFlag(value:) that stores the value in UserDefaults declares C56D.1 in its own manifest. Apple's intent is that the host app does not declare C56D.1 on behalf of the SDK; the SDK ships the entry.
How do I pick the right reason without guessing?
The honest answer is to read the call sites, not the SDK list. Three signals decide the code.
First, search your own source for UserDefaults. Calls to UserDefaults.standard and to a custom suite name behave differently. The latter, when the suite name matches an App Group identifier declared in the entitlements file, means 1C8F.1 applies. The former is CA92.1 unless an extension also reads the same key.
Second, walk the Entitlements.plist file or the Signing and Capabilities tab in Xcode. An com.apple.security.application-groups entry means the project has at least one App Group declared. The presence of an App Group is not enough on its own; what matters is whether UserDefaults uses it. Search for any UserDefaults(suiteName:) call that passes the App Group string.
Third, inspect the Pods and Swift Package directories. A framework that ships its own PrivacyInfo.xcprivacy already declares its reason code. The host app does not need to repeat the declaration. A framework that does not ship a manifest needs investigation: identify the call pattern inside the framework, decide whether C56D.1 applies, and either ask the maintainer to add the manifest or copy the relevant entry into the app manifest as a temporary fix.
What does the manifest entry look like in practice?
The privacy manifest is an XML property list named PrivacyInfo.xcprivacy. A minimal entry for a single-bundle app reads:
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
An app that shares preferences with a widget through an App Group swaps CA92.1 for 1C8F.1. An SDK that wraps UserDefaults ships its own manifest with C56D.1. The structure stays identical; the reason string changes.
The file must sit inside the app target's Copy Bundle Resources phase. The Xcode GUI editor reads and writes the same XML, so the choice between editing in Xcode and editing in a text editor is style only. The official shape is documented in Apple's technote TN3183 on adding required reason API entries, and the validator accepts no variation outside that shape.
How do the three reason codes compare?
The table below maps each reason code to the call pattern Apple expects. Use it to confirm the choice before archiving.
| Reason code | Apple's intent | Typical call site |
|---|---|---|
| CA92.1 | Read or write user defaults visible only to the app, its app extensions, and its App Clips that share the same bundle, used for app state. | A call to UserDefaults.standard for feature flags, opt-in records, or onboarding state in a single-bundle app. |
| 1C8F.1 | Read or write user defaults shared across an App Group with widgets, App Clips, watch apps, or other app extensions. | A call to UserDefaults(suiteName: "group.com.example.app") from the host app or a widget extension reading the same suite. |
| C56D.1 | Read or write user defaults inside a third-party SDK whose API surface wraps UserDefaults, called on behalf of the host app. | A framework function such as Crashlytics.setUserID(_:) whose implementation stores the value in UserDefaults; declared in the SDK's manifest, not the app's. |
The codes are not exclusive when multiple patterns apply. A single app can list CA92.1 and 1C8F.1 together when both patterns are real. Stacking codes you do not need to claim does not buy more leniency at review; it widens the surface a future audit might check.
How do I verify the fix before re-uploading?
Three checks catch most reupload failures. First, open the built .app bundle and confirm PrivacyInfo.xcprivacy is inside it. From the command line, archive the app and run unzip -l YourApp.ipa | grep -i privacyinfo. A manifest that is part of the app target but missing from the resources phase will not appear in the bundle, and the audit will reject the build for the same reason as before.
Second, run plutil -lint PrivacyInfo.xcprivacy to catch XML errors. A malformed plist is read as no declaration; the rejection email is identical to the missing-entry case. The lint output is binary: valid or not.
Third, increment the build number. CFBundleVersion in Info.plist must change. App Store Connect deduplicates uploads by marketing version plus build number, so an upload reusing the same build keeps the rejected binary in front of the new one. The marketing version (CFBundleShortVersionString) does not need to change.
For an external read of the compiled .ipa against the broader set of mobile security controls, PTKD.com (https://ptkd.com) is one platform focused on pre-submission scanning of iOS and Android binaries against OWASP MASVS. It flags missing manifest entries alongside secret storage and network configuration issues, which sit outside the App Store Connect manifest audit but inside the same upload window.
What to watch out for
- Declaring a wider reason than the build supports. Claiming 1C8F.1 when no App Group entitlement exists is technically accepted by ingestion, but it misrepresents the storage model and is flagged on closer review.
- Editing the manifest only in the Pods directory.
pod installrewrites generated manifests on every install. Edits to a Pod's manifest disappear on the next dependency update; the durable fix lives in the host app manifest or in a Podfile post-install hook. - Missing target membership on the manifest file. A
PrivacyInfo.xcprivacyfile in the project navigator that is not a member of the app target stays out of the bundle. Xcode's File Inspector shows the membership toggle on the right. - Confusing ITMS-91053 with ITMS-91056. ITMS-91053 covers required reason API declarations. ITMS-91056 covers third-party SDK signature requirements. A UserDefaults entry never clears an ITMS-91056 rejection.
- Treating the audit as a security check. ITMS-91053 is a privacy paperwork audit. Hardcoded secrets, weak transport security, and insecure deep linking pass the manifest audit without flagging. Those issues belong to a separate review pass against OWASP MASVS.
Key takeaways
- ITMS-91053 with the UserDefaults category points at a call to UserDefaults inside your linked binaries with no matching declaration in PrivacyInfo.xcprivacy. The audit runs at ingestion, before TestFlight.
- The right reason code is CA92.1 for in-app data, 1C8F.1 for App Group sharing, and C56D.1 for third-party SDK wrappers. The call pattern picks the code, not the data type stored.
- Verify the fix by checking that PrivacyInfo.xcprivacy is inside the built
.ipa, thatplutil -lintreports the file as valid, and that CFBundleVersion has been incremented. - For native iOS apps the rejection usually traces to first-party code; for React Native, Expo, and Flutter projects the rejection usually traces to a transitive dependency that has not shipped its own manifest yet.
- For an external automated check of the compiled
.ipaagainst OWASP MASVS before resubmission, PTKD.com (https://ptkd.com) is one platform some teams use to catch missing manifest entries alongside other pre-submission issues in a single pass.




