Skip to main content

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 instead — it will fetch and store the file from that URL.

Flow

1

Initialize

Call POST /v1/files/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).
2

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.
3

Complete

Call POST /v1/files/{file_id}/complete-upload with the {part_number, etag} list. The uploaded bytes are now assembled into a single stored file.
4

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} to observe the transition.
If something goes wrong mid-upload, call POST /v1/files/{file_id}/abort-upload to discard the partial data.

Code Examples

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"])
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);
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);
})();
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"])
}
# 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>"}, ...]}'

Optional Parameters on init-upload

  • is_private: true — file lands in private storage. 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

LimitValue
Max file size5 TB
Min part size5 MB (last part exempt)
Max part size5 GB
Max parts10,000
Presigned upload-URL TTL6 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.