Skip to main content
Django creates the video in Bunny Stream and signs an upload. The browser sends the file to us over TUS, and your API key stays in the server’s environment. Everything here runs on Django and the standard library.

Django example on GitHub

A Django 6 app with no database.

Quickstart

1

Create the uploads app

These paths assume a project made with django-admin startproject config .. Create an app for the upload code and register it, so Django finds its templates and static files.
config/settings.py
2

Add your library credentials

.env
Copy both from your library’s API page. Django won’t read .env on its own, and uv run --env-file .env manage.py runserver loads it for you.
3

Create the Bunny Stream module

urllib covers the two API calls. The upload signature is a SHA-256 of the library ID, API key, expiry, and video ID.
uploads/bunny_stream.py
4

Add the views

Put the upload routes behind your own authentication before you deploy. The create route makes a video in your library and hands back a signature that lets the caller upload into it. Left open, anyone who finds the URL can fill your library with uploads that you pay to store, encode, and deliver.Server Actions and API routes are public HTTP endpoints, even when nothing in your UI links to them. Check the user on every request, and check that they own a video ID before you re-sign it or return its status.
index renders the page. create_upload re-signs an unfinished video when the browser sends its ID, and creates a new one otherwise.
uploads/views.py
Add the three routes next to the admin route that startproject created.
config/urls.py
5

Upload from the browser

Render the CSRF token into the page. The CSRF middleware rejects the POST without it.
uploads/templates/uploads/index.html
There’s no build step. tus-js-client comes from jsDelivr, and the token travels as X-CSRFToken.
uploads/static/uploads/video-uploader.js
tus-js-client sends the credentials as headers with every request. The video ID goes into localStorage against the file, which is how a reload finds its way back to the same upload. Add this to the same file.
abort() pauses. start() picks up from the last chunk we acknowledged.A 401 from the TUS endpoint means the signature doesn’t match the headers. Check that the library ID and API key belong to the same library. A 400 means the expiry has already passed. Re-signing keeps the upload’s original expiry, as the TUS FAQ explains.

Play it once it’s encoded

We start encoding when the last chunk arrives. Poll your status route until status reaches 4 (finished), 5 or 6 (failed), then embed embedUrl. This goes in the same file too.
encodeProgress gives you a percentage to show in the meantime. A webhook tells your server when encoding finishes. Then wire both to the page’s #video-file input and #video-output element. Picking another file cancels the current upload.
Start the server with uv run --env-file .env manage.py runserver, open http://localhost:8000, and choose a video.

Before you deploy

Wrap both views in login_required and record who owns each video ID. The example’s settings.py is for development, and production needs DJANGO_SECRET_KEY, DEBUG = False, and ALLOWED_HOSTS.

Troubleshooting

The template needs the csrf-token meta tag, and the fetch needs the X-CSRFToken header.
Last modified on October 6, 2026