ITMS-90338: fix “Non-public API usage” with Expo
By Mehdi Azizi
Published , 9 min
ITMS-90338 means your build references a method, function or class Apple considers private. Find the dependency behind it, update it, then rebuild.
ITMS-90338 is an automated check Apple runs on your build. It says your binary references APIs that Apple considers private: Objective-C methods, functions or classes. The fix: find the file named in the email, trace it back to the dependency that contains the name, update that dependency or rename your method, then rebuild.
With Expo, the name almost always comes from a native library, rarely from your JavaScript. On October 15, 2026, I ran the analysis commands from this guide on a simulator build of Solys, my alarm clock app with mini-games (Expo SDK 57), and on the iOS SDK in Xcode 26.2. Here's how to read the email, find the culprit and spot a false positive.
What does the ITMS-90338 error mean?
The message is titled "ITMS-90338: Non-public API usage". It comes in three variants, depending on what Apple found in your binary:
- "references non-public selectors": Objective-C method names, like
boldFontOfSize:; - "references non-public symbols": functions, written with a leading underscore, like
__dyld_get_shared_cache_range; - "contains or inherits from non-public classes": a class name.
Each variant gives a path, then the list of names. The text adds two pieces of advice: rename your methods if their names match a private API, and remove the static library that contains them. An email from September 2026 also suggests xcrun swift-demangle to make encoded Swift names readable.
The rule behind this check is guideline 2.5.1: an app may only use public APIs. The Apple Developer Program License Agreement says it again in section 3.3.1: no calls to private APIs. Human review of this rule is covered in the guideline 2.5.1 guide. Here, we're dealing with the upload error.
Like the other ITMS errors, nobody opened your app: Apple analyzed the binary while processing the build. Apple doesn't publish any list of private APIs: the names in your email are the only reliable list.
Reading the path: an SDK or your executable?
The path written before the list tells you which file to search.
Frameworks/Name.framework/Name: the name lives in an embedded dynamic framework, so in a specific SDK. In the Solys build, this folder contains 14 frameworks: Hermes, React Native, Expo modules shipped precompiled, and the SDWebImage image library.- Just your app's name: the name lives in the main executable. With Expo, it bundles your native code and the pods compiled from source, statically linked by default. Any native library could be the cause.
The decision tree: from the cited name to the fix
Go through the rows in order. The first one that matches your case gives you the lead.
| What you see | Where to look | Fix |
|---|---|---|
Name starting $s/_$s | xcrun swift-demangle | Restart the tree |
| Not found in your IPA | grep on the bundle | Resubmit a build |
| Public name on iOS | iOS SDK headers | Resubmit a build |
API_UNAVAILABLE(ios) | iOS SDK headers | Update the package |
In Frameworks/X | The SDK that provides X | Update the SDK |
| Selector, your app | Pods and native modules | Prefix or update |
Symbol _name, your app | nm -m, then the pods | Replace the call |
| Class, your app | Your classes and pods | Prefix the class |
The "Resubmit a build" rows flag a false positive, covered below.
Step 1: open the build you uploaded
Work on the file you actually sent to Apple, not on your project. Download the IPA from the build page on expo.dev, then run on your Mac:
unzip -q MyApp.ipa -d ipa
ls ipa/Payload/MyApp.app/Frameworks
otool -L ipa/Payload/MyApp.app/MyApp | grep PrivateFrameworks
The second line lists the embedded frameworks. The third looks for a direct link to a private Apple framework: it should print nothing. On Solys, otool -L finds no private framework, neither in the executable nor in the 14 frameworks.
Step 2: find the file that contains the name
Start broad. Search for the cited name, without the leading underscores, across the whole bundle:
grep -rl "dyld_get_shared_cache_range" ipa/Payload/MyApp.app
The command lists the files containing that text. Then narrow it down by name type (selector, symbol, class), on the file you found, shown as Binary:
strings -a Binary | grep -x "boldFontOfSize:"
nm -m Binary | grep "_dyld_get_shared_cache_range"
nm -m Binary | grep '_OBJC_CLASS_$_ClassName'
For a selector, use strings, not nm. On the Solys React framework, nm shows methods with their class, like -[RCTRootView initWithBridge:moduleName:initialProperties:], never the bare selector initWithBridge:. strings finds it.
For a symbol, read the end of the line. On Solys, nm -m shows for example (undefined) external __dyld_get_image_name (from libSystem): the binary calls the function without containing it, and libSystem provides it. That particular function is public, as the next step shows.
Step 3: check whether the name is really private
The iOS SDK headers describe the public APIs. A search that takes a few seconds tells you whether the cited name is there, and on which platforms. For a symbol, first remove the underscore added at compile time: _SCDynamicStoreCreate becomes SCDynamicStoreCreate.
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
grep -rn "SCDynamicStoreCreate" "$SDK/usr/include" "$SDK"/System/Library/Frameworks/*/Headers
On October 15, 2026, with the iOS 26.2 SDK, here's what four names taken from real ITMS-90338 emails gave:
| Cited name | In the iOS SDK | Reading |
|---|---|---|
| _dyld_get_shared_cache_range | Missing | Private API |
| boldFontOfSize: | Missing | Private or collision |
| SCDynamicStoreCreate | API_UNAVAILABLE(ios) | macOS only |
| isMainFrame | WebKit property | Public |
A name that's present and available on iOS points to a false positive. A missing name points to a private API or a collision. Two limits: a Swift-only public API won't show up in these headers, and for an accessor like setDelegate:, search for the delegate property.
Step 4: trace it back to the package
If the path names a framework, its name leads to the pod, then to the npm package:
grep -n "FrameworkName" ios/Podfile.lock
npm explain package-name
npm explain tells you which dependency installed it, if you never added it yourself.
If the path names the executable, search for the name in the native sources, then in the pods:
grep -rl --include='*.m' --include='*.mm' --include='*.swift' --include='*.h' \
"boldFontOfSize" node_modules modules
grep -rl "boldFontOfSize" ios/Pods
Adjust the folder list: modules is the one for local Expo modules. The ios/Pods folder appears after npx expo prebuild -p ios, which also installs the pods on a Mac. Careful: it can regenerate the ios folder. If you edited it by hand, work on a copy.
Searching ios/Pods matters as much as searching the sources. On Solys, that folder contains 15 frameworks shipped precompiled, in .xcframework format: Expo modules, Hermes, React Native and third-party SDKs. A search limited to source files would miss their code.
Typical causes
A third-party SDK, too old or too new. In September 2026, a Flutter developer reported a rejection caused by a security SDK: its version 8.2.3 called __dyld_get_shared_cache_range. Version 8.2.2 went through, and the vendor advised rolling back to it while waiting for a fix.
A native module calling an undocumented API. Your own module, a plugin or a small library might use an internal UIKit method found on a forum. The check catches it by name.
A name collision. Your method or class happens to share its name with a private Apple API. The message anticipates this and asks you to rename it. Apple recommends prefixing methods you add to its classes, for example with three lowercase letters followed by an underscore.
An API that's public on macOS, not on iOS. In 2024, a .NET MAUI app was rejected for _SCDynamicStoreCreate. Apple does document this function, but for macOS only, and the iOS SDK header marks it API_UNAVAILABLE(ios, tvos, watchos).
Fix it without working around it
- Update the dependency. For packages tracked by Expo,
npx expo install --checkflags the ones that don't match your SDK, according to the Expo CLI documentation. For the others, read the changelog. - Roll back to the previous version if the new one introduced the name, and let the vendor know.
- Replace or remove an abandoned library.
- Rename your method or class, with a prefix.
Don't hide the name. Splitting a string or calling the method through a workaround hides the usage without fixing it. It's still forbidden by rule 2.5.1 and by the agreement, and then it's your account on the line.
Then rebuild with a new build number. An over-the-air update isn't enough: the name is in the compiled native code. Before uploading, run step 2 again on the new IPA. The name shouldn't show up anymore.
When it's a false positive
Apple's check sometimes gets it wrong. On June 6, 2022, React Native and Flutter apps got ITMS-90338 for public WebKit names, like setNavigationDelegate: or isMainFrame, including on nearly unchanged updates. The next day, an Apple engineer wrote that the issue was fixed on the validation side and invited people to resubmit their builds. The thread is preserved in the Expo GitHub repository.
Three clues together point to a false positive:
- the build looks like the last accepted build;
- step 2 doesn't find the name, or step 3 shows it's public on iOS;
- other developers report the same error on the same day on Apple's forums.
In that case, check Apple's system status, then resubmit a build with a new number. If the error persists, write to Apple Developer Support with the full email. A name collision, on the other hand, isn't a false positive in Apple's eyes: rename.
Before your next upload
The other ITMS errors have their own guides too: ITMS-90725 for the Xcode version, ITMS-91053 for the privacy manifest and ITMS-90034 for the signature. Once the build is accepted, install it with TestFlight before review.
Rejections from a reviewer call for more perspective: the process is on the app rejected by Apple page, and the most common reasons are in why Apple rejects an app.
To check your submission before uploading, the Review Kit bundles my checklist and Claude Code skills that review your project. If your upload is stuck and you want a human eye on it, book an hour with me: we go through your project on a video call, on your screen. And if you're getting ready for your first release, take the free diagnostic: it puts the steps in order and sends you my Apple anti-rejection checklist.
Frequently asked questions
Is ITMS-90338 an App Review rejection?
No. It's an automated check run while the build is processed, before any human review. The rejected build never reaches TestFlight or review. A private API spotted later by a reviewer falls under guideline 2.5.1.
Can you fix ITMS-90338 with an over-the-air update?
No. The cited names are in the binary's compiled native code. An over-the-air update only changes the JavaScript and assets. Fix the dependency, rebuild, then upload again.
Why does Apple cite methods I never wrote?
Because they come from an embedded library, or from a collision: a dependency declares a method with the same name as a private API. Apple reads the names present in the binary, not your source code. Trace it back to the package with grep and npm explain.
How do I know if an API is public on iOS?
Search for its name in the iOS SDK headers, whose path you get from xcrun --sdk iphoneos --show-sdk-path, then read its availability. Apple's documentation page shows it too. A function documented for macOS only isn't public on iPhone.
Do I need to change the build number after the fix?
Yes. Let EAS increment it: a number already uploaded for the same version can be rejected with ITMS-90189.

