Guides
Django + reupload-sdk
Build a Django REST Framework API that holds your Reupload API key and proxies uploads to Reupload. This guide uses the official reupload-sdk (reupload-sdk) — same API as FastAPI, without a Django-specific adapter.
We cover direct upload first (browser or form → Django → Reupload), then the CDN session flow (browser PUTs to a signed URL), and webhooks to know when processing finishes. Compare with React/Next + Node.js for a full browser CDN walkthrough using npm packages.
Prerequisites
- Python 3.10+, Django 5+, and Django REST Framework
- Reupload API key with
files.writeand a project UUID — sign up, Authentication - (CDN flow only) Your front-end origin in the project's upload CORS settings in the dashboard
Install
pip install django djangorestframework reupload-sdk python-dotenv
# Optional: OpenAPI / Swagger UI for your routes
pip install drf-spectacular django-cors-headersEnvironment variables
Load with python-dotenv in settings.py or export in your process manager. Never commit keys to git.
REUPLOAD_API_KEY=ru_xxxxxxxx
REUPLOAD_PROJECT_ID=your-project-uuid| Variable | Required | Description |
|---|---|---|
REUPLOAD_API_KEY | Yes | Bearer key (ru_…) |
REUPLOAD_PROJECT_ID | Yes | Default project UUID for uploads |
REUPLOAD_API_BASE_URL | No | Default https://api.reupload.dev/api/v1 |
Reupload client
Use a single sync Reupload client per process (WSGI-friendly). Shared helpers from reupload.fastapi.shared work in Django — they are framework-agnostic upload utilities, not FastAPI-only.
from functools import lru_cache
from reupload import Reupload, create_reupload_from_env
from reupload.errors import DirectUploadHandlerError, ReuploadError, is_reupload_error
from reupload.fastapi.shared import (
UploadedFilePart,
parse_direct_upload_from_form,
run_direct_upload,
)
@lru_cache(maxsize=1)
def get_reupload_client() -> Reupload:
return create_reupload_from_env()
def reupload_error_payload(error: BaseException) -> tuple[int, dict[str, str]]:
if isinstance(error, DirectUploadHandlerError):
return error.status, error.to_json()
if is_reupload_error(error):
err: ReuploadError = error
return err.status, {"error": err.code, "message": err.message}
return 500, {"error": "INTERNAL_ERROR", "message": str(error) or "Unexpected error."}
def django_file_to_part(uploaded_file) -> UploadedFilePart:
data = uploaded_file.read()
return UploadedFilePart(
data=data,
filename=uploaded_file.name or "upload",
content_type=uploaded_file.content_type or "application/octet-stream",
size=len(data),
)
def direct_upload(
*,
file_part: UploadedFilePart,
form_project_id: str | None = None,
filename_override: str | None = None,
is_public: str | None = None,
) -> dict:
client = get_reupload_client()
payload = parse_direct_upload_from_form(
client=client,
file=file_part,
form_project_id=form_project_id,
filename_field=filename_override,
is_public_field=is_public,
)
return run_direct_upload(client, payload)Direct upload
The client sends multipart/form-data to your Django API; your server forwards bytes to Reupload POST /uploads/direct. No signed CDN URL and no browser PUT to storage.
Browser
React / Next
Your API
Node.js
Reupload
API
Full Reupload contract: Server-side uploads (direct), Server-side upload guide.
Upload view
from rest_framework import status
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from uploads import services
class DirectUploadView(APIView):
"""Browser → Django (multipart) → Reupload POST /uploads/direct → 202."""
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
uploaded = request.FILES.get("file")
if not uploaded:
return Response(
{"error": "MISSING_FILE", "message": 'Missing multipart field "file".'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
part = services.django_file_to_part(uploaded)
result = services.direct_upload(
file_part=part,
form_project_id=request.data.get("projectId"),
filename_override=request.data.get("filename"),
is_public=request.data.get("isPublic"),
)
except Exception as error:
code, body = services.reupload_error_payload(error)
return Response(body, status=code)
return Response(result, status=status.HTTP_202_ACCEPTED)URL routing
from django.urls import path
from uploads import views
urlpatterns = [
path("uploads/direct/", views.DirectUploadView.as_view(), name="upload-direct"),
]from django.urls import include, path
urlpatterns = [
path("api/", include("uploads.urls")),
]Try it
curl -X POST http://127.0.0.1:8000/api/uploads/direct/ \
-F "[email protected]" \
-F "isPublic=true"Response (202 Accepted):
{
"uploadId": "00000000-0000-4000-8000-000000000002",
"fileId": "00000000-0000-4000-8000-000000000003",
"status": "processing"
}Direct upload stores your file before the response returns
202 + processing means background work (scan, finalize, quotas) is still running — not that the upload failed. Use webhooks or poll GET /uploads/session/:uploadId for path, url, and confirmed sizeBytes. Why this design →
Poll until the file is ready
class UploadSessionDetailView(APIView):
def get(self, request, upload_id: str):
client = services.get_reupload_client()
wait = request.query_params.get("wait", "").lower() in ("1", "true", "yes")
try:
if wait:
result = client.uploads.wait_for_completion(upload_id)
else:
result = client.uploads.get_session(upload_id)
except Exception as error:
code, body = services.reupload_error_payload(error)
return Response(body, status=code)
return Response(result)curl "http://127.0.0.1:8000/api/uploads/session/<uploadId>/?wait=true"When session.status is COMPLETED, use file.publicUrl or GET /files/{fileId}/access/ for a signed URL. In production, prefer webhooks instead of long polling.
Multiple files
Use request.FILES.getlist("file") and run_direct_upload_batch from reupload.fastapi.shared — same pattern as direct upload. See multiple files on the API docs.
CDN upload flow
Use this when the browser should PUT bytes directly to Reupload storage (large files, less load on Django). Your API creates a session and completes it; the client handles the PUT.
Browser
React / Next
Your API
Node.js
Reupload
API
CDN
Storage
Expose a small file router on Django, then use @reupload/client or @reupload/react in the front end (JavaScript/TypeScript only).
Backend file router
| Your route | reupload-sdk |
|---|---|
POST /api/uploads/session/ | client.uploads.create_session() |
Browser PUT to uploadUrl | Not Django — client only |
POST /api/uploads/session/complete/ | client.uploads.complete() |
GET /api/uploads/session/{id}/ | client.uploads.get_session() |
Create session
from rest_framework.parsers import JSONParser
from rest_framework import serializers
class CreateUploadSessionSerializer(serializers.Serializer):
projectId = serializers.UUIDField(required=False)
filename = serializers.CharField(max_length=512)
contentType = serializers.CharField(max_length=255)
size = serializers.IntegerField(min_value=1)
isPublic = serializers.BooleanField(required=False, default=False)
class CreateUploadSessionView(APIView):
parser_classes = [JSONParser]
def post(self, request):
serializer = CreateUploadSessionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
body = dict(serializer.validated_data)
client = services.get_reupload_client()
if "projectId" not in body:
body["projectId"] = client.default_project_id
try:
result = client.uploads.create_session(body)
except Exception as error:
code, payload = services.reupload_error_payload(error)
return Response(payload, status=code)
return Response(result, status=status.HTTP_201_CREATED)Returns uploadId, fileId, and uploadUrl for the browser PUT.
Complete session
class CompleteUploadSessionSerializer(serializers.Serializer):
uploadId = serializers.CharField()
class CompleteUploadSessionView(APIView):
parser_classes = [JSONParser]
def post(self, request):
serializer = CompleteUploadSessionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
result = services.get_reupload_client().uploads.complete(
serializer.validated_data,
)
except Exception as error:
code, payload = services.reupload_error_payload(error)
return Response(payload, status=code)
return Response(result)Server-side CDN test (optional)
To exercise the full CDN path from Django only (no browser), create a session, PUT bytes with put_to_upload_url, then complete:
from reupload.cdn import put_to_upload_url
def cdn_upload_with_file(session_input: dict, file_part: UploadedFilePart) -> dict:
client = get_reupload_client()
session = client.uploads.create_session(session_input)
put_to_upload_url(
session["uploadUrl"],
file_part.data,
file_part.content_type,
)
return client.uploads.complete({"uploadId": session["uploadId"]})Wire this to a multipart view (e.g. POST /api/uploads/cdn/) for manual testing — production apps usually split create/complete and let the browser PUT.
CDN details: Client-side uploads, reupload-sdk CDN upload.
Interactive API docs
Add drf-spectacularso you can test multipart uploads from Swagger UI (same as FastAPI's /docs).
INSTALLED_APPS = [
# ...
"rest_framework",
"drf_spectacular",
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
path("api/", include("uploads.urls")),
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/docs/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
]Open http://127.0.0.1:8000/api/docs/, use Try it out on POST /api/uploads/direct/, choose a file, and execute.
Webhooks
After a direct upload returns 202, Reupload processes the file in the background. Instead of polling GET /uploads/session/:uploadId, register a webhook in the dashboard pointing at your Django URL (e.g. https://api.example.com/api/webhooks/reupload/). There is no public API to create webhooks programmatically in the current release.
Event types you will see most often:
file.uploaded— processing finished; payload includesfileId,url,mimeType,sizeBytesfile.updated— metadata changed (e.g. rename)file.deleted— file removed
file.uploaded fires when the file is ready to use — not when your upload route returns 202. Full envelope and retry behavior: Webhooks.
Signing secret
When you create the webhook endpoint, copy the signing secret once (prefix whsec_). Store it server-side only:
REUPLOAD_WEBHOOK_SECRET=whsec_xxxxxxxxWebhook view (verify signature)
Read request.body as raw bytes before json.loads. Verify the Reupload-Signature header (HMAC-SHA256 over {timestamp}.{rawBody}), then handle the event. Exempt this route from CSRF — Reupload is an external caller, not a browser form.
import hashlib
import hmac
import json
import os
import time
from secrets import compare_digest
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.response import Response
from rest_framework.views import APIView
MAX_AGE_SECONDS = 5 * 60
def _parse_signature_header(header: str | None) -> tuple[int, str] | None:
if not header or not header.strip():
return None
t: int | None = None
v1: str | None = None
for part in header.split(","):
key, _, value = part.strip().partition("=")
if key == "t":
try:
t = int(value, 10)
except ValueError:
return None
elif key == "v1":
v1 = value
if t is None or not v1:
return None
return t, v1
def verify_reupload_signature(secret: str, raw_body: bytes, header: str | None) -> bool:
parsed = _parse_signature_header(header)
if not parsed:
return False
t, v1 = parsed
if abs(time.time() - t) > MAX_AGE_SECONDS:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return compare_digest(expected, v1)
@method_decorator(csrf_exempt, name="dispatch")
class ReuploadWebhookView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
secret = os.environ.get("REUPLOAD_WEBHOOK_SECRET", "")
if not secret:
return Response({"error": "missing_secret"}, status=500)
raw_body = request.body
header = request.headers.get("Reupload-Signature")
if not verify_reupload_signature(secret, raw_body, header):
return Response({"error": "invalid_signature"}, status=401)
event = json.loads(raw_body)
event_type = event.get("type")
data = event.get("data") or {}
if event_type == "file.uploaded":
# Persist fileId, url, etc. — file is ready for your app
file_id = data.get("fileId")
public_url = data.get("url")
...
return Response({"received": True})urlpatterns = [
path("uploads/direct/", views.DirectUploadView.as_view(), name="upload-direct"),
path("webhooks/reupload/", webhooks.ReuploadWebhookView.as_view(), name="reupload-webhook"),
]Return 2xx quickly after enqueueing work (DB update, Celery task). Reupload retries failed deliveries with backoff — see delivery logs in the dashboard.
Errors
Map ReuploadError and validation errors to DRF responses using reupload_error_payload above. See Errors and SDK error handling.
Related
- reupload-sdk (Python) — full SDK reference
- Server-side upload — architecture and Reupload API contract
- React/Next + Node.js — CDN flow with npm browser packages
- Server-side uploads (direct) — HTTP reference
- Webhooks — events, payload, and signature details