Privacy

    How do I fix ITMS-91053 NSPrivacyAccessedAPICategoryUserDefaults?

    App Store Connect rejection email referencing ITMS-91053 and a missing NSPrivacyAccessedAPICategoryUserDefaults declaration in PrivacyInfo.xcprivacy

    You uploaded a build to App Store Connect, the ingestion email landed within minutes, and the body referenced ITMS-91053 with NSPrivacyAccessedAPICategoryUserDefaults as the flagged category. The previous build went through TestFlight without a comment, but today the same upload is held with the status Invalid Binary. The question is which UserDefaults call inside the binary set this off, and the smallest manifest change that lets the next archive process cleanly.

    Short answer

    The short answer is that App Store Connect's ingestion audit found at least one call to UserDefaults or NSUserDefaults in your linked binaries without a matching declaration in PrivacyInfo.xcprivacy. The fix is a single dictionary added to the NSPrivacyAccessedAPITypes array with the category NSPrivacyAccessedAPICategoryUserDefaults and at least one approved reason code (CA92.1, 1C8F.1, or C56D.1). Apple has enforced this rule for new App Store submissions and TestFlight builds since 1 May 2024, per Apple's privacy manifest documentation.

    What you should know

    • The audit runs on the compiled binary, not on Swift source. It reads exported symbols and method calls; calling UserDefaults.standard once is enough to trigger the category requirement.
    • The reason code carries the meaning, not the category alone. A bare entry with no NSPrivacyAccessedAPITypeReasons array fails the audit even when the category is correct.
    • SDKs are the most common source. Patterns reported on Apple Developer Forums show analytics, push, and crash-reporting SDKs touching UserDefaults far more often than first-party code does.
    • PrivacyInfo.xcprivacy must be in the right target. A manifest file added to a workspace but not to the app target resources is invisible to App Store Connect during ingestion.
    • A new build number is required after every fix. App Store Connect deduplicates by version plus build, so a re-upload with the same build sits behind the rejected one.
    • The rejection is unrelated to the App Tracking Transparency prompt. UserDefaults declarations affect ingestion, not runtime; users see nothing change.

    Why has NSPrivacyAccessedAPICategoryUserDefaults become the most common ITMS-91053 trigger?

    The short answer is that UserDefaults is the cheapest API in iOS for storing a flag, and a large slice of the compiled SDK surface uses it. The 2024 enforcement window opened the rejection email to thousands of apps whose code or dependencies had been touching UserDefaults forever without ever needing to declare it. According to Apple's list of required reason APIs, four categories carry the visible audit weight: file timestamp, system boot time, disk space, and UserDefaults. UserDefaults sits at the top because it is reached from more code paths.

    A typical iOS analytics SDK stores opt-in state, install timestamps, and session counters in UserDefaults. A crash reporting framework keeps a last-launch flag there. A push provider stores token state. None of these calls are exotic, and none of them were considered worth flagging before May 2024. The rule change converted years of normal usage into an ingestion gate, and the rejection email reflects that volume.

    The pattern reported by developers is that builds rebuilt against current SDK releases clear the rejection without code changes, because most maintainers shipped manifests in 2024 and 2025. Builds that still pin older SDKs see the same email each upload. Apple's TN3183 technote on required reason entries covers the precise plist shape expected at ingestion.

    What does the privacy manifest entry look like for UserDefaults?

    The privacy manifest is a property list named PrivacyInfo.xcprivacy placed at the root of the app target. The relevant block for UserDefaults is one dictionary inside the NSPrivacyAccessedAPITypes array. A complete minimal block reads:

    <key>NSPrivacyAccessedAPITypes</key>
    <array>
      <dict>
        <key>NSPrivacyAccessedAPIType</key>
        <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
        <key>NSPrivacyAccessedAPITypeReasons</key>
        <array>
          <string>CA92.1</string>
        </array>
      </dict>
    </array>
    

    The NSPrivacyAccessedAPIType key names the category; the NSPrivacyAccessedAPITypeReasons array lists the reason codes you claim. Apple validates both. An empty reasons array fails. An unknown reason code fails. The whole file must be valid plist; an XML typo prevents ingestion from reading any of the other declarations and causes a separate rejection family. The same shape works whether you author the manifest in Xcode's GUI editor or in a text editor; the GUI editor renders the same XML.

    How do I find which dependency calls UserDefaults?

    Three commands cover most cases without needing source access to the offending framework. The first is grep against your installed Pods and Swift Packages:

    grep -R "UserDefaults\|NSUserDefaults" Pods/ SourcePackages/
    

    A direct hit names the file inside the dependency that touches the API. For static binaries with no source, the second tool is nm:

    nm Pods/<Framework>.framework/<Framework> | grep -i userdefaults
    

    The nm output lists the symbols the framework imports; a reference to the UserDefaults Objective-C class or any of its selectors appears here. The third option is strings for the case where the SDK uses runtime APIs to call UserDefaults indirectly:

    strings Pods/<Framework>.framework/<Framework> | grep -i userdefaults
    

    This works because Objective-C selectors live as null-terminated ASCII inside the binary; even dynamic dispatch leaves traces visible to strings. Once the responsible framework is identified, the manifest entry can name it explicitly through a vendor-supplied PrivacyInfo.xcprivacy or via a top-level copy in the app manifest.

    Which reason code applies in which scenario?

    Apple defines three approved reasons for the UserDefaults category. Each maps to a distinct call pattern, and only one needs to appear when the call matches.

    Reason codeApple's descriptionWhen it applies
    CA92.1Read or write user defaults accessible only to the app, app extensions, and App Clips that are members of the same app group, for storing app state.Default settings, feature flags, and opt-in records read or written through UserDefaults.standard and never shared outside the bundle. The most common case.
    1C8F.1Read or write user defaults shared across an App Group with widgets, App Clips, watch apps, or other app extensions.A widget reading a token written by the host app; a watchOS companion reading a preference; any access to defaults instantiated with UserDefaults(suiteName:) and an App Group identifier.
    C56D.1Read or write user defaults on behalf of a third-party SDK whose API surface wraps UserDefaults.A library that exposes a function like setFlag(value:) and implements it with a UserDefaults call; the host app does not call UserDefaults directly. Apple's intent is that the SDK declares this in its own manifest, not the app.

    Pick the code that matches the actual call. Multiple codes can appear in the same NSPrivacyAccessedAPITypeReasons array when more than one pattern is in play.

    What patterns dominate 2026 React Native, Expo, and Flutter builds?

    For React Native and Expo apps, the dependency tree is the surface that produces the rejection. The 2026 picture across the Expo privacy manifest guide and the Flutter issue 145269 on privacy manifests is that core framework code now ships with its own manifest entries, but transitive iOS dependencies are still uneven.

    In Expo apps, the expo-application and expo-modules-core packages declare CA92.1 by default. The remaining gap is third-party native modules that have not been updated. In Flutter, path_provider_foundation, shared_preferences_foundation, and the platform engine declare CA92.1 in current versions; an older shared_preferences pin re-introduces the rejection. In Capacitor, @capacitor/preferences and @capacitor/storage carry their own manifest in recent releases.

    For native Swift apps, the rejection usually traces to first-party code rather than to dependencies. Recent Swift code calls UserDefaults.standard directly in AppDelegate or in feature flag classes; the manifest fix is a single block in the app target.

    A useful diagnostic for any of these stacks is to walk the Pods directory after pod install and list every file named PrivacyInfo.xcprivacy. The frameworks that do not ship one are the candidates for the next rejection.

    What to watch out for

    • A manifest in the workspace but not in the target. The file must appear in the app target Copy Bundle Resources phase. Workspace presence alone is invisible to App Store Connect.
    • One manifest per dynamic framework. When the project links multiple custom dynamic frameworks, each one needs its own manifest. The app-level manifest does not cover statically linked code reliably.
    • Reusing the same CFBundleVersion. Recycling a build number after a manifest fix leaves the rejected binary in front of the corrected one. Increment in Info.plist or the Build Settings of the relevant target.
    • Confusing ITMS-91053 with ITMS-91056 or ITMS-91065. ITMS-91056 covers missing privacy tracking declarations; ITMS-91065 covers third-party SDK privacy manifest signing. The fixes do not overlap, and a UserDefaults entry clears only ITMS-91053.
    • Treating the rejection as a security review. The manifest audit covers a single class of policy. Hardcoded secrets, weak App Transport Security configuration, and unsafe deep linking sit outside it. Those checks live in a separate pass against OWASP MASVS. For an external automated read of the compiled IPA before resubmission, PTKD.com (https://ptkd.com) is one platform focused specifically on pre-submission scanning of iOS and Android binaries against OWASP MASVS controls.

    Key takeaways

    • ITMS-91053 with NSPrivacyAccessedAPICategoryUserDefaults points at a UserDefaults call inside your linked binaries without a matching entry in PrivacyInfo.xcprivacy. The audit runs at ingestion, before TestFlight and App Review.
    • The minimum fix is one dictionary in NSPrivacyAccessedAPITypes naming the category and listing at least one of CA92.1, 1C8F.1, or C56D.1.
    • The most common source in 2026 is a third-party SDK that has not yet shipped its own manifest. Walk the Pods or SwiftPM directory for files named PrivacyInfo.xcprivacy and identify the gaps.
    • After the manifest change, bump CFBundleVersion before archiving again, otherwise the corrected binary stays behind the rejected upload.
    • For an independent automated check of the compiled IPA against OWASP MASVS before resubmission, PTKD.com (https://ptkd.com) runs that pass and flags missing manifest entries together with secret storage and network configuration issues, so a corrected upload does not stall on a different rejection family.
    • #itms-91053
    • #privacy manifest
    • #userdefaults
    • #ios
    • #app store connect
    • #required reason api

    Frequently asked questions

    Why is this the most common privacy manifest rejection in 2026?
    UserDefaults is the API almost every iOS app calls, from analytics flags to first-run tokens, and the surface that produces this rejection is wide. The pattern reported on Apple Developer Forums is that a large share of post-May-2024 ITMS-91053 emails name NSPrivacyAccessedAPICategoryUserDefaults. The next most common category, file timestamp, sits well behind it because fewer code paths touch the file system in the same automatic way.
    Which reason code applies to a regular UserDefaults.standard call?
    CA92.1 is the right code for the standard case. Apple describes it as reading and writing user defaults that are accessible only to your app, app extensions, and App Clips. A single dictionary in NSPrivacyAccessedAPITypes naming NSPrivacyAccessedAPICategoryUserDefaults and listing CA92.1 in NSPrivacyAccessedAPITypeReasons clears the audit for a build whose UserDefaults calls all sit inside one bundle and do not share data through an App Group.
    Why does the rejection cite a SDK I do not directly import?
    iOS SDKs pull in transitive dependencies through CocoaPods and Swift Package Manager. A single npm package can land four or five compiled frameworks in your Pods directory, each free to call UserDefaults. The ingestion audit reads every linked binary, not only your own target. Tracking the exact SDK responsible needs grep across the .ipa, not a review of your Podfile top level.
    Will the rejection clear if a vendor adds their own PrivacyInfo.xcprivacy?
    Yes, when the SDK ships its own manifest inside its framework bundle, App Store Connect reads that file too and stops citing the SDK. The catch noted in the Expo guide is that Apple does not always parse manifests inside statically linked CocoaPods. For static frameworks, the safer fix is to copy the SDK declaration into your app manifest until vendor builds switch to dynamic linking.
    Do I need to bump CFBundleVersion after fixing the manifest?
    Yes. App Store Connect deduplicates by marketing version plus build number, so a re-upload reusing the same build keeps the rejected binary in processing instead of replacing it. Increment CFBundleVersion, archive again, and Transporter will surface the new build under the same version row. A manifest fix without a new build number stays invisible to App Store Connect because the new binary never enters ingestion.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free