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 the X-Upload-Single-Use note 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 URLPUT 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.mp4

A 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_uri is 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: off to 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.