Large file upload (visual understanding)
POST /v1/uploads · POST /v1/uploads/{id}/complete · GET /v1/uploads/{id} · DELETE /v1/uploads/{id}
For large-file visual understanding with Gemini multimodal models (video / image / audio / PDF), material larger than about 15 MB cannot be sent inline as base64 (the whole request body is capped at 20 MB), and a public https:// URL only gets its first ~15 MB read by the upstream (anything beyond that is silently dropped, with no error). This endpoint solves that: upload the large file to our storage first, get back a gs:// reference, then use it as the file_data.file_uri of the Gemini Native API — the upstream can then understand the entire file.
Three steps: ① create an upload task to get a direct-upload URL → ② upload the file directly → ③ complete and retrieve the gs:// reference.
Single use, deleted after use: the
gs://reference can only be used once. After the first successful Gemini request that references it, the file is deleted automatically. To reference the same file across multiple turns, see theX-Upload-Single-Usenote in section 4.The file uploads directly to our storage, not through the gateway: the bytes in step ② go to a presigned direct-upload URL — it does not consume your API quota and is not subject to request-body size limits.
Validity: the upload URL (steps ①→②) is valid for 1 hour by default; a completed
gs://reference that is never used is automatically cleaned up after about 24 hours.
1. Create an upload task: POST /v1/uploads
| Field | Type | Required | Description |
|---|---|---|---|
size_bytes |
int | Yes | The file's exact byte count (it is signed into the upload URL; a size mismatch on upload is rejected). |
mime_type |
string | Yes | The file's MIME type. Supports video/*, image/*, audio/*, application/pdf. |
curl https://api.portal.aiin1.ai/v1/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "size_bytes": 33852748, "mime_type": "video/mp4" }'Response
{
"upload_id": "upload_2b9574243b714a13a9fceda4c152f314",
"upload_url": "https://<account>.r2.cloudflarestorage.com/...(presigned direct-upload URL)...",
"upload_method": "PUT",
"upload_headers": { "Content-Type": "video/mp4", "Content-Length": "33852748" },
"upload_expires_at": "2026-07-23T13:10:38+00:00",
"single_use": true
}| Field | Description |
|---|---|
upload_id |
The upload-task ID, used for complete / query / delete. |
upload_url |
The presigned direct-upload URL — PUT the file here in step 2. |
upload_headers |
Headers you must send verbatim on upload (they are signed into the URL; a mismatch is rejected). |
upload_expires_at |
When the upload URL expires (1 hour by default). |
2. Upload the file: PUT to upload_url
Using the upload_url and upload_headers from the previous step, PUT the file bytes. This step goes straight to storage, not through the API gateway:
curl -X PUT "<the upload_url from the previous step>" \
-H "Content-Type: video/mp4" \
--data-binary @my-video.mp4A successful upload returns 200 (no body). Content-Type must match the mime_type you created with, and the number of bytes uploaded must equal size_bytes, otherwise storage rejects it.
3. Complete and retrieve the reference: POST /v1/uploads/{id}/complete
After the file is uploaded, call complete. It verifies the file was uploaded at the correct size, transfers it into a gs:// reference the upstream can read, waits synchronously for the transfer to finish (usually a few seconds for 200 MB), then returns:
curl -X POST https://api.portal.aiin1.ai/v1/uploads/upload_2b9574243b714a13a9fceda4c152f314/complete \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'Response
{
"upload_id": "upload_2b9574243b714a13a9fceda4c152f314",
"file_uri": "gs://.../30367d4d.../upload_2b9574243b714a13a9fceda4c152f314",
"uri_type": "gcs",
"size_bytes": 33852748,
"single_use": true
}| Field | Description |
|---|---|
file_uri |
The reference to paste into the Gemini request's file_data.file_uri (see section 4). |
uri_type |
The reference type; currently always gcs. |
complete is idempotent: once the file is ready, calling it again returns the same file_uri; while the transfer is still in progress it returns 409 (retry after a brief wait).
4. Use it in the Gemini Native API
Put the file_uri into the file_data of a Gemini Native API request (make sure mime_type matches the file):
curl https://api.portal.aiin1.ai/v1beta/models/gemini-3.1-pro-preview:generateContent \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contents": [{
"parts": [
{"text": "Describe this video in detail."},
{"file_data": {"mime_type": "video/mp4", "file_uri": "gs://.../upload_2b9574243b714a13a9fceda4c152f314"}}
]
}]
}'Single use: once the request above returns successfully, the file behind
file_uriis deleted automatically, and referencing it again will fail.Multi-turn reuse: to reference the same file across multiple turns, add the header
X-Upload-Single-Use: offto the Gemini request — the file is then not deleted immediately after being referenced, and is cleaned up when its validity (about 24 hours) expires instead.
5. Query and delete (optional)
Query status by upload_id:
curl https://api.portal.aiin1.ai/v1/uploads/upload_2b9574243b714a13a9fceda4c152f314 \
-H "Authorization: Bearer YOUR_API_KEY"{ "upload_id": "upload_2b9574243b714a13a9fceda4c152f314", "status": "ready", "file_uri": "gs://.../upload_2b9574243b714a13a9fceda4c152f314", "uri_type": "gcs", "size_bytes": 33852748, "mime_type": "video/mp4", "created_at": "2026-07-23T12:10:38+00:00", "ready_at": "2026-07-23T12:11:02+00:00" }status values: pending_upload (awaiting upload) / copying (transferring) / ready (usable) / consumed (already referenced) / deleted / expired / copy_failed.
Delete explicitly (usually unnecessary with single-use):
curl -X DELETE https://api.portal.aiin1.ai/v1/uploads/upload_2b9574243b714a13a9fceda4c152f314 \
-H "Authorization: Bearer YOUR_API_KEY"{ "upload_id": "upload_2b9574243b714a13a9fceda4c152f314", "deleted": true }An upload_id that is not your org's always returns 404.
6. Common errors
| HTTP | Situation | Description |
|---|---|---|
| 400 | Invalid size_bytes / mime_type |
size_bytes must be a positive integer within the limit; mime_type supports only video/* image/* audio/* application/pdf. |
| 400 | File not uploaded at complete |
Finish the PUT in step 2 before calling complete. |
| 400 | Size mismatch | The uploaded byte count differs from the size_bytes you created with. |
| 404 | Upload task not found / not your org | Wrong upload_id, or it does not belong to your organization. |
| 404 | Feature not enabled | Large-file upload is not enabled on your organization's line; contact support. |
| 409 | Transfer in progress | complete is still transferring; retry after Retry-After seconds. |
| 410 | Reference no longer valid | The file was already referenced (single use), deleted, or expired. |
| 429 | Daily quota exceeded | Your organization reached its daily limit for created upload tasks. |