<
>
COTREE
كود ينمو معك

Cotree · Code that grows

Firebase Phone Auth “Error code: 39”: Why OTP Messages Fail and How to Fix It

Developers around the world report Firebase Phone Authentication failing with “An internal error has occurred. [ Error code:39 ]” on real numbers while test numbers work. Here is what the error actually means, what does not fix it, and the two solutions that do.

If your app uses Firebase Phone Authentication, you may have seen this message when sending a verification code to a real phone number: “[firebase_auth/unknown] An internal error has occurred. [ Error code:39 ]”. Test numbers work, some carriers work, and others fail every time. Developers from Iraq, Vietnam, Lebanon, Pakistan and other markets report the same pattern, and most threads end without an answer. We hit it twice in our own products, and this is what we learned.

Error code 39 is not a bug in your code. It is Google refusing to send the SMS on a specific path. Before Firebase delivers a verification code, it has to prove the request comes from your real app and not from a bot. It tries three things in order: a reCAPTCHA Enterprise token if that is configured for the project, a Play Integrity token on Android, which only passes when the app was installed from Google Play, and finally a reCAPTCHA v2 fallback that uses Firebase’s own keys.

During development your app is usually installed from an APK, so Play Integrity fails with “app not recognized by Play Store” and the request drops to the fallback. That fallback is exactly what Google restricts in regions and carriers with high SMS-fraud rates. Firebase Support confirmed this to us in writing: the flow reverts to Firebase’s reCAPTCHA keys, “which triggers the error due to regional limitations”. In our case the code reached one Iraqi carrier and never reached another, with identical settings.

You can see the whole sequence yourself. Enable request logging for Identity Platform on your project, then open Cloud Logging and filter by resource type identitytoolkit_project. A failing attempt shows INVALID_APP_CREDENTIAL, then ALTERNATE_CLIENT_IDENTIFIER_REQUIRED with “Invalid PlayIntegrity token”, then “Error code: 39”. Repeating the attempt only makes it worse: after a few tries the number itself is rate-limited with TOO_MANY_ATTEMPTS_TRY_LATER for several hours.

A few things look like fixes but are not. Registering SHA-1 and SHA-256 fingerprints is required, yet it does not change the result. Enabling the Play Integrity API has no effect on a sideloaded build. Opening the SMS region policy to all countries changes nothing. Raising the SMS fraud score threshold does nothing either, because no score is computed when the token is rejected in the first place.

The first real fix is to switch SMS protection to your own reCAPTCHA Enterprise keys. Enable the reCAPTCHA Enterprise API on the project, create the Identity Toolkit service identity and grant it the identitytoolkit.serviceAgent role, then update the project’s recaptchaConfig with phoneEnforcementState set to AUDIT, useSmsTollFraudProtection set to true, and a tollFraudManagedRules entry with startScore 0.8 and action BLOCK. The default threshold is very strict and rejects legitimate users in many countries, which is why 0.8 matters. On Android, add the com.google.android.recaptcha:recaptcha dependency and use firebase-auth 23.1 or newer. One condition applies: the Android key Firebase creates does not accept apps installed outside Google Play, and that setting cannot be changed later, so this fix works once your app is on the Play Store, even on an internal testing track.

The second fix removes the dependency on Google’s SMS path entirely, and it is the one we now ship by default. A Cloud Function generates the one-time code, stores only its hash with a five-minute expiry, three attempts and per-number and per-IP limits, and sends it through a local or regional messaging provider, ideally on WhatsApp first with SMS as fallback, which is cheaper and faster in most emerging markets. A second function verifies the code, creates the Firebase user by phone number if needed, and returns a custom token. The app signs in with signInWithCustomToken, so the Firebase account, Firestore rules and everything built on top stay exactly the same. The Cloud Functions service account needs the Service Account Token Creator role on itself to mint the custom token. We keep Firebase Phone Auth as the fallback, not the primary path.

In short: Error code 39 is a regional and carrier restriction on Firebase’s fallback verification path, not a problem in your app. If your app is on Google Play, reCAPTCHA Enterprise SMS defense with a 0.8 threshold solves it. If you need verification that works for every user on every phone, a provider-based OTP with custom tokens is the reliable route. If you are stuck on either, write to us at info@cotreeiq.com.

More news