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

# Uploads

> Send a file in parts with signed links, or let us download it from a link.

A dub starts from an upload. Files never pass through the API itself: you send them straight to storage, or we fetch them for you. Video and audio files are both accepted, and an audio file is dubbed as audio. Your plan's file size limit is checked when the upload starts and again when it completes.

An upload can be dubbed once. To dub the same file again, upload it again.

## Uploading in parts

**1. Start the upload**

`POST /uploads` with the file's `filename` (with its extension) and its `size` in bytes. The answer gives the `part_size`, the `part_count`, and signed links for the first parts.

**2. PUT each part**

Send bytes `(n - 1) × part_size` up to `n × part_size` of the file to part `n`'s link, and keep the `ETag` header of each answer. Parts can go in any order and in parallel.

**3. Ask for more links when you need them**

The first answer has links for up to 100 parts, and each link lasts 15 minutes. `POST /uploads/{id}/parts` with `{"part_numbers": [101, 102, …]}` signs up to 100 more at a time. Ask again for any link that expired.

**4. Complete the upload**

`POST /uploads/{id}/complete` with every part number and its ETag. The upload becomes `uploaded`.

Parts are at least 16 MiB, and a file has at most 1,000 of them, so larger files get larger parts. Use the `part_size` we send rather than choosing your own.

```javascript Node.js theme={null}
import { open, stat } from "node:fs/promises";
import { basename } from "node:path";

const API = "https://api.reedapt.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.REEDAPT_API_KEY}`,
  "Content-Type": "application/json",
};

async function post(path, body) {
  const res = await fetch(API + path, { method: "POST", headers, body: JSON.stringify(body) });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
  return json;
}

export async function uploadFile(path) {
  const { size } = await stat(path);
  const upload = await post("/uploads", { filename: basename(path), size });
  const links = new Map(upload.parts.map((p) => [p.part_number, p.url]));
  const file = await open(path);
  const parts = [];
  try {
    for (let n = 1; n <= upload.part_count; n++) {
      if (!links.has(n)) {
        const last = Math.min(n + 99, upload.part_count);
        const part_numbers = Array.from({ length: last - n + 1 }, (_, i) => n + i);
        const more = await post(`/uploads/${upload.id}/parts`, { part_numbers });
        for (const p of more.parts) links.set(p.part_number, p.url);
      }
      const start = (n - 1) * upload.part_size;
      const length = Math.min(upload.part_size, size - start);
      const { buffer } = await file.read(Buffer.alloc(length), 0, length, start);
      // No API key here: the signed link carries its own permission.
      const res = await fetch(links.get(n), { method: "PUT", body: buffer });
      if (!res.ok) throw new Error(`Part ${n} failed with ${res.status}`);
      parts.push({ part_number: n, etag: res.headers.get("etag") });
    }
  } finally {
    await file.close();
  }
  return post(`/uploads/${upload.id}/complete`, { parts });
}
```

```python Python theme={null}
import os
import requests

API = "https://api.reedapt.com/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['REEDAPT_API_KEY']}"


def post(path, body):
    res = session.post(API + path, json=body, timeout=60)
    if not res.ok:
        error = res.json()["error"]
        raise RuntimeError(f"{error['code']}: {error['message']}")
    return res.json()


def upload_file(path):
    size = os.path.getsize(path)
    upload = post("/uploads", {"filename": os.path.basename(path), "size": size})
    links = {p["part_number"]: p["url"] for p in upload["parts"]}
    parts = []
    with open(path, "rb") as f:
        for n in range(1, upload["part_count"] + 1):
            if n not in links:
                numbers = list(range(n, min(n + 100, upload["part_count"] + 1)))
                more = post(f"/uploads/{upload['id']}/parts", {"part_numbers": numbers})
                links.update({p["part_number"]: p["url"] for p in more["parts"]})
            f.seek((n - 1) * upload["part_size"])
            # requests.put, not the session: the signed link carries its own permission.
            res = requests.put(links[n], data=f.read(upload["part_size"]), timeout=600)
            res.raise_for_status()
            parts.append({"part_number": n, "etag": res.headers["ETag"]})
    return post(f"/uploads/{upload['id']}/complete", {"parts": parts})
```

> [!WARNING]
>
> Don't send your API key to the signed links. An `Authorization` header there makes storage refuse the part.

## Uploading from a link

If the file is already online, send its address instead of a filename and size:

```bash theme={null}
curl https://api.reedapt.com/v1/uploads \
  -H "Authorization: Bearer $REEDAPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source_url": "https://example.com/media/talk.mp4"}'
```

The answer is `202` with the upload in `fetching`. We download the file and the upload becomes `uploaded`, or `failed` with an `error` saying why. Check it with `GET /uploads/{id}` every 10 to 30 seconds, then start the dubs as usual.

* The link must be `http` or `https` on the standard ports and reachable from the internet. Links to private networks are refused.
* If the link doesn't end in a file name, add `filename` with the right extension.
* Each account can have 3 links downloading at once. Past that you get `429 TOO_MANY_FETCHES`.
* A download that is very slow or stalls fails with `SOURCE_URL_TOO_SLOW`. Host the file somewhere faster, or upload it in parts.

## Upload statuses

| Status | Meaning |
| - | - |
| `uploading` | Waiting for its parts and the call to complete. |
| `fetching` | We're downloading it from `source_url`. |
| `uploaded` | Ready to dub. |
| `used` | Already dubbed. Upload the file again to dub it again. |
| `failed` | The download from `source_url` didn't work. `error` says why. |

Uploads that are never completed expire. If completing one gives `UPLOAD_GONE`, start a new upload.


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