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

# Direct File Upload

> Upload local files directly to Rendi storage using multipart upload.

# Direct File Upload

Upload local files directly to Rendi's storage and run them through FFprobe. Bytes stream from your client straight to Rendi's storage. Files up to **5 TB** are supported.

Use direct upload for local files. If your file is already at a publicly accessible URL, use [`/files/store-file`](/api-reference/endpoint/store-file) instead — it will fetch and store the file from that URL.

## Flow

<Steps>
  <Step title="Initialize">
    Call [`POST /v1/files/init-upload`](/api-reference/endpoint/init-upload) with `filename` and `size_bytes`. You get back a `file_id`, a `part_size`, and a list of `upload_urls` (one per part).
  </Step>

  <Step title="Upload parts">
    Split your file into chunks of exactly `part_size` bytes (the last chunk can be smaller). PUT each chunk directly to its corresponding URL in `upload_urls`. Collect the `ETag` response header from each PUT. Parts can be uploaded in parallel and retried independently.
  </Step>

  <Step title="Complete">
    Call [`POST /v1/files/{file_id}/complete-upload`](/api-reference/endpoint/complete-upload) with the `{part_number, etag}` list. The uploaded bytes are now assembled into a single stored file.
  </Step>

  <Step title="FFprobe (automatic)">
    After completion, Rendi analyzes the file with FFprobe to extract media metadata (duration, dimensions, codec, mime type, bitrate, frame rate, etc.). The file's `status` transitions from `UPLOADED` to `STORED` when analysis completes. Poll [`GET /v1/files/{file_id}`](/api-reference/endpoint/get-file) to observe the transition.
  </Step>
</Steps>

If something goes wrong mid-upload, call [`POST /v1/files/{file_id}/abort-upload`](/api-reference/endpoint/abort-upload) to discard the partial data.

## Code Examples

<CodeGroup>
  ```python Python theme={null}
  import os, requests

  API = "https://api.rendi.dev/v1"
  API_KEY = "YOUR_API_KEY"
  FILE = "./my_video.mp4"

  # 1. Init
  init = requests.post(f"{API}/files/init-upload",
      headers={"X-API-Key": API_KEY},
      json={"filename": os.path.basename(FILE),
            "size_bytes": os.path.getsize(FILE)},
  ).json()

  file_id, part_size, upload_urls = init["file_id"], init["part_size"], init["upload_urls"]

  # 2. Upload each part
  parts = []
  with open(FILE, "rb") as f:
      for i, url in enumerate(upload_urls):
          chunk = f.read(part_size)
          r = requests.put(url, data=chunk)
          r.raise_for_status()
          parts.append({"part_number": i + 1, "etag": r.headers["ETag"]})

  # 3. Complete
  resp = requests.post(f"{API}/files/{file_id}/complete-upload",
      headers={"X-API-Key": API_KEY},
      json={"parts": parts},
  ).json()

  print("Uploaded:", resp["file_id"], "status:", resp["status"])
  ```

  ```typescript TypeScript theme={null}
  import { readFile, stat } from "node:fs/promises";
  import { basename } from "node:path";

  const API = "https://api.rendi.dev/v1";
  const API_KEY = "YOUR_API_KEY";
  const FILE = "./my_video.mp4";

  const size = (await stat(FILE)).size;

  // 1. Init
  const init = await fetch(`${API}/files/init-upload`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ filename: basename(FILE), size_bytes: size }),
  }).then(r => r.json());

  const { file_id, part_size, upload_urls } = init as {
    file_id: string; part_size: number; upload_urls: string[];
  };

  // 2. Upload each part
  const buf = await readFile(FILE);
  const parts: { part_number: number; etag: string }[] = [];

  for (let i = 0; i < upload_urls.length; i++) {
    const start = i * part_size;
    const chunk = buf.subarray(start, start + part_size);
    const r = await fetch(upload_urls[i], { method: "PUT", body: chunk });
    if (!r.ok) throw new Error(`part ${i + 1} failed: ${r.status}`);
    parts.push({ part_number: i + 1, etag: r.headers.get("ETag")! });
  }

  // 3. Complete
  const done = await fetch(`${API}/files/${file_id}/complete-upload`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ parts }),
  }).then(r => r.json());

  console.log("Uploaded:", done.file_id, "status:", done.status);
  ```

  ```javascript JavaScript (Node.js) theme={null}
  const fs = require("node:fs/promises");
  const path = require("node:path");

  const API = "https://api.rendi.dev/v1";
  const API_KEY = "YOUR_API_KEY";
  const FILE = "./my_video.mp4";

  (async () => {
    const size = (await fs.stat(FILE)).size;

    // 1. Init
    const init = await fetch(`${API}/files/init-upload`, {
      method: "POST",
      headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ filename: path.basename(FILE), size_bytes: size }),
    }).then(r => r.json());

    const { file_id, part_size, upload_urls } = init;

    // 2. Upload each part
    const buf = await fs.readFile(FILE);
    const parts = [];
    for (let i = 0; i < upload_urls.length; i++) {
      const chunk = buf.subarray(i * part_size, (i + 1) * part_size);
      const r = await fetch(upload_urls[i], { method: "PUT", body: chunk });
      if (!r.ok) throw new Error(`part ${i + 1} failed: ${r.status}`);
      parts.push({ part_number: i + 1, etag: r.headers.get("ETag") });
    }

    // 3. Complete
    const done = await fetch(`${API}/files/${file_id}/complete-upload`, {
      method: "POST",
      headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ parts }),
    }).then(r => r.json());

    console.log("Uploaded:", done.file_id, "status:", done.status);
  })();
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "io"
      "net/http"
      "os"
      "path/filepath"
  )

  const API = "https://api.rendi.dev/v1"
  const APIKey = "YOUR_API_KEY"
  const File = "./my_video.mp4"

  type initResp struct {
      FileID     string   `json:"file_id"`
      PartSize   int64    `json:"part_size"`
      UploadURLs []string `json:"upload_urls"`
  }
  type part struct {
      PartNumber int    `json:"part_number"`
      ETag       string `json:"etag"`
  }

  func main() {
      fi, _ := os.Stat(File)

      // 1. Init
      body, _ := json.Marshal(map[string]any{"filename": filepath.Base(File), "size_bytes": fi.Size()})
      req, _ := http.NewRequest("POST", API+"/files/init-upload", bytes.NewReader(body))
      req.Header.Set("X-API-Key", APIKey)
      req.Header.Set("Content-Type", "application/json")
      resp, _ := http.DefaultClient.Do(req)
      var init initResp
      json.NewDecoder(resp.Body).Decode(&init)
      resp.Body.Close()

      // 2. Upload each part
      f, _ := os.Open(File)
      defer f.Close()
      parts := []part{}
      for i, url := range init.UploadURLs {
          chunk := make([]byte, init.PartSize)
          n, _ := io.ReadFull(f, chunk)
          putReq, _ := http.NewRequest("PUT", url, bytes.NewReader(chunk[:n]))
          putResp, err := http.DefaultClient.Do(putReq)
          if err != nil || putResp.StatusCode >= 300 {
              panic(fmt.Sprintf("part %d failed", i+1))
          }
          parts = append(parts, part{PartNumber: i + 1, ETag: putResp.Header.Get("ETag")})
          putResp.Body.Close()
      }

      // 3. Complete
      body, _ = json.Marshal(map[string]any{"parts": parts})
      req, _ = http.NewRequest("POST", API+"/files/"+init.FileID+"/complete-upload", bytes.NewReader(body))
      req.Header.Set("X-API-Key", APIKey)
      req.Header.Set("Content-Type", "application/json")
      resp, _ = http.DefaultClient.Do(req)
      var done map[string]any
      json.NewDecoder(resp.Body).Decode(&done)
      resp.Body.Close()
      fmt.Println("Uploaded:", done["file_id"], "status:", done["status"])
  }
  ```

  ```bash cURL theme={null}
  # 1. Init — save file_id, part_size, upload_urls from the response
  curl -sX POST https://api.rendi.dev/v1/files/init-upload \
    -H "X-API-Key: $RENDI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"filename": "my_video.mp4", "size_bytes": 104857600}'

  # 2. Upload each part (part 1 shown; repeat for each URL in upload_urls)
  # Split your file first: `split -b <part_size> my_video.mp4 chunk_`
  curl -sX PUT "<upload_urls[0]>" --data-binary @chunk_aa -D headers.txt
  # The ETag header from headers.txt goes into the parts array below

  # 3. Complete
  curl -sX POST https://api.rendi.dev/v1/files/<file_id>/complete-upload \
    -H "X-API-Key: $RENDI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"parts": [{"part_number": 1, "etag": "<etag-from-part-1>"}, ...]}'
  ```
</CodeGroup>

## Optional Parameters on init-upload

* `is_private: true` — file lands in [private storage](/private-files). Requires downloading via presigned URL.
* `part_size: <bytes>` — override the default part size. The server clamps to the valid range (5 MB minimum, 5 GB maximum) and to whatever's needed to fit within 10,000 parts. The actually-used value is returned in `part_size` on the response.

## Constraints

| Limit                    | Value                   |
| ------------------------ | ----------------------- |
| Max file size            | 5 TB                    |
| Min part size            | 5 MB (last part exempt) |
| Max part size            | 5 GB                    |
| Max parts                | 10,000                  |
| Presigned upload-URL TTL | 6 hours                 |

## Errors

* **`400` on complete-upload**: the upload session has expired or was aborted. Start a new upload with `init-upload`.
* **`400` on complete-upload / abort-upload**: file is not in an upload-in-progress state (already completed, aborted, or the row is a different kind of file).
* **`403` on init-upload**: your account plan does not support uploads (trial), or private storage was requested on a trial plan.
* **`403` on init-upload**: `size_bytes` would exceed your storage quota.
