◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-06-02Intermediate

Why Firebase AI Logic Returns 403 When Calling Gemini from iOS — And How to Fix It

Firebase AI Logic (formerly Vertex AI in Firebase) often returns 403 PERMISSION_DENIED when calling Gemini from an iOS app. Here is how to isolate the three real causes — App Check enforcement, disabled APIs, and missing Blaze billing — based on hands-on device testing.

gemini-api290firebase6ios13troubleshooting95app-check

As an indie developer who has shipped iOS apps since 2014, I once lost a full day to a frustrating symptom: Firebase AI Logic worked fine in the simulator, but the moment I pushed the build to TestFlight, every single Gemini call failed with a 403.

The error message is short — usually just PERMISSION_DENIED or The request is missing a valid API key — which makes it hard to tell whether the problem is in your code or your configuration. This article walks through the three causes I actually hit, and the order in which to check them.

First, read the full 403 response

When you call Gemini through Firebase AI Logic (the FirebaseAI module in Swift), a failure comes back like this:

import FirebaseAI
 
let model = FirebaseAI.firebaseAI(backend: .googleAI())
    .generativeModel(modelName: "gemini-2.5-flash")
 
do {
    let response = try await model.generateContent("Hello")
    print(response.text ?? "")
} catch {
    // Don't swallow this — print the error as-is
    print("Gemini call failed:", error)
}

Printing error directly in the catch block reveals details alongside httpStatusCode = 403, such as status: PERMISSION_DENIED or App Check token is invalid. Reading that full message is the starting point for diagnosis. If you only notice that response.text is nil and move on, you'll take the long way around.

Behind a 403 there are broadly three causes. Checking them top to bottom is the fastest path.

Cause 1: App Check is enforced but no token is being issued (most common)

This was the culprit in my case. Once you flip App Check to "Enforce" in the Firebase console, any request without a valid App Check token is rejected with a 403. The reason it worked in the simulator but failed on device and TestFlight: the simulator was passing a debug-provider token, while on a real device I had never initialized the App Attest provider.

The fix is to set the App Check provider before FirebaseApp.configure(). Get the order wrong and the provider gets swapped in after Firebase initializes, so your first few requests fly out with no token.

import FirebaseCore
import FirebaseAppCheck
 
class AppCheckSetup: NSObject, AppCheckProviderFactory {
    func createProvider(with app: FirebaseApp) -> AppCheckProvider? {
        // App Attest on iOS 14+, DeviceCheck on older devices
        if #available(iOS 14.0, *) {
            return AppAttestProvider(app: app)
        }
        return DeviceCheckProvider(app: app)
    }
}
 
// At launch, call this BEFORE configure
AppCheck.setAppCheckProviderFactory(AppCheckSetup())
FirebaseApp.configure()

To test in the simulator during development, use the debug provider and register the debug token (printed to the console) under "App Check → your app → Manage debug tokens" in the Firebase console.

#if DEBUG
AppCheck.setAppCheckProviderFactory(AppCheckDebugProviderFactory())
#else
AppCheck.setAppCheckProviderFactory(AppCheckSetup())
#endif
FirebaseApp.configure()

One caveat: because App Attest verifies device integrity server-side, the first verification can take a few seconds, even on a TestFlight build. If your app calls Gemini immediately on launch, the token may not be issued yet and you'll get a 403. Waiting for a user action before the first call, or adding a light retry, makes it stable.

Cause 2: The required Google APIs are not enabled

If you run the initial setup from "AI Logic" in the Firebase console via the wizard, the required APIs are enabled automatically. But if you reuse another Firebase project, or just swap in a GoogleService-Info.plist from elsewhere, the APIs can stay disabled and you'll get a 403.

The required API depends on the backend you use:

  • Gemini Developer API backend (.googleAI()): firebasevertexai.googleapis.com and the Generative Language API
  • Vertex AI backend (.vertexAI()): aiplatform.googleapis.com

Check under "APIs & Services → Enabled APIs" in Google Cloud Console that these appear for the target project. To check or enable from the command line:

# Check whether enabled
gcloud services list --enabled --project=YOUR_PROJECT_ID | grep -E "aiplatform|firebasevertexai"
 
# Enable if missing (Vertex AI backend example)
gcloud services enable aiplatform.googleapis.com --project=YOUR_PROJECT_ID

In my experience this trap is easiest to fall into when bolting AI features onto an existing app. If the project never went through the new-setup wizard, suspect this first.

Cause 3: The Blaze (pay-as-you-go) plan is not set up

If you use the Vertex AI backend while the project is still on the free Spark plan, you'll hit a 403 or a billing-related error because billing is required. Upgrading to the Blaze plan from "Usage and billing" in the Firebase console resolves it.

The Gemini Developer API backend runs on Spark within the free tier, but for a production app where you want to avoid rate limits, you'll likely move to Blaze anyway. If cost is a concern, set a Google Cloud budget alert for peace of mind.

The isolation order, summarized

When you hit a 403, checking in this order gets you to the cause fastest:

  1. Print the full error in catch and look for App Check token is invalid → if present, it's Cause 1
  2. If not, confirm the target API is enabled in Google Cloud Console → if disabled, Cause 2
  3. If the API is enabled too, check that the billing plan is Blaze → if Spark, Cause 3
  4. If all of these are satisfied and 403 persists, verify that GoogleService-Info.plist matches the calling project (mixing in a plist from another environment makes the console settings and the actual destination diverge)

Causes 1 and 4 in particular can't be spotted by staring at console settings alone. Cross-check the full on-device error log against the PROJECT_ID in the plist your build actually ships.

Preventing a recurrence

My release checklist for AI-enabled apps now includes one line: "Before enforcing App Check, confirm on a real device (TestFlight) that an App Attest token is actually issued." Enforcement is the last switch you flip — turning it on early in development makes isolating any cause far harder.

During development, keep App Check unenforced (monitoring only), watch token issuance in the console, and only switch to enforcement for production. Since adopting that order, I've stopped getting blindsided by 403s.

I hope this saves someone else the day I lost.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ API / SDK2026-10-19
When 404 and 429 Show Up in the Same Gemini API Log, Check the Model Name Before the Quota
A triage order for Gemini API logs that mix 404 NOT_FOUND and 429 RESOURCE_EXHAUSTED, plus a small Python script that separates a bad model name from a real quota limit without generating anything.
◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-06-23
Integrating Gemini 3.2 Pro Function Calling into iOS/Android Apps: Production Design Patterns
A practical guide to integrating Gemini 3.2 Pro Function Calling into iOS and Android apps. Includes working SwiftUI, Kotlin, and Python code, plus production patterns proven in a real indie wallpaper app — cost, latency, staged rollout, and regression testing.