Guideline 2.5.1: private APIs you did not call
2.5.1 says apps may only use public APIs. The rejection names a symbol you have never written.
That is because the check is static analysis of the binary you uploaded, not a reviewer using your app. In a React Native project, the call almost always comes from a dependency, often a transitive one, and often from a version that was fine last year and is not now, because Apple's view of what is private is not frozen.
Three things are true and surprising:
- You can be flagged for a name collision. The analysis matches symbols. A method of your own whose name matches a private selector can trip it, and the fix is renaming your own symbol. Check this before assuming a library is guilty.
- Reaching a private API dynamically still counts. Building a selector from a string does not make it a different API; it makes it a circumvention.
- The fix is usually an upgrade. Maintained libraries generally fix these fast, because everyone using them is blocked at the same time.
The symbol name in the rejection is the search key. Not the feature you think it relates to.
Finding it
The message names something like _setApplicationBadgeString or another selector. Search for that literal string:
# the symbol, across every compiled dependency
grep -rl "SYMBOL_FROM_THE_REJECTION" ios/Pods node_modules 2>/dev/null | head
# and in anything already built
nm -a ios/build/**/*.framework/* 2>/dev/null | grep -i "SYMBOL" | head
# your own code, in case the collision is yours
grep -rn "SYMBOL" ios src app --include=*.m --include=*.swift --include=*.h 2>/dev/nullThen decide which of the three cases you are in:
A dependency uses it. Update it. If the current version still does, the library is unmaintained on this point and the honest answer is replacement, not a patch you carry forever.
Your own symbol collides. Rename it. Annoying, and a five-minute fix.
You are calling it deliberately. Stop. There is a supported way to do the thing, or there is not and the feature does not ship.
Why RN projects hit it more
Depth. A moderate Expo app can pull in hundreds of transitive packages, some with native code, some barely maintained. You did not choose most of them.
Age. Native modules written years ago used approaches that were tolerated then. The list of what counts as private is not static.
Bridging. Some libraries reach into UIKit internals to fix a layout or an animation quirk, which is exactly the kind of code that ends up in this category.
And the RN-specific twist: the rejection arrives with no stack trace and no reproduction, so the instinct is to look for a feature. There is no feature. There is a symbol. Why RN and Expo apps fail differently.
The Android equivalent
Android restricts non-SDK interfaces too, and enforces it at runtime on the device rather than at review. So the failure is not a rejection, it is a crash or a quietly disabled feature on newer OS versions, discovered by users rather than by a reviewer.
Different mechanism, same root cause: a dependency reaching for something it was not given. Google Play rejections.
Preventing it rather than discovering it
Audit native dependencies on a schedule, not before a release. The list of packages with native code is short in most projects, and knowing which they are is most of the work.
# packages that actually contain native iOS code
for d in node_modules/*/ node_modules/@*/*/; do
ls "$d"ios/*.podspec "$d"*.podspec >/dev/null 2>&1 && basename "$d"
done | head -40
# and which are stale
npm outdated 2>/dev/null | head -30Prefer maintained libraries for anything touching native UI. This class of problem is almost entirely a maintenance question.
Keep the native dependency count deliberate. Every native package is a way for someone else's decision to become your rejection, which is the same reason a capability you do not use is pure liability. ITMS-90683, purpose strings you did not ask for.
Where it sits
| Guideline | Concern |
|---|---|
| 2.5.1 | Non-public API use, found by binary analysis |
| 2.3.1 | Functionality concealed from review |
| 2.1 | Completeness, where most things are actually reported |
| 4.2 | Minimum functionality |
shipcheck lists the native packages in your tree with their versions and maintenance status, so the audit is a report rather than an afternoon. It cannot run Apple's static analysis for them, and it says so rather than implying otherwise. What it checks. · After a rejection.
More in this section
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 is App Store Guideline 2.5.1?
Apps may only use public APIs and must run on the currently shipping OS. Using non-public, undocumented or private APIs is a rejection, and it is detected by analysis of the binary rather than by a reviewer using the app.
How do I find which library uses a private API?
The rejection usually names the symbol. Search the compiled frameworks and the pods for that symbol, and the library that contains it is the one to replace or update. The symbol name is the search key, not the feature you think it relates to.
Can a React Native library cause a 2.5.1 rejection?
Yes, and that is the usual cause. You did not call the API, a dependency did, and the check looks at the binary you shipped rather than at the code you wrote.
Is using a method with a private-looking name always a problem?
Not always. Static analysis matches symbol names, so a method of your own whose name collides with a private selector can be flagged. Renaming your own symbol is the fix in that case, and it is worth checking before assuming a dependency is at fault.
What about dynamic calls and reflection?
Calling a private API through a string at runtime is still using it, and constructing selectors dynamically to reach non-public behaviour is treated as circumvention rather than as a clever workaround.
Does this apply to Android?
Android has its own restrictions on non-SDK interfaces, enforced by the platform at runtime rather than by store review. The failure mode differs: instead of a rejection you get a crash or a silently disabled feature on newer OS versions.
shipcheck. “Guideline 2.5.1: private APIs you did not call.” Baker Ventures LLC, September 7, 2026. https://shipcheck.bakerventuresstudio.com/rejections/guideline-2-5-1-private-apis/