# Veo Video Download 403: The URI Needs Your API Key

> The video.uri that Veo returns through the Gemini API is not a public link. Request it with the same API key that generated the video, within 2 days.

- URL: https://blog.laozhang.ai/en/posts/veo-video-download-403
- Published: 2026-10-02
- Updated: 2026-10-02
- Author: LaoZhang AI Team (https://blog.laozhang.ai/en/about)
- Category: AI Video Generation
- Tags: Veo, Gemini API, 403, PERMISSION_DENIED, Video Download

---
If Veo finished generating, you were billed, and the request for the returned `video.uri` comes back `403 PERMISSION_DENIED`, the download request almost certainly went out without your API key. The URI points at the Gemini API's file endpoint on `generativelanguage.googleapis.com`, and that endpoint rejects any caller that does not identify itself. Enabling the API again, re-creating the key, or hunting for an unfamiliar project number in your console does not change what the download request carries.

The fix depends on where the fetch happens:

- **Server code with the Google Gen AI SDK:** download through the SDK client, which sends the key for you.
- **Plain HTTP, curl, or an n8n-style HTTP node:** add the `x-goog-api-key` header with the key that generated the video, and follow redirects.
- **A browser video tag or download link:** the raw URI cannot work there. Download the file on your server and give the browser a URL from your own storage.

All of this applies to the Gemini Developer API. Vertex AI returns a Cloud Storage path or base64 bytes instead, and a 403 on the generation call or while polling is a different problem with a different answer.

## Which 403 you have: the failing request decides the fix

A Veo job is three requests, and only the third one is the download. Find the request that returned 403 before changing anything.

![Map of the four places a Veo 403 can come from: starting the job, polling, the download request, and a Vertex AI gs:// path, each with what it means](https://blog.laozhang.ai/posts/en/veo-video-download-403/img/which-request-returned-403.webp)

| Request that returned 403 | What it means | Where to go |
|---|---|---|
| `predictLongRunning` or `generate_videos` (starting the job) | The key or project is not allowed to call the model: key restrictions, a blocked key, the API not enabled | [Fix a Gemini API 403 by error source](https://blog.laozhang.ai/en/posts/gemini-api-key-permission-denied) |
| `operations.get` (polling) | Reported on September 29, 2026 for `veo-3.1-generate-preview`; cause unknown, no fix published | [Open forum report](https://discuss.ai.google.dev/t/185954) |
| `GET` on `.../files/FILE_ID:download?alt=media` | The download request has no key, or not the key of the project that generated the video | The rest of this page |
| Reading a `gs://` path from Vertex AI | A Cloud Storage permission question, not a Gemini API key question | "When adding the key will not fix the 403" below |

The download case has a recognizable shape: the operation reports `done`, the response contains a URI that looks like `https://generativelanguage.googleapis.com/v1beta/files/FILE_ID:download?alt=media`, and the 403 arrives only when something tries to fetch that address.

## Why the returned video.uri answers 403 PERMISSION_DENIED

The URI is an API resource that requires the caller's key, not a pre-signed link. Google's own download example makes this visible. The REST sample in the [Veo documentation](https://ai.google.dev/gemini-api/docs/veo) reads the URI from the finished operation and then fetches it with the key in a header:

```bash
# Extract the download URI from the final response.
video_uri=$(echo "${status_response}" | jq -r '.response.generateVideoResponse.generatedSamples[0].video.uri')

# Download the video using the URI and API key and follow redirects.
curl -L -o dialogue_example.mp4 -H "x-goog-api-key: $GEMINI_API_KEY" "${video_uri}"
```

The comment in Google's sample says "Download the video using the URI and API key and follow redirects." A request that drops the header is an anonymous request to a Google API, and the API answers the way it answers any anonymous caller.

That also explains why the error mentions a project you have never seen. Reports from 2025 quote this body:

```text
Generative Language API has not been used in project 542708778979 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/generativelanguage.googleapis.com/overview?project=542708778979 then retry
```

Users in the [main forum thread](https://discuss.ai.google.dev/t/96867), a [second thread](https://discuss.ai.google.dev/t/106512), and an [n8n thread](https://discuss.ai.google.dev/t/85592) all saw the same number, `542708778979`, with different accounts and different keys, and none of them owned that project. Forum users inferred that a keyless request gets attributed to a default project on Google's side. Google has not confirmed that in those threads, so treat it as a plausible reading, not an official diagnosis. "Project mismatch", the label in one thread title, is likewise a user's description and not a Google error name.

What is established is narrower and more useful: the message describes the anonymous request, not your project. Nothing you enable in your own console can make it go away.

## What a keyless request and a wrong key return, as of October 2, 2026

A request without a key returns 403, and a request with a bad key returns 400, so the status code tells you which mistake you made. Both responses below come from requests sent on October 2, 2026 to a made-up file id. You can send the same requests without a key or a billing account.

**What was run:** two requests against a file id that does not exist. **What was not run:** a real Veo generation and a successful download, because no billed Gemini API key was available for the test. The success path below rests on Google's documentation and on user reports, each marked as such.

Without a key:

```bash
curl -s "https://generativelanguage.googleapis.com/v1beta/files/abc123xyz000:download?alt=media"
```

```json
{
  "error": {
    "code": 403,
    "message": "Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.",
    "status": "PERMISSION_DENIED"
  }
}
```

The same URL under the `/download/v1beta/...` path prefix returned the identical 403. With an invalid key in `x-goog-api-key`, the answer changed to HTTP 400, status `INVALID_ARGUMENT`, reason `API_KEY_INVALID`, message `API key not valid. Please pass a valid API key.`

Two things follow. First, the file id was invented and the keyless request still got 403, so the rejection happens before the server looks for a file. A 403 with this message says nothing about whether your video exists. Second, the wording has changed since 2025: the keyless message now names "unregistered callers" instead of project `542708778979`. If you see either text on a download, read it the same way.

| Response on the download request | Reading |
|---|---|
| 403, "Method doesn't allow unregistered callers" | No key reached the server |
| 403, "has not been used in project 542708778979" | The 2025 form of the same situation, per user reports |
| 400, `API_KEY_INVALID` | A key arrived but is malformed, truncated, or not a real key |

## Fix the download by runtime: SDK, HTTP header, n8n, browser

The request that works depends on where the fetch runs. Each case below gives the request for one runtime and says whether it comes from Google's documentation or from user reports.

### Server code with the Google Gen AI SDK: files.download

The SDK client you created with your key performs the authenticated download, so you never touch the URI. These are the documented forms:

```python
generated_video = operation.response.generated_videos[0]
client.files.download(file=generated_video.video, destination="dialogue_example.mp4")
```

```javascript
await ai.files.download({
    file: operation.response.generatedVideos[0].video,
    downloadPath: "dialogue_example.mp4",
});
```

Google's JavaScript sample calls `ai.files.download` without `await`. Add it, as shown above. A [fix in the JS SDK](https://github.com/googleapis/js-genai/commit/127c9bf) made the file write complete once the awaited call returns; before that, code could read an empty or partial file. That fix concerns truncated files, not the 403.

If the SDK call itself returns the 403, you are in rarer territory. Mark Daoust, who writes in the forum thread as a maintainer of the SDK, said on October 23, 2025: "Getting that error is 'normal' when pasting the URL with no key into a web-browser. But it should not be coming back from ai.files.download." The team could not reproduce it and asked whether the affected users were on Supabase Edge Functions, which run on Deno. No SDK version has been shown to cause the 403. In practice, the users in that thread got unblocked by fetching the URI themselves with the key, which is the next subsection. `downloadPath` also writes to a local file system, so a runtime without one needs the HTTP form anyway.

### Plain HTTP, Deno, and edge functions: send the key yourself

Use the documented header form: `x-goog-api-key` with the key of the project that generated the video, and follow redirects. In curl that is the command from Google's sample shown earlier. In JavaScript the same request looks like this:

```javascript
const res = await fetch(video.uri, {
  headers: { "x-goog-api-key": process.env.GEMINI_API_KEY },
});
const bytes = new Uint8Array(await res.arrayBuffer());
```

This `fetch` version is a direct translation of the documented curl command and was not executed against a real video here.

The form that users report as working puts the key in the query string instead. This is the code from the [accepted answer](https://discuss.ai.google.dev/t/96867/8) in the forum thread, which its author traced to the Veo 3 sample in AI Studio:

```javascript
const url = decodeURIComponent(generatedVideo.video.uri);
const res = await fetch(`${url}&key=${process.env.API_KEY}`);
```

At least four other users confirmed it between August 2025 and April 2026. The `&key=` form does not appear in Google's Veo documentation, which shows only the header. Prefer the header: a key in a URL ends up in server logs, proxies, and anything that records request lines.

### n8n and other HTTP nodes: add the header to the node that fetches the file

In n8n the generation call and the polling call succeed because they carry the key, and then a separate HTTP Request node fetches the returned URI with no credentials. That node is where the 403 comes from. Add a header named `x-goog-api-key` to it, with the same key the generation node uses, have the node treat the response as a file instead of JSON, and keep redirect following on.

A reply in the n8n thread on Google's developer forum says the same thing from the other side: "If you are using the same API key to generate and download the video, it should work." A key from a different project is not the same key, even when both belong to you.

### A browser video tag or download link: the raw URI cannot work

A video element or a plain link sends a bare `GET`. It cannot attach an `x-goog-api-key` header, so the raw URI returns 403 there by design. An [open Genkit issue](https://github.com/genkit-ai/genkit/issues/4025) describes exactly this: the plugin hands the raw URI to the Developer UI as `media.url`, the UI loads it in a video tag, and the browser request is rejected.

Appending `key=` to the URL makes the tag load, and it also shows your API key to every person who opens the page. That is acceptable on your own machine during development and nowhere else.

## Showing a Veo video to end users without exposing the key

Download the file on your server and serve it from storage you control. The Gemini API has no documented option that returns a signed or public URL for a Veo file. A developer [asked for one](https://discuss.ai.google.dev/t/96867/22) in December 2025, noting that "Exposing the API key in the URL isn't an option since end users need access", and the question has no answer in the thread.

Google's documentation does not prescribe an architecture here. The pattern below follows from three facts: the URI needs the key, the key must not reach a browser, and the file disappears after 2 days.

1. When the operation reports `done`, download the bytes on the server with the key.
2. Write them to your own object storage or disk. In a serverless function, stream the response body straight into the storage upload instead of a temp file.
3. Store your own URL with the job record, and hand that to the client. Sign it or make it public according to your own access rules.
4. Never persist the Google URI as the playback address.

![Four-step flow for serving a Veo video: download on the server with the key, write to your own storage, hand out your own URL, and never keep the Google URI](https://blog.laozhang.ai/posts/en/veo-video-download-403/img/serve-veo-video-from-own-storage.webp)

One forum user passed `output_gcs_uri` to the Gemini API hoping to receive a bucket path, and still got the `generativelanguage.googleapis.com` URI back. Bucket output belongs to Vertex AI.

## The 2-day limit: download before the file is removed

Generated videos stay on Google's servers for 2 days and are then removed. The [Veo documentation](https://ai.google.dev/gemini-api/docs/veo) states that to keep a copy you must download within 2 days of generation. Extended videos count as newly generated, and referencing a video for extension resets its 2-day timer.

The documentation does not say which HTTP status an expired file returns, and no recorded response for that case is available. If a URI that used to work starts failing with the key attached, check the generation time first. The file cannot be recovered after removal; you have to generate again.

This is another reason to download inside the job that polls the operation instead of on first playback. If you reach Veo through a third-party gateway instead of Google's endpoint, the gateway may return its own URL with its own lifetime and its own 403 causes, so its documentation applies, not the 2-day figure.

## When adding the key will not fix the 403

The key fixes a keyless download from the Gemini API. It does not cover these cases.

- **403 while polling.** A report from September 29, 2026 describes `operations.get()` returning `403 PERMISSION_DENIED` for 6 of 6 `veo-3.1-generate-preview` operations created successfully with the Python SDK 2.25.0. No download URI exists at that stage, and no cause or fix has been posted.
- **Batch API result files.** One user reported in November 2025 that downloading Batch result files still returned permission denied with both the header and `key=`, although the same key worked for status calls. It is a single unresolved report on a different kind of file, and it shows that the key is not a universal answer for every file download.
- **Vertex AI.** The [Vertex AI Veo API](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo-video-generation) takes an optional `storageUri` such as `gs://BUCKET_NAME/SUBDIRECTORY` and returns a `gcsUri`. Without it, "base64-encoded video bytes are returned in the response" as `bytesBase64Encoded`. A `gs://` value is a Cloud Storage object path, not an HTTP link, and reading it requires Cloud Storage access for the caller. An API key header does nothing for it.
- **Retrying.** Google's [troubleshooting guide](https://ai.google.dev/gemini-api/docs/troubleshooting) says to retry only transient errors such as 429, 408, and 5xx, and not client errors like 400, 402, or 403. A retry loop on the download changes nothing and burns part of the 2-day window.

If you are moving a pipeline from another video API and the polling and download steps are new to you, the [Sora 2 vs. Veo 3.1 API migration guide](https://blog.laozhang.ai/en/posts/sora-2-api-vs-veo-3-1) covers how Veo's long-running operation differs from a job API.

## Questions about the Veo download 403

### What is project 542708778979 in the Veo 403 error?

It is not one of your projects, and you cannot enable anything on it. The number appeared in 2025 error bodies for different users and different keys whenever the download URI was requested without a key. Forum users read it as a Google-side default project for anonymous requests; Google has not stated that. As of October 2, 2026, a keyless request returns a different message about "unregistered callers".

### I already enabled the Generative Language API, so why is the download still 403?

Because the error is about the download request, not about your project. Your project was clearly allowed to use the API, since generation succeeded and was billed. In the forum threads, enabling the API, changing IAM roles, and re-creating keys did not help anyone; adding the key to the download request did.

### Is there a way to get a signed URL for a Veo video instead of downloading it?

Not from the Gemini API, according to its Veo documentation, which describes only the authenticated download. For browser playback, download the file server-side, store it yourself, and issue a signed URL from your own storage.

### How long does the Veo download URI stay valid?

The file behind it is stored for 2 days from generation, per Google's Veo documentation, and is then removed. Extending the video or referencing it for extension resets that timer.

### Does Veo on Vertex AI return the same download URL?

No. Vertex AI returns a `gcsUri` in your bucket when you pass `storageUri`, or base64 video bytes when you do not. The `generativelanguage.googleapis.com` download URI exists only on the Gemini Developer API.
