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
There are two APIs in play:
| API | Who calls it | You implement |
|---|---|---|
Your upload API (e.g. POST /uploads) | Web, mobile, or desktop app | Accept multipart from clients; auth your users |
Reupload API (POST /uploads/direct) | Your server only | Forward file bytes with your ru_ API key — never expose the key to clients |
Prerequisites
- Reupload API key with
files.writeand a project UUID — sign up, Authentication - A backend that can send
multipart/form-dataHTTP 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
https://api.reupload.dev/api/v1Authentication
Every Reupload request from your server uses a Bearer API key. Requires files.write for direct upload.
Authorization: Bearer ru_xxxxxxxxStore the key in server environment variables only. See Authentication.
Direct upload — single file
| Value | |
|---|---|
| Method | POST |
| Path | /uploads/direct |
| Content-Type | multipart/form-data |
| Success status | 202 Accepted |
Form fields
| Field | Required | Description |
|---|---|---|
projectId | Yes | Target project UUID |
file | Yes | File bytes; set each part's Content-Type to the file MIME type |
filename | No | Storage 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 -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'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)
{
"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:
uploadIdandfileId
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.uploadedwithpath,url,sizeBytes, andurlExpiresAt
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
| Approach | Best for | You get |
|---|---|---|
Webhook file.uploaded | Production integrations | path, url, sizeBytes, urlExpiresAt |
Poll GET /uploads/session/:uploadId | Scripts, tests, no webhook endpoint | Session status; when COMPLETED, a top-level file object with the same fields as webhook data |
GET /files/:fileId after completion | Fetching metadata from your backend | File 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 -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)
{
"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 | |
|---|---|
| Method | GET |
| Path | /uploads/session/:uploadId |
| Permission | files.read (or files.write) |
curl -s 'https://api.reupload.dev/api/v1/uploads/session/UPLOAD_ID' \
-H 'Authorization: Bearer ru_...'{
"session": {
"id": "uuid",
"fileId": "uuid",
"status": "PROCESSING",
"expectedSize": 502400,
"expectedMime": "application/pdf"
}
}{
"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).
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
messageand a status code — see Errors - API key must have access to
projectId(project-scoped keys orallProjects)
Integration steps (any language)
- Receive an uploaded file from your client (multipart, base64, stream — your choice).
- Build
multipart/form-datawithprojectId, the file part(s), and optionalfilenamefor a single file. POST https://api.reupload.dev/api/v1/uploads/directwithAuthorization: Bearer ru_....- On
202, saveuploadIdandfileIdin your database. - Poll
GET /uploads/session/:uploadIdor 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 route | Purpose |
|---|---|
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 pattern | Use CDN flow instead |
|---|---|
| Clients must only call your API | Large files; avoid proxying bytes through your server |
| No CDN / project upload CORS setup | Minimize server bandwidth |
| Scan or transform files before Reupload | Browser PUT straight to signed URL |
Integration checklist
- Create API key + project; verify with
GET /public/whoami(Try it on Quickstart). - Confirm
POST /uploads/directfrom your server with curl or the Try it panel above. - Implement your client-facing upload route that forwards to Reupload.
- Store
fileId; poll session or use webhooks untilCOMPLETED.
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
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:3000Reupload client helper
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)
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.
NEXT_PUBLIC_API_URL=http://localhost:3001
# Vite: VITE_API_URL=http://localhost:3001const 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
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
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
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
fileIdafter processing completes. - For many large browser uploads, consider the CDN guide instead of proxying all bytes.