Try it

Pick a file once — Create session and PUT upload use the same name, type, and size.

Upload file
No file chosen

Required for upload session and CDN PUT examples.

Guides

Server-side upload

Upload files through your backend: the client sends bytes to your API; your server forwards them to Reupload with POST /uploads/direct. Works with any server language (Node, Python, PHP, Go, Java, etc.) — implement the Reupload API below, then expose your own upload route to your app.

No signed CDN URL and no browser PUT to storage. Compare with the CDN path in React/Next + Node.js.

Architecture

Browser

React / Next

Your API

Node.js

Reupload

API

POST file (multipart)
POST /uploads/direct
202 uploadId, fileId
fileId, status
RequestResponse

There are two APIs in play:

APIWho calls itYou implement
Your upload API (e.g. POST /uploads)Web, mobile, or desktop appAccept multipart from clients; auth your users
Reupload API (POST /uploads/direct)Your server onlyForward file bytes with your ru_ API key — never expose the key to clients

Prerequisites

  • Reupload API key with files.write and a project UUID — sign up, Authentication
  • A backend that can send multipart/form-data HTTP requests
  • (Optional) A web or mobile client that posts files to your backend

Reupload API

This is the contract your server must implement when talking to Reupload. Full reference: Server-side uploads (direct), API reference.

Base URL

base URL
https://api.reupload.dev/api/v1

Authentication

Every Reupload request from your server uses a Bearer API key. Requires files.write for direct upload.

Authorization
Authorization: Bearer ru_xxxxxxxx

Store the key in server environment variables only. See Authentication.

Direct upload — single file

Value
MethodPOST
Path/uploads/direct
Content-Typemultipart/form-data
Success status202 Accepted

Form fields

FieldRequiredDescription
projectIdYesTarget project UUID
fileYesFile bytes; set each part's Content-Type to the file MIME type
filenameNoStorage path such as docs/report.pdf (folders created under the project). Defaults to the multipart filename. Single-file only — do not use when sending multiple file parts.

Example request

curl — single file
curl -s -X POST 'https://api.reupload.dev/api/v1/uploads/direct' \
  -H 'Authorization: Bearer ru_...' \
  -F 'projectId=your-project-uuid' \
  -F 'filename=docs/report.pdf' \
  -F 'file=@./report.pdf;type=application/pdf'
Request
curl -s -X POST 'https://api.reupload.dev/api/v1/uploads/direct' \
  -H 'Authorization: Bearer ru_your_api_key_here' \
  -F 'projectId=00000000-0000-4000-8000-000000000001' \
  -F 'filename=your-file.pdf' \
  -F 'file=@./your-file.pdf;type=application/pdf'

Select a file in the Try it panel — session and upload will use the same file metadata.

Example response (one file)

202 Accepted
{
  "uploadId": "uuid",
  "fileId": "uuid",
  "status": "processing"
}

status is processing until background work finishes. Use session poll or webhooks below — there is no separate complete step for direct upload.

Why the response is 202 + processing

It is normal to expect a direct upload to return the full file record immediately — your server already sent the bytes in the same request. Reupload intentionally separates stored in object storage from ready to use.

What already happened when you get 202

  • File bytes were written to storage
  • Declared size and content type were verified against the stored object
  • You received stable identifiers: uploadId and fileId

You are not being asked to upload again or call a separate /complete step (that step is internal for direct upload).

What still runs in the background

The same processing pipeline as the CDN upload flow (create session → PUT → complete):

  • Malware scanning
  • Finalizing the file record (status = COMPLETED)
  • Usage and quota accounting
  • Webhooks file.uploaded with path, url, sizeBytes, and urlExpiresAt

Until that finishes, treat the file as pending. GET /files/:fileId returns the file only after processing completes.

Why not return url / path in the upload response?

  • Safety — processing can fail (for example malware rejection). A URL in the upload response would imply the file is safe to serve before that check finishes.
  • One integration model — CDN and direct uploads share one pipeline, so polling and webhooks work the same way regardless of how bytes were sent.
  • Fast API responses — your server is not blocked on scanning or webhook delivery; you avoid HTTP timeouts on large files.

How to get the file when it is ready

ApproachBest forYou get
Webhook file.uploadedProduction integrationspath, url, sizeBytes, urlExpiresAt
Poll GET /uploads/session/:uploadIdScripts, tests, no webhook endpointSession status; when COMPLETED, a top-level file object with the same fields as webhook data
GET /files/:fileId after completionFetching metadata from your backendFile record; use GET /files/:fileId/access for a fresh download URL

Save fileId from the 202 response — it is stable and appears in webhooks and file APIs. Typical processing completes in seconds.

Direct upload — multiple files

Repeat the file field in the same request. Do not send a global filename field — each part uses its own name. Maximum 10 files per request.

curl — multiple files
curl -s -X POST 'https://api.reupload.dev/api/v1/uploads/direct' \
  -H 'Authorization: Bearer ru_...' \
  -F 'projectId=your-project-uuid' \
  -F 'file=@./report.pdf;type=application/pdf' \
  -F 'file=@./notes.txt;type=text/plain'

Example response (two or more files)

202 Accepted
{
  "files": [
    { "uploadId": "uuid", "fileId": "uuid", "status": "processing" },
    { "uploadId": "uuid", "fileId": "uuid", "status": "processing" }
  ]
}

One file in the request still returns the single-object shape above (not { files: [...] }).

Files are processed sequentially. If a later file fails, earlier files may already be stored — there is no automatic rollback.

Get upload session (poll status)

Value
MethodGET
Path/uploads/session/:uploadId
Permissionfiles.read (or files.write)
curl
curl -s 'https://api.reupload.dev/api/v1/uploads/session/UPLOAD_ID' \
  -H 'Authorization: Bearer ru_...'
response (processing)
{
  "session": {
    "id": "uuid",
    "fileId": "uuid",
    "status": "PROCESSING",
    "expectedSize": 502400,
    "expectedMime": "application/pdf"
  }
}
response (completed)
{
  "session": {
    "id": "uuid",
    "fileId": "uuid",
    "status": "COMPLETED",
    "expectedSize": 502400,
    "expectedMime": "application/pdf"
  },
  "file": {
    "fileId": "uuid",
    "projectId": "uuid",
    "path": "projects/uuid/files/uuid/docs/report.pdf",
    "name": "report.pdf",
    "mimeType": "application/pdf",
    "sizeBytes": 502400,
    "url": "https://cdn.example.com/v1/download/...",
    "urlExpiresAt": "2026-05-30T14:35:00.000Z",
    "isPublic": false
  }
}

Poll until session.status is COMPLETED or FAILED. When completed, use the top-level file object (same shape as webhook data) instead of calling GET /files/:fileId separately. In production, prefer webhooks (file.uploaded).

Request
curl -s 'https://api.reupload.dev/api/v1/uploads/session/00000000-0000-4000-8000-000000000002' \
  -H 'Authorization: Bearer ru_your_api_key_here'

Errors and limits

  • Same supported file types, per-file size limit (default 50 MB), quotas, and rate limits as session create
  • Error bodies use message and a status code — see Errors
  • API key must have access to projectId (project-scoped keys or allProjects)

Integration steps (any language)

  1. Receive an uploaded file from your client (multipart, base64, stream — your choice).
  2. Build multipart/form-data with projectId, the file part(s), and optional filename for a single file.
  3. POST https://api.reupload.dev/api/v1/uploads/direct with Authorization: Bearer ru_....
  4. On 202, save uploadId and fileId in your database.
  5. Poll GET /uploads/session/:uploadId or wait for a webhook before treating the file as ready.

Official SDKs: @reupload/sdk (uploadDirect, uploadDirectBatch) and reupload-sdk for Python (upload_direct, upload_direct_batch). Other languages: use your HTTP client's multipart support (e.g. HttpClient in .NET, curl, etc.).

Your backend API (for clients)

Reupload does not define your app's upload route — you do. Typical pattern:

Your routePurpose
POST /uploads (example)Accept multipart/form-data from browser or mobile; return 202 + fileId after forwarding to Reupload
GET /uploads/:id/status (optional)Proxy or wrap Reupload session status so clients never hold the API key

Authenticate your users on these routes (session, JWT, API token). Only your server calls Reupload with the ru_ key.

When to use server-side upload

Use this patternUse CDN flow instead
Clients must only call your APILarge files; avoid proxying bytes through your server
No CDN / project upload CORS setupMinimize server bandwidth
Scan or transform files before ReuploadBrowser PUT straight to signed URL

Integration checklist

  1. Create API key + project; verify with GET /public/whoami (Try it on Quickstart).
  2. Confirm POST /uploads/direct from your server with curl or the Try it panel above.
  3. Implement your client-facing upload route that forwards to Reupload.
  4. Store fileId; poll session or use webhooks until COMPLETED.

Reference: Node.js + Express

Optional copy-paste example if you use Node 20+ and Express. Skip this section if you integrate from another stack using the Reupload API above.

Environment variables

.env
REUPLOAD_API_BASE_URL=https://api.reupload.dev/api/v1
REUPLOAD_API_KEY=ru_xxxxxxxx
REUPLOAD_PROJECT_ID=your-project-uuid
 
PORT=3001
CORS_ORIGIN=http://localhost:3000

Reupload client helper

reupload.js
const apiBase = process.env.REUPLOAD_API_BASE_URL.replace(/\/$/, "");
const apiKey = process.env.REUPLOAD_API_KEY;
const projectId = process.env.REUPLOAD_PROJECT_ID;
 
export async function uploadDirect({ filename, contentType, buffer }) {
  const form = new FormData();
  form.append("projectId", projectId);
  form.append("filename", filename);
  form.append(
    "file",
    new Blob([buffer], { type: contentType }),
    filename.split("/").pop() ?? "upload",
  );
 
  const res = await fetch(`${apiBase}/uploads/direct`, {
    method: "POST",
    headers: { Authorization: `Bearer ${apiKey}` },
    body: form,
  });
 
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw Object.assign(new Error(err.message ?? `Direct upload failed (${res.status})`), {
      status: res.status,
    });
  }
 
  return res.json();
}
 
export async function uploadDirectBatch(files) {
  const form = new FormData();
  form.append("projectId", projectId);
  for (const f of files) {
    form.append(
      "file",
      new Blob([f.buffer], { type: f.contentType }),
      f.filename.split("/").pop() ?? f.filename,
    );
  }
 
  const res = await fetch(`${apiBase}/uploads/direct`, {
    method: "POST",
    headers: { Authorization: `Bearer ${apiKey}` },
    body: form,
  });
 
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw Object.assign(new Error(err.message ?? `Direct upload failed (${res.status})`), {
      status: res.status,
    });
  }
 
  const body = await res.json();
  return body.files ?? [body];
}
 
export async function getUploadSession(uploadId) {
  const res = await fetch(`${apiBase}/uploads/session/${uploadId}`, {
    headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
  });
  if (!res.ok) throw new Error(`get session failed (${res.status})`);
  return res.json();
}

Express routes (your API)

server.js
import "dotenv/config";
import express from "express";
import cors from "cors";
import multer from "multer";
import { uploadDirect, uploadDirectBatch, getUploadSession } from "./reupload.js";
 
const app = express();
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 52_428_800 },
});
 
app.use(
  cors({ origin: process.env.CORS_ORIGIN ?? "http://localhost:3000", credentials: true }),
);
 
app.post("/uploads", upload.single("file"), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ message: "file is required" });
    const filename = req.body?.filename?.trim() || req.file.originalname || "upload.bin";
    const result = await uploadDirect({
      filename,
      contentType: req.file.mimetype,
      buffer: req.file.buffer,
    });
    res.status(202).json(result);
  } catch (err) {
    res.status(err.status ?? 500).json({ message: err.message });
  }
});
 
app.post("/uploads/batch", upload.array("file", 10), async (req, res) => {
  try {
    const files = req.files ?? [];
    if (files.length === 0) {
      return res.status(400).json({ message: "at least one file is required" });
    }
    const results = await uploadDirectBatch(
      files.map((f) => ({
        filename: f.originalname || "upload.bin",
        contentType: f.mimetype,
        buffer: f.buffer,
      })),
    );
    res.status(202).json(results.length === 1 ? results[0] : { files: results });
  } catch (err) {
    res.status(err.status ?? 500).json({ message: err.message });
  }
});
 
app.get("/uploads/:uploadId/status", async (req, res) => {
  try {
    const { session } = await getUploadSession(req.params.uploadId);
    res.json({ status: session.status, fileId: session.fileId });
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});
 
app.listen(Number(process.env.PORT ?? 3001));

Reference: React / Next.js client

Posts FormData to your API — not to Reupload directly.

.env.local
NEXT_PUBLIC_API_URL=http://localhost:3001
# Vite: VITE_API_URL=http://localhost:3001
lib/upload-via-api.js
const API_URL =
  process.env.NEXT_PUBLIC_API_URL ??
  process.env.VITE_API_URL ??
  "http://localhost:3001";
 
export async function uploadFileViaApi(file) {
  const form = new FormData();
  form.append("file", file);
  const res = await fetch(`${API_URL}/uploads`, { method: "POST", body: form });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.message ?? "Upload failed");
  }
  return res.json();
}

Reference: Python

Python 3.10+ with the official reupload-sdk. Django REST Framework: Django + reupload-sdk (direct upload, CDN file router, Swagger). FastAPI: helpers below.

Install

terminal
pip install "reupload-sdk[fastapi]"

Environment variables

Same names as the Node SDK: REUPLOAD_API_KEY, REUPLOAD_PROJECT_ID, optional REUPLOAD_API_BASE_URL.

FastAPI upload route

main.py
from fastapi import FastAPI
from reupload import create_async_reupload_from_env
from reupload.fastapi import create_async_fastapi_direct_upload_handler
 
app = FastAPI()
client = create_async_reupload_from_env()
upload = create_async_fastapi_direct_upload_handler(client=client)
 
@app.post("/uploads")
async def uploads(request):
    return await upload(request)

Direct upload without FastAPI helper

upload.py
from reupload import create_reupload_from_env
 
client = create_reupload_from_env()
 
result = client.uploads.upload_direct(
    {
        "projectId": client.default_project_id,
        "file": {
            "data": file_bytes,
            "filename": "photo.jpg",
            "contentType": "image/jpeg",
            "size": len(file_bytes),
        },
    },
)
print(result["fileId"])

Production tips

  • Rate-limit your upload routes per user/IP.
  • Validate file type and size before calling Reupload — see supported file types.
  • Store fileId after processing completes.
  • For many large browser uploads, consider the CDN guide instead of proxying all bytes.