# Veo 视频下载 403：生成成功却拒绝访问，下载请求要带 API key

> Veo 生成成功但下载 403，是因为下载请求没带 API key。服务端加 x-goog-api-key 请求头或用 SDK 下载，2 天内转存到自己的存储。

- URL: https://blog.laozhang.ai/zh/posts/veo-video-download-403
- Published: 2026-10-02
- Updated: 2026-10-02
- Author: LaoZhang AI Team (https://blog.laozhang.ai/zh/about)
- Category: API 指南
- Tags: Veo, Gemini API, 403, 视频下载, PERMISSION_DENIED

---
通过 Gemini API 调用 Veo，任务完成、费用也扣了，响应里的 `video.uri` 一请求却是 403。原因通常不在你的项目设置：这个地址是一个需要凭证的 API 资源，不是公开的文件链接，**下载请求本身没有带 API key**，Google 就按匿名调用拒绝。生成请求带了 key，不代表之后对这个地址的请求也带了。

修法是让下载请求带上生成视频时用的那个 key：

- 服务端用 SDK：调用 `client.files.download(...)`（Python）或 `ai.files.download(...)`（Node），SDK 客户端会带着 key 去取文件。
- 自己发 HTTP 请求（curl、fetch、n8n 的 HTTP 节点）：加请求头 `x-goog-api-key`，并跟随重定向。
- 浏览器里播放或给用户下载：不要把 `video.uri` 直接交给前端，先在服务端下载，再用你自己的存储地址提供给用户。

文件在服务器上只保留 2 天，拿到地址就应该下载。下面的请求写法来自 [Google 的 Veo 文档](https://ai.google.dev/gemini-api/docs/veo)和开发者论坛里的用户确认；真实的 Veo 生成和成功下载没有在这里跑过，实际跑过的只有三次探测请求，范围见“三次 curl 探测”一节。

## 先看 403 出在哪一步：只有“取视频文件”这一步适用

Veo 的调用分三步：提交生成请求，轮询长时间运行操作（operation）直到 `done` 为 `true`，再去取视频文件。三步都可能返回 403，但原因和处理各不相同。

| 403 出现的位置 | 你看到的情况 | 怎么处理 |
| --- | --- | --- |
| 提交生成请求 | 拿不到 operation 名称，没有扣费 | key 受限、被封、API 未启用等，属于另一类问题，见 [Gemini API Key 权限被拒绝：按 403 报错位置修复](https://blog.laozhang.ai/zh/posts/gemini-api-key-permission-denied) |
| 轮询 operation | 生成请求成功，`operations.get` 返回 403，还没有任何视频地址 | 目前只有一条无人回复的用户报告，原因不明，加 key 的修法不适用 |
| 请求 `video.uri` | operation 已完成，响应里有地址，请求这个地址返回 403 | 下面各节讲的就是这种情况 |

![Veo 调用的提交、轮询、取视频文件三步都可能返回 403，只有请求 video.uri 这一步适用加 key 的修法](https://blog.laozhang.ai/posts/zh/veo-video-download-403/img/veo-403-three-stages.webp)

第三种情况的地址形如：

```text
https://generativelanguage.googleapis.com/v1beta/files/FILE_ID:download?alt=media
```

如果你拿到的是 `gs://` 开头的路径或一段 base64，说明走的是 Vertex AI，不是这条线，直接看后面“Vertex AI、Batch 文件和中转服务”一节。

## 为什么自己刚拿到的 video.uri 会拒绝自己

因为 Google 只认请求里的凭证，不认“这个地址是谁生成的”。`generativelanguage.googleapis.com` 上的文件下载是一次普通的 API 调用，和生成、轮询一样要带 key。官方 REST 示例在下载这一步写的注释是 “Download the video using the URI and API key and follow redirects.”，命令里也明确带着请求头：

```bash
curl -L -o dialogue_example.mp4 -H "x-goog-api-key: $GEMINI_API_KEY" "${video_uri}"
```

容易漏掉 key 的几种场景，都是把这个地址当成了普通链接：

- 把地址粘进浏览器地址栏，或者在日志里点开；
- 后端把地址原样返回给前端，前端放进 video 标签的 `src` 或下载链接；
- 在 n8n 这类工具里，生成和轮询节点配了凭证，后面单独加的 HTTP Request 节点只填了 URL；
- 用 `requests.get(uri)`、`fetch(uri)` 直接取，没有加请求头。

`@google/genai` 的维护者 Mark Daoust 在[论坛主帖](https://discuss.ai.google.dev/t/96867/16)里（2025 年 10 月 23 日）的说法是：把不带 key 的地址粘进浏览器得到这个报错是“正常”的，但它不应该从 `ai.files.download` 里返回。也就是说，不带 key 被拒是设计如此；通过 SDK 下载仍然 403 才是值得上报的异常。

## 三次 curl 探测：不带 key 是 403，key 无效是 400

2026 年 10 月 2 日，用 curl 对一个并不存在的文件 ID（`abc123xyz000`）发了三次请求。没有可用的已开通结算的 Gemini API key，所以没有真实生成过 Veo 视频，也没有跑通过一次成功下载；这三次请求能说明的只是“拒绝是怎么发生的”。

第一次，不带任何凭证：

```bash
curl "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"
  }
}
```

第二次，把路径换成 `/download/v1beta/files/abc123xyz000:download?alt=media`，同样不带凭证，返回的 403 一字不差。

第三次，在 `x-goog-api-key` 里放一个无效的 key：

```json
{
  "error": {
    "code": 400,
    "message": "API key not valid. Please pass a valid API key.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "API_KEY_INVALID",
        "domain": "googleapis.com",
        "metadata": {
          "service": "generativelanguage.googleapis.com"
        }
      }
    ]
  }
}
```

从这三个响应可以读出两件事：

1. 文件 ID 是编的，服务端却没有回答“文件不存在”，而是先以“调用方没有身份”拒绝。匿名请求在查找文件之前就被挡下了，所以这个 403 与你的视频是否存在、项目是否启用了 API 都无关。
2. key 写错得到的是 400 `API_KEY_INVALID`，不是 403。如果你的下载请求返回 403 `PERMISSION_DENIED` 且报错里说的是调用方身份或一个陌生项目，更可能是 key 根本没有随请求发出去，而不是 key 的值不对。

## 542708778979 是什么项目：不是你的，也不需要去启用

2025 年的报告里，不带 key 请求下载地址得到的是另一段报错，很多人就是拿着它来搜索的：

```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
```

状态是 `PERMISSION_DENIED`，详情里的 reason 是 `SERVICE_DISABLED`。多个帖子的发帖人都确认 `542708778979` 不在自己的项目列表里，而且不同用户、不同 key 看到的是同一个编号（见[这个主帖](https://discuss.ai.google.dev/t/96867)和[另一个帖子](https://discuss.ai.google.dev/t/106512)）。

这个编号为什么会出现，Google 没有在这些帖子里解释。论坛用户的推测是：没有 key 的请求被算到了某个 Google 自己的默认项目头上，而那个项目没有启用这个 API。这只是推测。可以确定的是发帖人试过的这些办法都没有用：在自己的项目里启用 API、调整 IAM、重建 key、传入 `output_gcs_uri` 想让视频写到自己的存储桶。问题不在你的项目，报错链接也不用点。

截至 2026 年 10 月 2 日，同样的匿名请求返回的已经是上一节那段 “Method doesn't allow unregistered callers” 的文字。两段报错说的是同一件事：请求里没有可识别的调用方。

## 按运行环境选下载方式：SDK、请求头、HTTP 节点

判断标准只有一个：这次下载请求是谁发的，它能不能带上 key。

| 运行环境 | 用什么方式 | 出处 |
| --- | --- | --- |
| Python / Node / Go 服务端，已经在用官方 SDK | SDK 的 `files.download` | 官方文档 |
| curl、`requests`、`fetch`、Deno 或 Edge Function | 请求头 `x-goog-api-key`，跟随重定向 | 官方文档的 REST 示例 |
| n8n 等工具的 HTTP 节点 | 在下载节点的请求头里加 `x-goog-api-key` | 同上；论坛里有“同一个 key 应该可以”的回复 |
| 只能改 URL、加不了请求头 | 地址后拼 `&key=` | 论坛用户确认有效，官方 Veo 文档没有这种写法 |
| 浏览器页面、给终端用户 | 不直接用这个地址，服务端转存 | 见下一节 |

![按运行环境选择 Veo 视频下载方式的对照图：SDK 用 files.download，HTTP 请求和 n8n 节点加 x-goog-api-key 请求头，浏览器端改为服务端转存](https://blog.laozhang.ai/posts/zh/veo-video-download-403/img/veo-download-by-environment.webp)

国内环境下，下载请求和生成请求一样要能连上 `generativelanguage.googleapis.com`。

### 服务端 SDK：files.download 会带着 key 去取

官方文档里的 Python 写法：

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

Node 的写法：

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

Go 对应的是 `client.Files.Download(ctx, video.Video, nil)`。注意传进去的是 `video` 对象，不是把 `video.uri` 取出来再自己请求。

Node 这段有两点要留意。第一，`downloadPath` 是往本地文件系统写文件，属于服务端调用，不能在浏览器里用。第二，文档示例没有写 `await`，而 js-genai 在 2025 年 10 月有一个[修复提交](https://github.com/googleapis/js-genai/commit/127c9bf)，说明是“改动之后，await 完 download，文件才写入完成、数据可完整读取”。下载后马上要读取或上传这个文件时，在调用前加上 `await`，并使用较新的 SDK 版本。这个提交修的是文件为空或不完整，不是 403。

### curl、fetch 和 Edge Function：加 x-goog-api-key 请求头

官方 REST 示例先从 operation 的响应里取地址，再带请求头下载：

```bash
video_uri=$(echo "${status_response}" | jq -r '.response.generateVideoResponse.generatedSamples[0].video.uri')

curl -L -o dialogue_example.mp4 -H "x-goog-api-key: $GEMINI_API_KEY" "${video_uri}"
```

`-L` 不能省，注释里写明了要跟随重定向。换成 `fetch` 就是把同一个请求头照搬过去。下面这段是按官方 curl 写法改写的，没有实际运行过：

```javascript
const res = await fetch(videoUri, {
  headers: { "x-goog-api-key": process.env.GEMINI_API_KEY },
});
if (!res.ok) throw new Error(`download failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
```

`fetch` 默认跟随重定向。主帖的发帖人就跑在 Supabase Edge Functions（Deno）上，这类环境没有本地磁盘可写，用请求头自己取字节、再直接上传到对象存储，比依赖 `downloadPath` 更合适。

### n8n 等 HTTP 节点：下载节点单独配同一个 key

n8n 上的典型情况是：生成和轮询都成功，后面单独的 HTTP Request 节点去请求返回的地址时 403。那个节点是一次全新的请求，前面节点的凭证不会自动跟过来。处理方式：

1. 在下载节点的请求头里加一项，名称 `x-goog-api-key`，值是生成视频时用的同一个 key；
2. 确认节点会跟随重定向；
3. 把响应当作文件（二进制）接收，而不是 JSON 或文本。

论坛里对 n8n 用户的回复（2025 年 6 月 13 日）是 “If you are using the same API key to generate and download the video, it should work.”，同时建议先在 n8n 之外直接调 API 下载一次，以区分是节点配置问题还是 API 本身的问题。这个排查顺序值得照做：先用上面的 curl 在命令行拿到文件，再回头改节点。

### 论坛里的 &key= 写法：能用，但 key 会留在 URL 里

论坛主帖里被标记为解决方案的是把 key 拼到地址后面。原帖的代码，发帖人说来自 AI Studio 里的 Veo 3 示例：

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

因为地址里已经有 `?alt=media`，所以用 `&key=` 接在后面。之后至少四位用户回帖确认有效，时间分布在 2025 年 8 月、9 月和 2026 年 4 月。官方 Veo 文档里只有请求头写法，没有这种查询参数写法；它来自用户确认，这里也没有运行过。

能加请求头时优先用请求头：带 key 的 URL 容易进入访问日志、错误上报和代理记录。只有在工具确实只允许填 URL 时才用这种写法，并且只在服务端使用。

## 给用户播放时：video.uri 不能当播放地址，服务端下载后转存

浏览器的 video 标签和普通下载链接发出的请求加不了 `x-goog-api-key` 请求头，所以把原始地址交给前端必然 403。Genkit 的开发者界面就出过这个问题：插件把 `...:download?alt=media` 原样作为 `media.url` 返回，界面用 video 标签加载时被拒，[对应的 issue](https://github.com/genkit-ai/genkit/issues/4025) 截至 2026 年 10 月 2 日仍是打开状态。

把 key 拼进 URL 再发给前端可以让视频播放，但任何打开页面的人都能在网络面板里看到这个 key，等于把它公开了。论坛里有用户问过能不能像别的平台那样拿到一个可在浏览器里使用的签名链接，理由正是“终端用户要访问，不能把 key 暴露在 URL 里”，这个问题在帖子里没有得到回答。

在没有签名链接的前提下，可行的做法是把这个地址只当作服务端的取件凭据：

1. operation 完成后，服务端立即带 key 下载视频字节；
2. 写入你自己的对象存储（S3、R2、Cloud Storage、Supabase Storage 等）；
3. 前端只拿到你自己存储的地址，公开读或自己签发有时效的链接都可以。

这是从“地址需要 key”和“文件 2 天后删除”两个事实推出来的做法，Google 的 Veo 文档没有规定架构。它顺带解决了过期问题：用户一周后回来看视频，取的是你的副本。

## 下载地址多久有效：2 天后文件被删除

官方文档的说法是：生成的视频在服务器上保存 2 天，之后被移除；要保留副本，必须在生成后 2 天内下载。两个细节：

- 用视频延长功能时，延长出来的视频按新生成的视频计算；一个视频被引用去做延长，它的 2 天计时会重置。
- 过期之后请求这个地址返回什么状态码，文档没有写，也没有可供引用的测试结果。所以不要靠状态码判断“是否过期”，自己记录生成时间更可靠。

实际影响是不要把 `video.uri` 存进数据库留着以后用。队列积压、任务失败后隔天重跑，都可能错过这 2 天。

## Vertex AI、Batch 文件和中转服务：这套修法不适用的情况

加 key 解决的是“Gemini API 的 Veo 下载请求没有带凭证”这一种情况。以下几种不在此列。

**通过 Vertex AI 调用 Veo。** Vertex 的返回方式不同：请求里可以传 `storageUri`（形如 `gs://BUCKET_NAME/SUBDIRECTORY`），响应里得到的是 `gcsUri`，例如 `gs://BUCKET_NAME/TIMESTAMPED_FOLDER/sample_0.mp4`；不传则在响应里直接返回 base64 编码的视频字节。这条线上没有 `generativelanguage.googleapis.com` 的下载地址。`gs://` 是 Cloud Storage 的对象路径，不是 HTTP 链接，读取它需要调用方对那个存储桶有访问权限，和 API key 无关。反过来，有论坛用户在 Gemini API 里传了 `output_gcs_uri`，拿到的仍然是 generativelanguage 的地址，这个参数在 Gemini API 这边没有起作用。

**轮询阶段的 403。** 2026 年 9 月 29 日有一条[报告](https://discuss.ai.google.dev/t/185954)：用 google-genai Python 2.25.0 创建 `veo-3.1-generate-preview` 任务成功，随后 `operations.get()` 返回 403 PERMISSION_DENIED，6 个任务 6 个如此。截至 2026 年 10 月 2 日没有回复，原因不明。这时连下载地址都还没有，不是同一个问题。

**Batch API 的结果文件。** 同一个主帖里，2025 年 11 月有用户下载 Batch 结果文件时，请求头和 `key=` 两种方式都试过，仍然被拒，而同一个 key 查询状态是正常的。这条报告没有后续。它说明“带上 key”并不能保证所有文件下载都成功。

**第三方中转服务。** 通过中转接口调用 Veo 时，返回的视频地址、有效期和 403 的原因都由那家服务自己决定，比如有的服务写的是约 24 小时失效，这不是 Google 的规则。这种情况要看对方的文档，上面关于 `x-goog-api-key` 和 2 天的内容都不能直接套用。

## 带了 key 仍然 403：按这个顺序排查，不要循环重试

先停掉重试。Gemini API 的[排错文档](https://ai.google.dev/gemini-api/docs/troubleshooting)明确写着只重试瞬时错误（429、408、5xx），不要重试 400、402、403 这类客户端错误。对 403 循环重试不会成功，还会消耗掉文件仅有的 2 天。

然后逐项确认：

1. **key 真的发出去了吗。** 打印下载请求的请求头名称（不要打印 key 的值），或者把同一个地址和 key 拿到命令行用官方 curl 写法试一次。环境变量在部署环境里为空时，请求头存在，值却是空字符串。
2. **是不是生成时的那个 key。** 论坛回复给出的条件是用同一个 key 生成和下载。多项目、多环境时，下载服务读到的可能是另一个项目的 key。
3. **重定向后请求头还在不在。** 官方示例要求跟随重定向。自己封装的 HTTP 客户端、代理或网关可能在跳转时去掉自定义请求头，或者根本不跟随跳转。
4. **地址有没有被改动。** 论坛代码在使用前对 `video.uri` 做了一次 `decodeURIComponent`；地址经过数据库、消息队列或模板引擎后，被二次编码或截断都会导致请求的不是原来的资源。
5. **文件是否已超过 2 天。** 对照你记录的生成时间。
6. **用的是哪条线。** 重新看一眼地址的域名和格式，确认不是 Vertex 的 `gs://` 路径，也不是中转服务自己的地址。

如果是通过 `files.download` 下载、key 也确认无误，仍然得到 403，这属于维护者说的“不应该发生”的情况。有没有哪个 SDK 版本自己会触发这个 403，公开信息里没有结论；维护者当时表示无法复现，并询问出问题的人是否都在 Supabase Deno / Edge Functions 上。这时可以先改用请求头方式绕过，再带上 SDK 版本、运行环境和完整报错（去掉 key）到[论坛主帖](https://discuss.ai.google.dev/t/96867)或 SDK 仓库反馈。

如果你是从别的视频接口迁到 Veo，才第一次接触“提交、轮询、再下载”这套流程，[Sora 2 迁移到 Veo 3.1：接口改造、视频限制与成本对比](https://blog.laozhang.ai/zh/posts/sora-2-api-vs-veo-3-1)里有两边接口的对照。
