shipcheck

ITMS-90683 — missing purpose string

Updated September 7, 2026 · published by Baker Ventures LLC · sources cited inline

ITMS-90683 means your binary can reach a protected resource and your Info.plist does not say why.

The message names the exact key — NSCameraUsageDescription, NSLocationWhenInUseUsageDescription, NSPhotoLibraryUsageDescription, NSContactsUsageDescription, NSMicrophoneUsageDescription and so on — and it blocks the upload, before any human review.

The React Native version of this is that you probably do not use the capability. Apple checks what the binary can do, not what your code does. A dependency that links a framework with camera capability triggers it even if no code path in your app ever opens a camera.

Which means the fix has two branches, and picking the wrong one is how this becomes a review problem instead of an upload problem. If the feature is real, add an honest purpose string. If it is not, remove the dependency — because a purpose string for a feature you do not have is a Guideline 5.1.1 conversation waiting to happen.

Find which dependency brought it in

# what the built app actually declares
/usr/libexec/PlistBuddy -c "Print" ios/*/Info.plist | grep -i 'UsageDescription' -A1

# which pods link the capability
grep -rl "AVCaptureDevice\|CLLocationManager\|PHPhotoLibrary\|CNContactStore" \
  ios/Pods --include="*.h" --include="*.m" --include="*.swift" 2>/dev/null \
  | cut -d/ -f3 | sort -u

# what your config plugins add
grep -A40 '"plugins"' app.json app.config.js 2>/dev/null

The usual culprits in an RN tree: image pickers and camera wrappers, anything doing QR scanning, media library access, geolocation, contacts import, audio recording, and several analytics or support SDKs that bundle a screenshot or screen-recording feature.

The two fixes

If the feature is real: add the key with a purpose string that names the actual feature, set through app config or the plugin rather than in the generated Info.plist.

["expo-camera", {
  "cameraPermission": "Used to photograph a job so it can be attached to the invoice."
}]

Write your own string. Plugin defaults are generic, and a generic purpose string is both a worse user prompt and a weaker answer when App Review asks what the permission is for.

If the feature is not real: remove the dependency. That removes the key, removes the review question, and usually removes a privacy-manifest obligation at the same time. A capability you do not use is pure liability. Auditing what your config plugins added.

Why "add a generic string to get it uploaded" backfires

It works, and it moves the problem downstream to somewhere more expensive.

The upload succeeds. Then a reviewer reads an Info.plist declaring camera access, looks for a camera feature, does not find one, and you are in Guideline 5.1.1 data-minimisation territory — where the fix is the thing you avoided doing, and you have now spent a full review cycle finding that out.

The same applies with more force to location: Guideline 2.5.4 specifically addresses declaring the location background mode without a legitimate persistent-location feature. Detail.

The Expo trap

ios/ is a build artefact regenerated on every prebuild. Adding the key directly to ios/YourApp/Info.plist works locally, disappears when EAS rebuilds, and produces an identical second upload failure — with the change having been correct both times.

Set it in app.json, app.config.js, or a config plugin. Why RN and Expo fail differently.

Where it sits among the other upload blockers

CodeMeans
ITMS-90683Missing Info.plist usage description for a protected resource
ITMS-91053Required-reason API used with no declared reason
ITMS-91054/5/6Invalid category, reason, or malformed manifest
ITMS-91061A listed third-party SDK ships no privacy manifest

All four are checked at upload, before review, and an app can hit several in one submission. They also share a cause: capability arriving through a dependency rather than through a decision.

That is why the useful habit is auditing on a dependency change rather than before a release. shipcheck reads app config, config plugins, the generated Info.plist, Podfile.lock and node_modules together and reports purpose strings with no corresponding code path, before you build. What it checks. · The first-submission checklist.

About shipcheck

shipcheck is a pre-submission checker from Baker Ventures LLC that runs inside Claude Code. It reads your React Native or Expo project, finds the things that get apps rejected or blocked at upload, cites the exact App Store Review Guideline or Google Play policy clause, and names the file and line to fix.

It exists because React Native and Expo apps fail for reasons that are invisible in the code you wrote: capability arriving through a dependency, purpose strings added by a config plugin, a privacy manifest missing from an SDK you never chose directly, and a generated ios/ directory that discards your edits on the next prebuild. shipcheck checks what the binary and the config actually declare, not what you intended. The rejection references on this site are free, need no account, and link to the primary Apple or Google document for every claim.

Questions and answers

What does ITMS-90683 mean?

Your app's Info.plist is missing a usage description key for a protected resource the binary can access. The message names the specific key, for example NSCameraUsageDescription or NSLocationWhenInUseUsageDescription, and it blocks the upload rather than waiting for review.

Why do I get ITMS-90683 for a permission I do not use?

Because a linked framework can access that resource, and Apple checks the binary rather than your intent. A React Native dependency that includes camera or location capability triggers it even if no code path in your app ever calls it.

How do I fix ITMS-90683 in Expo?

Add the usage description through app config or the relevant config plugin, not by editing ios/YourApp/Info.plist, because prebuild regenerates that file. If the capability is genuinely unused, the better fix is removing the dependency that brought it in.

Can I just add a generic purpose string to make it upload?

It will upload, and it moves the problem to App Review. A purpose string describing a feature the app does not have is a data-minimisation question under Guideline 5.1.1, and a reviewer who cannot find the feature will ask about it.

What is the difference between ITMS-90683 and a privacy manifest error?

ITMS-90683 is about a missing Info.plist usage description for a protected resource. ITMS-91053 and ITMS-91061 are about privacy manifests and required-reason APIs. Different mechanisms, both checked at upload, and an app can hit all of them in one submission.

Cite this pageshipcheck. “ITMS-90683 — missing purpose string.” Baker Ventures LLC, September 7, 2026. https://shipcheck.bakerventuresstudio.com/rejections/itms-90683-missing-purpose-string/