# Veo 영상 다운로드 403 오류 해결: video.uri 요청에 API 키가 없을 때

> Veo 생성 후 video.uri가 403을 돌려준다면 다운로드 요청에 API 키가 빠진 것입니다. 생성에 쓴 키를 x-goog-api-key 헤더로 보내고 2일 안에 저장합니다.

- URL: https://blog.laozhang.ai/ko/posts/veo-video-download-403
- Published: 2026-10-02
- Updated: 2026-10-02
- Author: LaoZhang AI Team (https://blog.laozhang.ai/ko/about)
- Category: API 가이드
- Tags: Veo, Gemini API, 403, 영상 다운로드, PERMISSION_DENIED

---
Gemini API로 Veo 영상을 생성하면 작업이 끝난 뒤 응답에 `video.uri`가 들어옵니다. 이 주소를 그대로 열었더니 403이 돌아왔다면, API 사용 설정이나 결제, IAM 문제가 아니라 **다운로드 요청에 API 키가 실리지 않은 것**입니다. `video.uri`는 누구나 열 수 있는 공유 링크가 아니라 Gemini API의 파일 리소스이고, 생성 요청과 마찬가지로 호출할 때마다 키가 필요합니다.

해결은 생성에 쓴 것과 같은 API 키를 다운로드 요청에도 보내는 것입니다. [공식 Veo 문서](https://ai.google.dev/gemini-api/docs/veo)의 REST 예제는 이렇게 받습니다.

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

헤더와 함께 `-L`, 즉 리디렉션을 따라가는 옵션이 붙어 있다는 점도 같이 봐야 합니다. SDK를 쓴다면 URL을 직접 요청하지 말고 `files.download`를 호출합니다. 아래에서 실행 환경별로 나눠 설명합니다.

## 키 없이 요청하면 403 PERMISSION_DENIED, 틀린 키는 400

아래 두 응답은 2026년 10월 2일에 존재하지 않는 파일 ID를 넣어 curl로 받은 것입니다. 실행한 것은 키가 없는 요청 두 건(경로 `/v1beta/files/...`와 `/download/v1beta/files/...`)과 잘못된 키를 넣은 요청 한 건, 모두 세 건입니다. 실제로 Veo 영상을 생성해 파일을 내려받는 성공 경로는 실행하지 않았습니다. 따라서 이 기록이 보여 주는 것은 키가 없거나 틀린 요청이 어떻게 거부되는지까지이고, 키를 넣으면 받아진다는 근거는 공식 문서의 예제와 포럼 사용자들의 확인입니다.

키 없이 `GET https://generativelanguage.googleapis.com/v1beta/files/abc123xyz000:download?alt=media`를 보낸 결과입니다. `/download/v1beta/` 경로로 보내도 같은 응답이 왔습니다.

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

파일 ID가 실재하지 않는데도 404가 아니라 403이 왔습니다. 서버는 파일을 찾기 전에 호출자가 누구인지부터 확인하고, 키가 없으면 그 단계에서 거부합니다.

같은 주소에 `x-goog-api-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"
        }
      }
    ]
  }
}
```

이 차이가 진단 기준이 됩니다. 다운로드에서 403 `PERMISSION_DENIED`가 나오면 요청에 키가 아예 도착하지 않았을 가능성이 가장 크고, 400 `API_KEY_INVALID`가 나오면 키는 도착했지만 값이 틀린 것입니다. 환경 변수가 비어 있거나, HTTP 노드에 헤더를 추가하지 않았거나, 리디렉션 과정에서 헤더가 빠지는 경우가 모두 앞쪽에 해당합니다.

![같은 video.uri 요청이 키 없이 보내면 403 PERMISSION_DENIED, 잘못된 키면 400 API_KEY_INVALID, 생성에 쓴 키를 헤더로 보내면 파일 다운로드로 갈리는 비교](https://blog.laozhang.ai/posts/ko/veo-video-download-403/img/download-key-status-responses.webp)

## project 542708778979는 무슨 프로젝트인가요

2025년에 올라온 보고에서는 같은 403이 다른 문구로 나타났습니다. [Google AI Developers Forum의 스레드](https://discuss.ai.google.dev/t/96867)와 [후속 스레드](https://discuss.ai.google.dev/t/106512)에 적힌 메시지는 다음과 같습니다.

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

`status`는 `PERMISSION_DENIED`, 세부 사유는 `SERVICE_DISABLED`였습니다. 이 문구를 본 사람들은 메시지가 시키는 대로 콘솔에서 `542708778979`를 찾고, API를 다시 켜고, IAM을 바꾸고, 키를 새로 만들었습니다. 스레드의 보고자들에 따르면 어느 것도 효과가 없었습니다. 이 번호는 보고자 본인의 프로젝트가 아니었고, 서로 다른 사용자와 서로 다른 키에서 똑같이 나왔기 때문입니다.

이 번호가 왜 나오는지에 대한 Google의 설명은 해당 스레드들에 없습니다. "키 없는 요청이 Google 쪽 기본 프로젝트로 처리된다"는 해석은 포럼 사용자들의 추측입니다. 확실한 것은 조치 방향입니다. 자기 프로젝트 설정을 고치는 것으로는 풀리지 않았고, 다운로드 요청에 키를 넣자 풀렸습니다.

2026년 10월 2일에 키 없이 보낸 요청에는 이 문구가 아니라 위의 `Method doesn't allow unregistered callers` 문구가 돌아왔습니다. 문구는 달라도 상태 코드 403과 `PERMISSION_DENIED`는 같으므로, 어느 쪽을 보든 먼저 확인할 것은 다운로드 요청에 키가 들어 있는지입니다.

## 실행 환경별 다운로드 방법: SDK, curl, fetch, n8n

요청을 무엇이 보내느냐에 따라 키를 싣는 방법이 달라집니다.

| 요청을 보내는 주체 | 키를 싣는 방법 | 근거 |
|---|---|---|
| 서버의 Python·Node·Go SDK | API 키로 만든 클라이언트에서 `files.download` 호출 | 공식 문서 예제 |
| curl, 서버 쪽 HTTP 클라이언트 | `x-goog-api-key` 헤더, 리디렉션 따라가기 | 공식 문서 REST 예제 |
| 서버 쪽 `fetch`(Edge Function 등) | 헤더, 또는 URL 뒤에 `&key=` | 헤더는 공식 문서, 쿼리 형태는 포럼에서 확인된 방법 |
| n8n 같은 노코드 HTTP 노드 | 다운로드 노드에도 생성 노드와 같은 키를 헤더로 추가 | 포럼 답변 |
| 브라우저의 video 태그, 링크 | 방법 없음. 서버에서 받아 다시 제공 | 아래 별도 절 |

### 서버에서 SDK를 쓸 때

공식 문서의 Python 예제입니다. 클라이언트가 API 키를 갖고 있으므로 인증된 다운로드를 대신 수행합니다.

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

JavaScript 예제는 다음과 같습니다.

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

Go에서는 `client.Files.Download(ctx, video.Video, nil)`입니다.

JavaScript 쪽은 두 가지를 알고 써야 합니다. 첫째, `downloadPath`는 로컬 파일 시스템에 쓰는 매개변수이므로 Node 서버용 호출이고 브라우저에서 쓰는 방법이 아닙니다. 둘째, 문서 예제에는 `await`가 없지만 붙여서 호출하는 편이 안전합니다. `@google/genai` 저장소의 [2025년 10월 커밋](https://github.com/googleapis/js-genai/commit/127c9bf)은 "after you await the download, the file write is complete, and the data is fully readable"라고 설명하며 Node 다운로더의 파일 쓰기를 기다리도록 고쳤습니다. 이 수정이 다루는 것은 `download()` 직후 파일이 비어 있거나 잘리는 문제이고 403과는 다른 증상입니다.

SDK의 `files.download`에서 이 403이 나왔다는 보고도 있습니다. 같은 스레드에서 SDK 메인테이너 Mark Daoust는 2025년 10월 23일에 이렇게 답했습니다. "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." 팀에서는 재현하지 못했다고 했고, 보고자들이 Supabase Edge Functions(Deno)를 쓰는지 물었습니다. 특정 SDK 버전이 403을 일으켰는지는 공개된 자료로는 밝혀지지 않았습니다. 이 상황이라면 SDK 호출 대신 아래의 직접 요청으로 바꿔 보는 것이 현실적인 선택입니다.

### curl이나 일반 HTTP 요청으로 받을 때

공식 문서의 REST 예제는 작업 응답에서 주소를 꺼낸 뒤 헤더를 붙여 받습니다.

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

다른 언어의 HTTP 클라이언트로 옮길 때 지켜야 할 것은 세 가지입니다. `x-goog-api-key` 헤더를 넣을 것, 리디렉션을 따라갈 것, 응답 본문을 JSON으로 해석하지 말고 바이너리 그대로 저장할 것입니다.

### 서버 쪽 fetch에서 받을 때

Edge Function처럼 로컬 디스크에 쓰기 어려운 환경에서는 `fetch`로 직접 받아 스토리지로 넘기게 됩니다. `fetch`의 `headers`에 `x-goog-api-key`를 넣으면 위 curl 예제와 같은 요청이 됩니다.

포럼 스레드에서 해결책으로 채택된 코드는 헤더 대신 URL에 키를 붙입니다. 공식 문서의 코드가 아니라 [포럼 답변](https://discuss.ai.google.dev/t/96867/8)의 코드이며, 작성자는 출처를 AI Studio의 Veo 3 샘플이라고 밝혔습니다.

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

돌아오는 주소는 `https://generativelanguage.googleapis.com/v1beta/files/{FILE_ID}:download?alt=media` 형태로 이미 `?alt=media`가 붙어 있어서, 키는 `&key=`로 이어 붙입니다. 2025년 8월부터 2026년 4월 사이에 최소 네 명이 이 방법으로 해결됐다고 답글을 남겼습니다. 다만 `key=` 쿼리 형태는 공식 Veo 문서에 나오지 않으며, 이 요청을 2026년 10월 2일에 직접 실행해 본 기록도 없습니다. 헤더 형태가 문서에 실린 방식이므로 헤더를 먼저 쓰고, 헤더를 넣을 수 없는 도구에서만 쿼리 형태를 고려하면 됩니다. 어느 쪽이든 이 요청은 서버에서만 보내야 합니다.

### n8n 같은 HTTP 노드에서 받을 때

n8n에서 보고된 흐름은 모두 같습니다. 생성 호출과 폴링 노드는 성공하고, 돌아온 URI를 가져오는 별도의 HTTP Request 노드에서 403이 납니다. 앞의 두 노드에는 키를 넣었지만 세 번째 노드에는 URL만 넣었기 때문입니다. [포럼의 답변](https://discuss.ai.google.dev/t/85592)도 "If you are using the same API key to generate and download the video, it should work"라고 정리합니다.

다운로드 노드에 생성 노드와 같은 키를 `x-goog-api-key` 헤더로 추가하고, 리디렉션을 따라가게 하고, 응답을 파일(바이너리)로 받도록 설정합니다. 그래도 403이라면 같은 URI를 n8n 밖에서 위의 curl 명령으로 요청해 보면 원인이 노드 설정인지 API 쪽인지 가를 수 있습니다.

## 브라우저 video 태그에서 재생하려면 서버를 거쳐야 합니다

`video.uri`를 그대로 video 태그의 `src`나 다운로드 링크의 `href`에 넣으면 403이 납니다. 브라우저 태그는 요청에 `x-goog-api-key` 헤더를 추가할 수 없기 때문입니다. Genkit 개발자 UI에서 정확히 이 문제가 [이슈로 올라와](https://github.com/genkit-ai/genkit/issues/4025) 있고, 2025년 12월 30일에 등록된 이 이슈는 2026년 10월 2일 기준 열린 상태입니다.

URL 뒤에 `&key=`를 붙여 태그에 넣으면 재생은 될 수 있지만, 그 URL을 받은 사용자 누구나 키를 볼 수 있습니다. 내부 개발 도구라면 감수할 수도 있겠으나 최종 사용자에게 보이는 화면에서는 쓸 수 없는 방법입니다.

서명된 URL이나 공개 URL을 받을 수 있는지도 같은 스레드에서 2025년 12월 30일에 질문이 올라왔습니다. 사용자에게 키를 노출할 수 없고 서버리스 환경에서는 임시 저장이 번거롭다는 이유였는데, 답변은 달리지 않았습니다. 공식 Veo 문서에도 서명된 URL이나 공개 링크를 받는 방법은 나오지 않습니다.

그래서 사용자에게 영상을 보여 줘야 하는 서비스라면 다음 구조를 권합니다. Google 문서가 지정한 구조는 아니고, 키가 필요한 주소이며 보관 기한이 짧다는 두 가지 사실에서 나오는 설계입니다.

1. 작업이 끝나면 서버에서 키를 실어 파일을 받습니다.
2. 받은 바이트를 자체 스토리지(S3, Cloud Storage, Supabase Storage 등)에 저장합니다.
3. 사용자에게는 자체 스토리지의 URL이나 그 스토리지가 발급한 서명 URL을 줍니다.

Edge Function처럼 디스크를 쓰기 어려운 환경에서는 `fetch` 응답 본문을 스토리지 업로드로 바로 넘기면 임시 파일 없이 처리할 수 있습니다.

![서버가 키를 실어 Veo 영상을 받고 자체 스토리지에 저장한 뒤 사용자에게 자체 URL을 주는 3단계 구조와, video.uri를 브라우저에 그대로 넣을 때의 문제 및 2일 보관 기한](https://blog.laozhang.ai/posts/ko/veo-video-download-403/img/server-storage-delivery-flow.webp)

## 다운로드 URL은 생성 후 2일까지: 403을 재시도해도 풀리지 않습니다

Gemini API가 생성한 영상은 서버에 2일 동안만 보관됩니다. 공식 문서의 문장은 "Generated videos are stored on the server for 2 days, after which they are removed"이고, 사본을 남기려면 생성 후 2일 안에 내려받아야 한다고 덧붙입니다. 영상 확장(extension)으로 만든 결과는 새로 생성된 영상으로 취급되고, 확장에 참조된 영상은 2일 타이머가 다시 시작됩니다.

기한이 지난 파일을 요청했을 때 어떤 상태 코드가 오는지는 문서에 적혀 있지 않고 시험해 본 기록도 없습니다. 그러므로 며칠 지난 URI에서 나는 오류는 키 문제와 삭제를 구분하기 어렵습니다. `video.uri`를 데이터베이스에 저장해 두고 나중에 쓰는 설계는 피하고, 작업이 끝난 직후에 받아 두는 것이 안전합니다.

403을 받았을 때 같은 요청을 반복하는 것도 도움이 되지 않습니다. [Gemini API 문제 해결 문서](https://ai.google.dev/gemini-api/docs/troubleshooting)는 재시도 대상을 429, 408, 5xx 같은 일시적 오류로 한정하고 "Do not retry on client errors (like 400, 402, or 403)"라고 적고 있습니다. 재시도 루프를 돌리는 동안에도 2일 기한은 줄어들므로, 요청을 고친 뒤 한 번 다시 보내는 것이 맞습니다.

## 키를 넣어도 403이면: 다운로드가 아닌 다른 단계의 오류

키를 실어 보냈는데도 실패한다면 같은 403처럼 보여도 다른 문제일 수 있습니다. 어느 요청이 거부됐는지부터 가릅니다.

| 거부된 요청 | 상태 | 할 일 |
|---|---|---|
| 영상 생성 요청 자체 | 키 제한, 차단된 키, API 미사용 설정 등 다운로드와 무관한 원인 | [Gemini API 키 403 권한 거부: 실패한 작업부터 확인하는 복구 절차](https://blog.laozhang.ai/ko/posts/gemini-api-key-permission-denied) 참고 |
| 폴링(`operations.get`) | 2026년 9월 29일 보고: `veo-3.1-generate-preview` 작업 6건 모두 403. 답변 없음, 원인 불명 | 알려진 해결책 없음. [해당 스레드](https://discuss.ai.google.dev/t/185954) 확인 |
| `video.uri` 다운로드, 키 없음 | 403 `PERMISSION_DENIED` | 키를 헤더로 추가 |
| `video.uri` 다운로드, 키 값 오류 | 400 `API_KEY_INVALID` | 환경 변수와 키 값 확인 |
| Batch API 결과 파일 다운로드 | 2025년 11월 24일 보고: 헤더와 `key=` 모두 거부. 미해결 | Veo와 다른 엔드포인트. 알려진 해결책 없음 |

폴링 단계의 403은 다운로드 URI가 생기기도 전에 실패하는 것이어서 키를 다운로드 요청에 넣는 방법과는 관계가 없습니다. Batch API 사례는 사용자 한 명의 보고이지만, 파일 다운로드라면 키만 넣으면 항상 풀린다고 단정할 수 없다는 점을 보여 줍니다.

다음 두 경우도 대상이 다릅니다.

**Vertex AI로 Veo를 호출하는 경우.** [Vertex AI의 Veo API](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo-video-generation)는 `generativelanguage.googleapis.com`의 다운로드 URI를 돌려주지 않습니다. 요청에 `storageUri`를 지정하면 `gs://BUCKET_NAME/TIMESTAMPED_FOLDER/sample_0.mp4` 같은 `gcsUri`가 오고, 지정하지 않으면 base64로 인코딩된 영상 바이트가 응답에 담겨 옵니다. `gs://` 주소는 HTTP 링크가 아니라 Cloud Storage 객체 경로이므로, 읽으려면 호출자에게 해당 버킷 접근 권한이 있어야 합니다. 반대로 Gemini API 쪽에 `output_gcs_uri`를 넘겨도 `generativelanguage` URI가 그대로 돌아왔다는 포럼 보고가 있어, 두 경로의 옵션은 서로 바꿔 쓸 수 없습니다.

**서드파티 API 중계 서비스를 거치는 경우.** 중계 서비스는 자체 URL과 자체 보관 기한, 자체 키 규칙을 쓸 수 있습니다. 2일이라는 기한과 `x-goog-api-key` 헤더는 Google의 Gemini API에 직접 요청할 때의 기준이므로, 중계 서비스의 URL에서 나는 403은 해당 서비스의 문서를 확인해야 합니다.

Veo 3.1의 장기 실행 작업을 생성부터 폴링, 다운로드까지 어떻게 다루는지는 [Sora 2에서 Veo 3.1로 전환하기: API 차이, 비용, 작업 처리 방법](https://blog.laozhang.ai/ko/posts/sora-2-api-vs-veo-3-1)에서 이어서 볼 수 있습니다.

## API를 이미 켰는데 왜 403인가요

콘솔에서 Generative Language API가 사용 설정돼 있고 생성 요청이 성공했다면 프로젝트 설정은 이미 정상입니다. 생성과 폴링이 통과했다는 사실이 그 증거입니다. 그런데도 다운로드에서 403이 나는 이유는 그 한 건의 요청이 어느 프로젝트의 것인지 서버가 알 수 없기 때문입니다. 2025년 보고의 오류 문구가 낯선 프로젝트 번호의 API를 켜라고 안내해서 설정 문제처럼 읽히지만, 보고자들이 API를 다시 켜고 키를 재발급해도 달라지지 않았고 다운로드 요청에 키를 넣은 뒤에야 파일을 받았습니다.

점검 순서를 한 줄로 줄이면 이렇습니다. 다운로드 요청에 생성 때와 같은 키가 헤더로 들어가는지, 리디렉션을 따라가는지, 생성한 지 2일이 지나지 않았는지를 차례로 확인합니다.
