Build/Submit details page URL
N/A — issue occurs during Apple authentication, before any build starts.
Summary
Apple 2FA codes are rejected with error -21669 ("Incorrect verification code") for accounts where Apple delivers codes via voice call instead of SMS. The mode field in @expo/apple-utils is hardcoded to "sms" in both the code request and verification endpoints, but Apple requires it to match the actual delivery method indicated by pushMode in the handshake response. This is the same class of bug Fastlane had and fixed in fastlane/fastlane#17666.
Managed or bare?
Bare
Environment
expo-env-info 2.0.12 environment info:
System:
OS: macOS 26.4.1
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.19.0
npm: 11.12.1
SDKs:
iOS SDK:
Platforms: DriverKit 25.2, iOS 26.2, macOS 26.2
IDEs:
Xcode: 26.3/17C529
npmPackages:
expo: ~55.0.15 => 55.0.15
react-native: 0.83.4 => 0.83.4
npmGlobalPackages:
eas-cli: 18.7.0
Expo Workflow: bare
Error output
[PATCH] 2FA handshake: {
"trustedPhoneNumbers": [
{
"id": 1,
"pushMode": "voice",
"num": "+1 (•••) •••-••46"
}
],
"noTrustedDevices": true,
"securityCode": {
"length": 6,
"tooManyCodesSent": false,
"tooManyCodesValidated": false,
"securityCodeLocked": false,
"securityCodeCooldown": false
}
}
[PATCH] Apple error: -21669 Incorrect verification code.
Reproducible demo or steps to reproduce from a blank project
Steps to reproduce
- Have an Apple Developer account with a single trusted phone number where Apple delivers 2FA codes via voice call (not SMS). This is determined by Apple based on account/region/carrier settings — the
pushMode field in the handshake response will be "voice" instead of "sms".
- Run
eas credentials --platform ios (or eas build)
- Log in with Apple ID credentials
- Receive a phone call with the 2FA code
- Enter the correct code
- →
✖ Invalid code (error -21669: Incorrect verification code)
The same code works immediately on Apple's website.
Root cause
I traced through the minified @expo/apple-utils (v2.1.19) bundle and identified three issues:
1. smsAutomaticallySent detection ignores delivery method
The detection function checks trustedPhoneNumbers.length === 1 && noTrustedDevices, and if true, assumes Apple auto-sent an SMS and skips the explicit code request. For voice-mode accounts, Apple auto-sent a call, not an SMS — but the code doesn't know the difference.
2. verifyTwoFactorCodeAsync hardcodes mode: "sms"
// Current (minified):
e && (n = "phone", i.phoneNumber = { id: e }, i.mode = "sms")
// ^^^
// Always "sms", even when the code was delivered via voice call
Apple's POST endpoint at appleauth/auth/verify/phone/securitycode requires mode to match the actual delivery method. When Apple sent a voice call but the verify request says "sms", Apple rejects it with -21669.
3. sendRequestTokenToSMSAsync also hardcodes mode: "sms"
The PUT request to appleauth/auth/verify/phone that explicitly requests code delivery also hardcodes mode: "sms". For the fallback path (when smsAutomaticallySent is false), this would also fail for voice-mode accounts.
Verification
I patched the local @expo/apple-utils/build/index.js:
- Changed
i.mode = "sms" → i.mode = "voice" in the verify function
- Result: ✔ Valid code — authentication succeeds immediately
Suggested fix
Apple's handshake response includes pushMode on each trusted phone number. The fix is to read it and pass it through:
// Read the actual delivery mode from the handshake response
const phoneMode = trustedPhoneNumbers[0].pushMode ?? "sms";
// In verifyTwoFactorCodeAsync — use actual mode instead of hardcoded "sms":
if (phoneNumberId) {
payload.phoneNumber = { id: phoneNumberId };
payload.mode = phoneMode;
}
// In sendRequestTokenToSMSAsync — same fix:
data: { phoneNumber: { id: phoneId }, mode: phoneMode }
Workaround
Use ASC API key environment variables to bypass 2FA entirely:
EXPO_ASC_API_KEY_PATH=/path/to/AuthKey.p8 \
EXPO_ASC_KEY_ID=<key-id> \
EXPO_ASC_ISSUER_ID=<issuer-id> \
EXPO_APPLE_TEAM_ID=<team-id> \
eas build --platform ios --profile production --non-interactive
References
Build/Submit details page URL
N/A — issue occurs during Apple authentication, before any build starts.
Summary
Apple 2FA codes are rejected with error
-21669("Incorrect verification code") for accounts where Apple delivers codes via voice call instead of SMS. Themodefield in@expo/apple-utilsis hardcoded to"sms"in both the code request and verification endpoints, but Apple requires it to match the actual delivery method indicated bypushModein the handshake response. This is the same class of bug Fastlane had and fixed in fastlane/fastlane#17666.Managed or bare?
Bare
Environment
Error output
Reproducible demo or steps to reproduce from a blank project
Steps to reproduce
pushModefield in the handshake response will be"voice"instead of"sms".eas credentials --platform ios(oreas build)✖ Invalid code(error-21669: Incorrect verification code)The same code works immediately on Apple's website.
Root cause
I traced through the minified
@expo/apple-utils(v2.1.19) bundle and identified three issues:1.
smsAutomaticallySentdetection ignores delivery methodThe detection function checks
trustedPhoneNumbers.length === 1 && noTrustedDevices, and if true, assumes Apple auto-sent an SMS and skips the explicit code request. For voice-mode accounts, Apple auto-sent a call, not an SMS — but the code doesn't know the difference.2.
verifyTwoFactorCodeAsynchardcodesmode: "sms"Apple's POST endpoint at
appleauth/auth/verify/phone/securitycoderequiresmodeto match the actual delivery method. When Apple sent a voice call but the verify request says"sms", Apple rejects it with-21669.3.
sendRequestTokenToSMSAsyncalso hardcodesmode: "sms"The PUT request to
appleauth/auth/verify/phonethat explicitly requests code delivery also hardcodesmode: "sms". For the fallback path (whensmsAutomaticallySentis false), this would also fail for voice-mode accounts.Verification
I patched the local
@expo/apple-utils/build/index.js:i.mode = "sms"→i.mode = "voice"in the verify functionSuggested fix
Apple's handshake response includes
pushModeon each trusted phone number. The fix is to read it and pass it through:Workaround
Use ASC API key environment variables to bypass 2FA entirely:
References
appleauth/auth/verify/phone/securitycodeendpoint accepts both"sms"and"voice"asmodevalues