One morning my scheduled script's log was full of 403 PERMISSION_DENIED, and my stomach dropped a little. I hadn't changed a single line of code. What changed was the Gemini API policy: since June 19, 2026, requests from keys with no usage restrictions are blocked. It's meant to curb abuse and unexpected billing, but it's exactly the kind of change that quietly stops the automation you've been running on autopilot.
The tricky part is that the error looks a lot like the opposite problem—a key that's restricted too tightly and gets rejected. If you misread which one you're hit by, you'll strip out the restriction you just added and end up back where you started. Below is the order I actually follow as an indie developer running these jobs daily: confirm whether your key is affected, then add restrictions without interrupting anything that's already running.
What "unrestricted" actually means
A key issued from Google Cloud / AI Studio supports two broad kinds of restriction. One is the API restriction, which limits which APIs the key may call. The other is the application restriction, which limits where requests may come from (HTTP referrers, IP addresses, Android/iOS apps). A key with neither set is the "unrestricted" key that is now blocked.
So a key that has "only the Generative Language API enabled" is still unrestricted if its application restriction is empty. I got this wrong at first, assuming "the API is locked down, so I'm fine." In practice it's safer to assume both dimensions are being checked.
| Restriction type | What it limits | If left unset |
|---|---|---|
| API restriction | Which APIs the key can call (e.g. Generative Language API only) | A leaked key can hit any API |
| Application restriction | Request origin (referrer / IP / app) | Anyone, anywhere, can use the key |
First, confirm whether your key is affected
Before opening any console, call the API directly with the key your running script uses, and see whether it passes or gets blocked right now. For my small indie-developer scripts, I use a light call for triage—listing models rather than generating—and the key point is to use the same key as the real job.
# Use the exact same key your automation reads from its env
curl -s -o /dev/null -w "%{http_code}\n" \
"https://generativelanguage.googleapis.com/v1beta/models?key=${GEMINI_API_KEY}"A 200 means it passes for now. On 403, read the body. Whether error.message says "API key not valid" versus something about the request origin or key restrictions tells you whether this is the unrestricted-key block or a different restriction error.
curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=${GEMINI_API_KEY}" \
| python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('error',{}); print(e.get('status'), '|', e.get('message'))"If the message is about a referrer restriction rejecting the request, this isn't the unrestricted-key block—it's a problem with how the restriction itself is configured. That case is covered in "The 403 Error When You Add HTTP Referrer Restrictions to a Gemini API Key," so check there if the symptom matches.
Add restrictions without downtime—never edit the live key in place
This is where it's easiest to cause your own outage. The moment you edit a running key's restriction fields and save, every request origin you didn't account for gets rejected. Any gap between the origins in your head and the places actually using that key becomes an incident. I once nearly took down a batch server I'd simply forgotten about doing exactly this.
So leave the existing key alone, stand up a new restricted key in parallel, validate it, and swap.
- Issue a new key with restrictions set (API restriction = Generative Language API only; application restriction = your real origins)
- Put the new key in a separate env var (e.g.
GEMINI_API_KEY_NEXT) and confirm200with the check script - Verify it passes from every origin you expect—local, CI, production server
- Once clean, point the production env var at the new key
- Don't delete the old key immediately; watch the logs for a few days and disable it only after references drop to zero
The safe mechanics of the swap follow the same idea as "A Zero-Downtime Rotation Pattern for Gemini API Keys": keep old and new side by side for a while, confirm zero references in the logs, then retire the old one. Keep that order and you almost never get burned.
A standing canary so automation never fails silently
What scared me most wasn't the error itself—it was how late I noticed. Scheduled jobs are equally quiet on success and failure, so a 403 can sit unseen until the next run. So I added one light reachability check at the top of the job that detects this specific error and notifies me.
// A one-shot reachability check before the real work; notify only on 403 PERMISSION_DENIED
async function assertKeyUsable(apiKey) {
const url =
"https://generativelanguage.googleapis.com/v1beta/models?key=" + apiKey;
const res = await fetch(url);
if (res.ok) return;
const body = await res.json().catch(() => ({}));
const status = body?.error?.status ?? String(res.status);
const message = body?.error?.message ?? "unknown error";
// Single out config-driven blocks (bad/blocked key) from transient failures
if (res.status === 403) {
await notifyMe(`Gemini API key blocked: ${status} / ${message}`);
throw new Error(`KEY_BLOCKED: ${message}`);
}
// Let 5xx and other transient errors fall through to your normal retry path
throw new Error(`API check failed: ${res.status} ${message}`);
}In my case notifyMe is just a simple note to myself, which was enough. What matters is not treating a "config-driven block (403)" the same as a "transient server error (5xx)." The former never fixes itself until a human edits the restriction; the latter recovers if you wait. Lump them into the same retry loop and you'll hammer an unfixable block forever, inviting more billing or rate limits.
Your next step
Start by listing every key you hold and flagging the ones with an empty application restriction. In the AI Studio or Google Cloud key list, a blank restriction column marks this round's targets. If even one unrestricted key feeds an automated job, the safest place to begin is the "validate a new key, then swap" flow above—adding restrictions without stopping anything that's running. The more hands-off the setup, the more a check before it breaks pays off.