---
title: "Request a file upload URL"
method: POST
path: "/v1/uploads"
tags: ["uploads"]
---

# Request a file upload URL

`POST /v1/uploads`

## Request

- Base URL: `https://api.framelane.io`
- URL: `https://api.framelane.io/v1/uploads`
- Auth: HTTP bearer

Returns a GCS resumable upload URL and the corresponding CDN source URL. PUT your file bytes directly to `upload_url` with the correct `Content-Type` header. Once the upload completes, pass `source_url` as the `source_url` field in any render or task request.

The `upload_url` expires in **1 hour**. If your network refuses `storage.googleapis.com`, `first_party_upload_url` is the same session on a Framelane hostname; PUT there instead.

Alternatively, send the file's bytes as base64 in `content_base64` and this one call stores them: the response comes back with `uploaded: true` and a null `upload_url`, and `source_url` works immediately. That route needs no outbound network access of your own — the reason it exists — but it is capped at 256 KB, so it suits logos and icons rather than video.

## Request body

- UploadRequest
  - `content_type` string, required: MIME type of the file you will upload.
  - `filename` string, nullable: The file's original name. Sets the stored object's extension and is kept as the asset's display name; without it the asset shows as its uuid key.
  - `duration` number, nullable: Optional client-probed media duration in seconds — **video and audio only**; ignored for images, which have no duration. When provided, the asset is immediately usable/listable instead of waiting on server-side metadata extraction; the server probe (when it runs) overrides it.
  - `width` integer, nullable: Optional client-probed pixel width (video or image).
  - `height` integer, nullable: Optional client-probed pixel height (video or image).
  - `content_base64` string, nullable: The file's bytes, base64-encoded, to store in this same call. Use it when you cannot reach `storage.googleapis.com` — a sandboxed agent whose egress is allow-listed per host, typically. The response then carries `uploaded: true` and a null `upload_url`, and `source_url` is usable right away. Capped at 256 KB; larger files must use `upload_url`. A `data:` prefix, line wrapping and the URL-safe alphabet are all accepted.

## Response `201`

Successful Response

- UploadOut: Response from POST /v1/uploads.
  - `upload_url` string, nullable: GCS resumable upload URL. PUT your file bytes here with the correct Content-Type. **null** when you sent `content_base64`: the bytes are already stored, so there is no slot left to fill.
  - `source_url` string, required: CDN URL for the uploaded file. Pass this as source_url in render/task requests.
  - `expires_at` string, date-time, required: When the upload_url expires (1 hour from now).
  - `uploaded` boolean: True when this call also stored the file (you sent `content_base64`), so `source_url` is usable immediately. False means only a slot was minted and the bytes are still yours to PUT.
  - `asset_id` string, nullable: The media asset registered for this upload — poll it with `GET /v1/workspace/assets/{id}`. Null for fonts, which are storable but are not timeline media and so get no asset row.
  - `first_party_upload_url` string, nullable: The same upload session as `upload_url`, on a Framelane hostname. Use it when your network allows `*.framelane.io` but not `storage.googleapis.com` — same protocol, same chunks, same expiry. Null when you sent `content_base64`, or when this deployment has no first-party upload host.

## Other responses

- `401`: Missing or invalid API key.
- `413`: content_base64 exceeds the inline upload limit.
- `422`: Unsupported content_type, or undecodable content_base64.

---

[API](https://skmtc.dev/framelane/apis/framelane-api.md) · [All operations](https://skmtc.dev/framelane/apis/framelane-api/llms.txt) · [OpenAPI document](https://skmtc.dev/framelane/apis/framelane-api/revisions/a223001ca8e5?raw)
