Privacy

    How do I fix ITMS-91056 invalid keys with plutil in the terminal?

    Terminal window showing plutil -lint and plutil -p output against multiple PrivacyInfo.xcprivacy files extracted from an iOS IPA, with one invalid top-level key name highlighted next to an App Store Connect rejection email for ITMS-91056 invalid privacy manifest

    You finished your iOS archive, opened Transporter, and App Store Connect bounced the binary within minutes with ERROR ITMS-91056 naming a PrivacyInfo.xcprivacy file with invalid keys. The faster fix is to catch the bad key on your laptop before you press upload again. This page walks through a terminal-only pre-flight using plutil that finds the same invalid keys the App Store transporter rejects, with no Xcode round-trip and no second upload attempt.

    Short answer

    ITMS-91056 fires when a PrivacyInfo.xcprivacy file inside your IPA carries a key name outside Apple's published schema, almost always inside a third-party framework. Run plutil -lint against every PrivacyInfo.xcprivacy in your unzipped archive, then plutil -p to print each parsed dictionary, and diff the top-level keys against Apple's four recognized names: NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, NSPrivacyAccessedAPITypes. The Apple DTS engineer response in the Apple Developer Forums thread on validating privacy manifests confirms that plutil checks plist syntax only and the schema check is yours to run.

    What you should know

    • plutil ships with the macOS Command Line Tools at /usr/bin/plutil. No install step. Two flags do the work: -lint to confirm the file parses, -p to print the parsed dictionary as a readable tree.
    • The transporter enforces a four-key top-level schema. NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, NSPrivacyAccessedAPITypes. Any other key at the top level triggers ITMS-91056 on upload, regardless of plist validity.
    • plutil -lint catches syntax, not schema. A file with NSPrivacyAccesedAPIType (one 's') still parses cleanly and returns OK. The bad key bounces the upload server-side.
    • The transporter inspects every PrivacyInfo.xcprivacy in the IPA. Your app's manifest plus every framework, plus every resource bundle inside every framework. One stale file is enough.
    • Apple's TN3181 Technote is the canonical debugging document. TN3181 Debugging an invalid privacy manifest names plutil as the recommended validation tool and confirms that no Apple-hosted automated schema check exists for developers.
    • The path fragment in the rejection email is the only state Apple gives you. Save the email; App Store Connect does not surface that path anywhere else in the build status view.
    • Pre-upload validation costs minutes, post-rejection rework costs days. Each rejected upload forces a new build number and another archive cycle.

    Why does plutil -lint pass a file that App Store Connect still rejects?

    The short answer is that plutil is a plist parser, not a privacy manifest schema validator. The two checks run at different layers and against different rule sets.

    plutil -lint walks the file once and asks one question: does this byte stream parse as a valid property list, in XML or binary form? A misspelled key like NSPrivacyAccesedAPIType (one 's') is still a syntactically legal string inside a syntactically legal <key> element. plutil prints PrivacyInfo.xcprivacy: OK and moves on. The DTS engineer response in the Apple Developer Forums thread on validating privacy manifests puts it plainly: plutil confirms plist syntax, and the value-level check is yours to run by hand.

    The App Store transporter runs the schema check server-side, after the upload completes. It loads every PrivacyInfo.xcprivacy inside the IPA, walks each dictionary, and compares each key string against the published list. A key name that does not match by exact case fails. The wording in the rejection email, keys and values must be valid, refers to that schema match, not to plist syntax. Per Apple's documentation on adding a privacy manifest to your app or third-party SDK, the schema check began enforcing on May 1, 2024 for new uploads of apps that list SDKs on Apple's published required-reason list.

    How do you run plutil -lint against every privacy manifest inside an IPA?

    The fastest workflow is to unzip the IPA into a scratch directory, list every PrivacyInfo.xcprivacy under it, and run plutil -lint on each. Three commands cover the case:

    mkdir scratch && cd scratch
    unzip ../YourApp.ipa
    find . -name "PrivacyInfo.xcprivacy" -print0 | xargs -0 plutil -lint
    

    A clean pass prints one OK line per file. Any line that does not say OK is a plist syntax break, and you fix that before anything else. Once the file no longer parses, no further schema check is possible.

    The same find command pairs with plutil -p to print every parsed dictionary in sequence, which is what you actually diff against the schema:

    find . -name "PrivacyInfo.xcprivacy" -exec sh -c 'echo "=== $1 ==="; plutil -p "$1"' _ {} \;
    

    That prints the file path, then the dictionary tree, then the next file. Eyeball each tree for top-level keys outside the four Apple recognizes, and for sub-keys that sit under the wrong parent dictionary. A short shell script that greps the parsed output for unrecognized keys catches most cases in seconds. The OWASP MASTG technique MASTG-TECH-0136 on retrieving PrivacyInfo.xcprivacy files covers the same unzip-and-inspect pattern as part of the broader mobile testing flow.

    What does the output of plutil -p look like for a valid PrivacyInfo.xcprivacy?

    A valid manifest prints as a single top-level dictionary with one to four recognized keys. The shape:

    {
      "NSPrivacyTracking" => 0
      "NSPrivacyTrackingDomains" => []
      "NSPrivacyCollectedDataTypes" => []
      "NSPrivacyAccessedAPITypes" => [
        0 => {
          "NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryUserDefaults"
          "NSPrivacyAccessedAPITypeReasons" => [
            0 => "CA92.1"
          ]
        }
      ]
    }
    

    Two patterns to spot. First, the singular vs plural pair: NSPrivacyAccessedAPIType (singular) is the sub-key inside each dictionary; NSPrivacyAccessedAPITypes (plural) is the array that holds those dictionaries. Reversing them is one of the most frequent invalid-key reports inside hand-edited manifests, because the difference is one letter and both forms look reasonable at a glance. Second, each entry inside NSPrivacyCollectedDataTypes is itself a dictionary that must carry NSPrivacyCollectedDataType, NSPrivacyCollectedDataTypeLinked, NSPrivacyCollectedDataTypeTracking, and NSPrivacyCollectedDataTypePurposes. Missing any of those four fails with the same ITMS-91056 wording even though the cause is omission, not typo.

    Which key names produce most ITMS-91056 invalid key rejections?

    Three patterns dominate the GitHub issue trackers against named SDKs (Reachability.swift, NewRelic, Mapbox, WalletConnect, Twilio Voice, Braze). The first is the typo or capitalization slip: NSPrivacyAccesedAPIType (one 's'), nsPrivacyTracking (lowercase prefix), or NSPrivacyAccessedAPITypeReason (missing trailing 's'). The second is the early-draft holdover from key names that appeared on 2023 WWDC slides but did not survive into the released schema. The third is the parent-child mix-up: putting NSPrivacyAccessedAPITypeReasons outside a dictionary inside NSPrivacyAccessedAPITypes.

    Top-level keyTypeRequired sub-keysCommon invalid variants
    NSPrivacyTrackingBooleannonensPrivacyTracking, NSPrivacyTrackingEnabled
    NSPrivacyTrackingDomainsArray of stringsnoneNSPrivacyTrackingDomain, NSPrivacyTrackingDomainList
    NSPrivacyCollectedDataTypesArray of dictionariesNSPrivacyCollectedDataType, NSPrivacyCollectedDataTypeLinked, NSPrivacyCollectedDataTypeTracking, NSPrivacyCollectedDataTypePurposesNSPrivacyCollectedDataType (used as top-level), NSPrivacyDataCollected
    NSPrivacyAccessedAPITypesArray of dictionariesNSPrivacyAccessedAPIType, NSPrivacyAccessedAPITypeReasonsNSPrivacyAccesedAPIType, NSPrivacyAccessedAPITypeReason, NSPrivacyAccessedAPI

    The case-sensitive comparison is strict. nsPrivacyTracking fails. NSprivacyTracking fails. NSPrivacyTracking with a trailing space inside the <key> element fails. The transporter does not normalize.

    What does Apple's TN3181 add beyond plutil that you should run in your pipeline?

    TN3181 Debugging an invalid privacy manifest is the closest thing Apple publishes to a dedicated debugging guide for ITMS-91056. It names plutil as the validation tool, confirms that schema checking is the developer's responsibility, and walks through the steps to file a code-level support request when a rejection survives a clean local pass. The note also confirms that the path fragment in the rejection email is the only file Apple flagged on that upload, so a single rejection isolates a single file even when several frameworks ship invalid manifests. The follow-up DTS guidance in the ITMS-91056 troubleshooting thread on the Apple Developer Forums recommends exporting the .xcarchive that reproduces the rejection when the local terminal pass cannot find the bad key.

    In a CI pipeline, the practical sequence is: build the archive, export the IPA, unzip into a scratch directory, run plutil -lint across every PrivacyInfo.xcprivacy, then a follow-up script that diffs plutil -p output against a hard-coded list of legal top-level keys. The pipeline fails the build on the first unrecognized key. PTKD.com (https://ptkd.com) automates that exact diff against Apple's current schema as part of its IPA pre-submission scan, which is one of the cleaner ways to catch a single bad key inside a framework you did not write before it costs you a rejected upload.

    What to watch out for

    Three failure modes show up often after the obvious typos are fixed.

    Resource bundle confusion: many SDKs ship as Foo.framework that contains an internal Foo.bundle. The manifest sometimes lives in both the outer framework and the inner bundle, and a stale copy in one location is enough to trigger ITMS-91056 even when the other copy is correct. The find command above catches both.

    Deleting a third-party manifest to skip the schema check: removing PrivacyInfo.xcprivacy from a framework that sits on Apple's published list swaps ITMS-91056 for ITMS-91065, the missing-manifest error. The proper fix is to update the SDK to a version whose maintainer has corrected the manifest, or to patch and re-sign the framework locally, rather than delete the file.

    Treating plutil -lint as a final gate: a clean lint result confirms the bytes parse and nothing more. Without a follow-up schema diff, a clean lint is consistent with a build that the transporter will still reject. Per the DTS engineer response in the privacy manifest validation thread, the schema validation step is unambiguously the developer's responsibility.

    Key takeaways

    • Run plutil -lint then plutil -p against every PrivacyInfo.xcprivacy inside your unzipped IPA before each upload. A clean lint plus a manual diff against the four legal top-level keys catches the most common invalid-key rejections.
    • Save the rejection email when ITMS-91056 fires. The framework path in the email body is the only state App Store Connect surfaces, and the email is the only place it lives.
    • Update third-party SDKs before patching their manifests. Maintainers of Reachability.swift, Mapbox, NewRelic, WalletConnect, Twilio Voice, and Braze have all shipped corrected manifests in response to ITMS-91056 reports, and pulling the newer version is usually faster than re-signing a framework locally.
    • Some teams pipe IPA pre-submission scanning to platforms like PTKD.com (https://ptkd.com), which runs the schema diff and the OWASP MASVS gates in one pass. That trade-off makes sense for shops that ship across multiple clients or multiple stores per week.
    • #itms-91056
    • #privacyinfo-xcprivacy
    • #plutil
    • #ios
    • #privacy-manifest
    • #app-store-connect
    • #terminal
    • #privacy

    Frequently asked questions

    Can I use plutil -lint as my only pre-upload check for PrivacyInfo.xcprivacy?
    No. plutil -lint validates only that the file parses as a property list. A misspelled key like NSPrivacyAccesedAPIType is still a valid plist string, so plutil returns OK and the App Store transporter still rejects the upload server-side. Always follow the lint pass with plutil -p, then diff every top-level key against Apple's four recognized names: NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, NSPrivacyAccessedAPITypes.
    Where does plutil look for PrivacyInfo.xcprivacy files inside an IPA?
    Inside an unzipped IPA, PrivacyInfo.xcprivacy files appear in three locations: at Payload/YourApp.app/PrivacyInfo.xcprivacy for your own app, inside each Frameworks/SomeSDK.framework/PrivacyInfo.xcprivacy for third-party SDKs, and inside resource bundles at Frameworks/SomeSDK.framework/SomeBundle.bundle/PrivacyInfo.xcprivacy. A find pass from the unzipped root prints every copy. The transporter inspects each one, so a single stale file anywhere is enough to bounce the build.
    Does the App Store transporter cache rejected manifests across uploads?
    No. Each upload is parsed and validated independently. App Store Connect does not remember that the previous build had a bad key in MapboxCommon.framework. If you fix the key and re-upload, you also need a fresh CFBundleVersion build number, since App Store Connect refuses two uploads that share an identical version-and-build pair. A clean schema and a new build number unblock the queue on the next attempt.
    What happens if I run plutil -lint on a binary plist privacy manifest?
    plutil -lint handles binary plists the same way it handles XML plists. It calls the parser, prints OK if the bytes deserialize cleanly, and reports a parse error otherwise. The format is transparent at this layer. plutil -p also prints the parsed dictionary regardless of whether the underlying file is binary or XML, which makes it possible to inspect binary manifests shipped inside compiled frameworks without converting them first.
    Can I script the full pre-upload check in a Git hook?
    Yes, although a pre-push Git hook is the wrong layer because the IPA does not exist yet at push time. A post-archive Xcode build phase or a CI step that runs after the archive completes is the right place. The script unzips the archive, runs plutil -lint across every PrivacyInfo.xcprivacy, and fails the build on the first unrecognized top-level key. The check adds seconds; a rejected upload costs hours.

    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