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

> ## Agent Instructions
> Leap's API is at https://api.tryleap.ai. Send the key in the x-api-key header or as Authorization: Bearer; keys start with leap_.
> Take model IDs and input schemas from GET /v1/models/{creator}/{name}, or from https://api.tryleap.ai/v1/public/models without a key. Never guess an input field: unknown fields are a 400.
> Model inputs take uploaded file IDs (file_...), never URLs.
> To give a coding agent the whole API, install the Leap skill: curl -fsSL https://www.tryleap.ai/install.sh | sh
> The full OpenAPI 3.1 spec is at https://www.tryleap.ai/docs/openapi.json.

# Files and uploads

> Upload photos, videos and sounds, pass their file IDs as model inputs, and keep an output as a file to feed the next run.

Models that edit, animate or listen take files as input, by ID. Upload the file first, then put its `id` (`file_...`) in the input field the model's [input schema](/docs/models#read-an-input-schema) names, such as `photo`, `first_frame` or `images`. A field that takes a file has the pattern `^file_[0-9A-Za-z]{24}$`. Models don't take URLs.

## Upload an image

Send the image itself as the request body. There's no multipart encoding:

```bash theme={"theme":"css-variables"}
FILE_ID=$(curl -s "https://api.tryleap.ai/v1/files?filename=grandma-1962.jpg" \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "content-type: image/jpeg" \
  --data-binary @grandma-1962.jpg \
  | jq -r .id)
```

```json theme={"theme":"css-variables"}
{
  "id": "file_3Kd9Qm2Xr7Lp4Vn8Tc1Wb6Ys",
  "object": "file",
  "content_type": "image/jpeg",
  "bytes": 1843210,
  "width": 1200,
  "height": 1600,
  "duration": null,
  "filename": "grandma-1962.jpg",
  "url": "https://api.tryleap.ai/files/eyJrIjoibWVkaWEv...",
  "created_at": "2026-10-04T18:20:03.441Z"
}
```

`POST /v1/files` takes PNG, JPEG or WebP images up to 4 MB. The type is read from the file's bytes, not the header. `filename` is optional. Videos and sounds go through [direct uploads](#upload-a-video-or-a-sound), whatever their size.

To make a larger photo fit, resize it first, for example to 2048 pixels on its long side. On macOS, `sips -Z 2048 photo.jpg` does it in place; in code, use Pillow in Python or sharp in Node.js.

Then pass the ID:

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/generations \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "content-type: application/json" \
  -H "prefer: wait=60" \
  -d "{\"model\": \"leap/restore-photo@1\", \"input\": {\"photo\": \"$FILE_ID\", \"color\": \"colorize\"}}"
```

## Upload a video or a sound

Videos and sounds go straight to storage in three steps: ask for an upload URL, `PUT` the file there, then complete the upload. The upload becomes a file with the same ID.

<CodeGroup>
  ```ts TypeScript theme={"theme":"css-variables"}
  import { readFile } from "node:fs/promises";

  async function leap(path: string, body: unknown) {
    const response = await fetch(`https://api.tryleap.ai${path}`, {
      method: "POST",
      headers: { "x-api-key": process.env.LEAP_API_KEY!, "content-type": "application/json" },
      body: JSON.stringify(body),
    });
    const json = await response.json();
    if (!response.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
    return json;
  }

  const bytes = await readFile("interview.mp4");

  // 1. Ask for an upload URL for this exact type and size.
  const upload = await leap("/v1/uploads", {
    content_type: "video/mp4",
    bytes: bytes.length,
    filename: "interview.mp4",
  });

  // 2. PUT the file there, with exactly the headers it lists, within the hour.
  const put = await fetch(upload.upload_url, {
    method: upload.upload_method,
    headers: upload.upload_headers,
    body: bytes,
  });
  if (!put.ok) throw new Error(`Upload failed: ${put.status}`);

  // 3. Complete it. The answer is the file.
  const file = await leap(`/v1/uploads/${upload.id}/complete`, {});
  console.log(file.id, file.duration);
  ```

  ```python Python theme={"theme":"css-variables"}
  import os

  import requests

  API = "https://api.tryleap.ai"
  HEADERS = {"x-api-key": os.environ["LEAP_API_KEY"]}


  def leap(path, body):
      response = requests.post(f"{API}{path}", headers=HEADERS, json=body)
      data = response.json()
      if not response.ok:
          raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
      return data


  with open("interview.mp4", "rb") as f:
      content = f.read()

  # 1. Ask for an upload URL for this exact type and size.
  upload = leap("/v1/uploads", {"content_type": "video/mp4", "bytes": len(content), "filename": "interview.mp4"})

  # 2. PUT the file there, with exactly the headers it lists, within the hour.
  put = requests.request(upload["upload_method"], upload["upload_url"], headers=upload["upload_headers"], data=content)
  put.raise_for_status()

  # 3. Complete it. The answer is the file.
  file = leap(f"/v1/uploads/{upload['id']}/complete", {})
  print(file["id"], file["duration"])
  ```
</CodeGroup>

The upload answer:

```json theme={"theme":"css-variables"}
{
  "id": "file_8Vb2Nc5Xm1Qz7Lk4Rt9Wd3Ph",
  "object": "upload",
  "content_type": "video/mp4",
  "bytes": 48213992,
  "filename": "interview.mp4",
  "upload_url": "https://...",
  "upload_method": "PUT",
  "upload_headers": { "x-content-type": "video/mp4" },
  "expires_at": "2026-10-04T19:20:03.441Z"
}
```

| Limit | |
| - | - |
| Types | Video: MP4, MOV, WebM. Audio: MP3, WAV, M4A. |
| Size | Up to 1 GB of video or 500 MB of audio. `bytes` must be the file's exact size. |
| Length | Half a second to 4 hours. |
| Upload URL | Valid for one hour. An upload that's never completed is deleted a day after its URL expires. |
| Per workspace | 5 GB of uploads a day, and 10 uploads in flight at once. |
| Balance | Uploading a video or a sound needs credit on your balance. Without it, the request gets a `402`. |

`content_type` takes one of these values:

| Kind | `content_type` |
| - | - |
| Video | `video/mp4`, `video/quicktime`, `video/webm` |
| Audio | `audio/mpeg`, `audio/mp3`, `audio/wav`, `audio/x-wav`, `audio/wave`, `audio/mp4`, `audio/x-m4a` |

Completing an upload reads the file's type and length from its bytes, not from the `content_type` you declared. If the bytes aren't an MP4, MOV or WebM video or an MP3, WAV or M4A sound, or the file breaks a limit above, completing gets a `400` with `param` set to `file`, and the bytes are deleted, so start a new upload. Completing before the file has arrived is a `404`. Completing the same upload again returns the same file.

`bytes` counts toward the day's 5 GB when you start the upload, whether or not you complete it. An upload stops counting toward the 10 in flight once it's completed, refused or past its URL's hour.

## Read a file

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/files/$FILE_ID -H "x-api-key: $LEAP_API_KEY"
```

Returns the file with a fresh `url`, valid for 24 hours.

## Feed one run into the next

Output links expire after 24 hours, but you don't need to download and upload an image to use it again. Turn an output into a file of your workspace and pass its ID:

```bash theme={"theme":"css-variables"}
curl -X POST https://api.tryleap.ai/v1/generations/$GENERATION_ID/outputs/0/file -H "x-api-key: $LEAP_API_KEY"
```

The `0` is the output's index. This works for the image outputs of a succeeded generation; anything else gets a `400`. Calling it again returns the same file.

For example, make a still with an image model, keep it, then animate it with `google/veo-3.1-fast` by passing the file ID as `first_frame`. [Turn a photo into a video](/docs/examples/photo-to-video) does the same with a photo you upload, in one script.

## How long files last

The outputs of your runs are kept as long as your workspace exists. Only their links expire: read the generation or the file again for a fresh one.

Files you upload are kept too, for now. If we ever set a limit on how long they're kept, we'll announce it before it applies.

A direct upload that's never completed is deleted a day after its URL expires. If the URL expired before your file arrived, start a new upload.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.