> ## Documentation Index
> Fetch the complete documentation index at: https://docs.framelane.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Uploading media

> Upload local files to Framelane before using them in renders or tasks.

Most renders and tasks reference media via `source_url`. You have three options:

1. **Public URL you control** — host the file yourself (S3, GCS, CDN, etc.) and pass that URL directly in render/task requests. The renderer fetches it as-is. For video/audio referenced this way you must set `out_point` (Framelane can't probe the duration of a file it doesn't host).
2. **Framelane upload** — upload a local file with `POST /v1/uploads`, then use the returned `source_url`.
3. **Auto-ingest a public URL** — set `ingest_external: true` on a render and Framelane copies any non-Framelane `source_url` into your storage before rendering. See [Importing public URLs](#importing-public-urls) below.

This page covers **local uploads** (option 2) and **auto-ingest** (option 3).

## Upload flow

```
POST /v1/uploads  →  PUT bytes to upload_url  →  use source_url in render/task
```

### 1. Request an upload URL

```bash theme={null}
curl -X POST https://api.framelane.io/v1/uploads \
  -H "Authorization: Bearer $FRAMELANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content_type": "video/mp4",
    "filename": "my-clip.mp4"
  }'
```

Response (`201 Created`):

```json theme={null}
{
  "upload_url": "https://storage.googleapis.com/...",
  "source_url": "https://cdn-user.framelane.io/uploads/ws_01J.../a1b2c3.mp4",
  "expires_at": "2026-05-29T08:00:00Z"
}
```

The `upload_url` expires in **1 hour**.

### 2. Upload the file bytes

PUT the entire file to `upload_url` with the same `Content-Type` you declared in step 1. Bytes go directly to GCS — they never pass through the Framelane API.

```bash theme={null}
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @my-clip.mp4
```

Use a single PUT request (not multipart form upload).

### 3. Use `source_url` in your job

Pass `source_url` from the upload response as `source_url` in any render element or task body:

```json theme={null}
{
  "type": "video",
  "id": "clip1",
  "source_url": "https://cdn-user.framelane.io/uploads/ws_01J.../a1b2c3.mp4"
}
```

For video and audio uploads, Framelane extracts metadata (duration, codecs) asynchronously after the PUT completes. You can submit a render immediately; if metadata is not ready yet, the job stays queued until it is.

## Importing public URLs

If a render references a `source_url` that Framelane doesn't host, set `ingest_external: true` on the render and Framelane copies each such file into your storage **before** rendering. This gives you the reliability and metadata of a Framelane-hosted file (including automatic `out_point` for video/audio) without a separate upload step.

```json theme={null}
{
  "ingest_external": true,
  "elements": [
    {
      "type": "video",
      "id": "clip1",
      "source_url": "https://example.com/clip.mp4",
      "time": 0
    }
  ]
}
```

How it behaves:

* The render is created in the **`ingesting`** state and is **not** sent to the render engine yet.
* Framelane copies each non-Framelane source file into your storage. You receive an [`asset.ready`](/webhooks/events) webhook per file as it completes.
* Once every imported file is ready, the render automatically transitions to `queued` and proceeds. `out_point` is filled in from the imported file's duration when omitted.
* If a file can't be imported (unreachable, too large, or unsupported), the render fails with the [`ingest_failed`](/introduction/errors) error code.

Files already hosted by Framelane (a `cdn-user.framelane.io` `source_url` from `POST /v1/uploads`) are never re-copied. Omit `ingest_external` (or set it to `false`) to keep the legacy pass-through behavior described in option 1 above.

## Supported content types

| Category | MIME types                                                                                        |
| -------- | ------------------------------------------------------------------------------------------------- |
| Video    | `video/mp4`, `video/quicktime`, `video/webm`, `video/x-msvideo`, `video/x-matroska`, `video/mpeg` |
| Audio    | `audio/mpeg`, `audio/mp4`, `audio/wav`, `audio/x-wav`, `audio/aac`, `audio/flac`, `audio/ogg`     |
| Image    | `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `image/tiff`                                |
| Font     | `font/ttf`, `font/otf`, `font/woff`, `font/woff2`                                                 |

Unsupported `content_type` values return `422` with an `unsupported_content_type` error.

## Errors

| Code                       | Cause                             |
| -------------------------- | --------------------------------- |
| `unsupported_content_type` | MIME type not in the allowed list |
| `401`                      | Missing or invalid API key        |
