Tus Protocol Implementation Example
Tus is an open protocol for resumable file uploads. The protocol allows resuming uploads after a connection drop, uploading large files in chunks, and controlling the upload process.
When you need Tus
Use Tus if you are uploading large files and want to:
- Resume uploads after a network drop — the user won’t lose progress
- Send the file in chunks (chunk upload) — more reliable for large files
- Show progress to the user and manage retries
For developers: Official protocol documentation: https://tus.io/protocols/resumable-upload.html
Libraries for different languages: https://tus.io/implementations.html
How Tus upload works (4 steps)
- Client selects a file and makes a request to your backend (e.g.,
POST /upload), passing the filename and size (without the file itself). - Backend calls the Kinescope API
POST /v2/initand receives a unique Tus endpoint in response. - Backend returns this endpoint to the client (as a redirect or in JSON).
- Client uploads the file via the Tus protocol directly to Kinescope using the received endpoint.
init (to get a unique Tus endpoint), and then the browser uploads the file directly to Kinescope via the issued endpoint.What to prepare
Before starting, make sure you have:
- Kinescope API token (store on the server, do not publish in the browser)
- Project or folder ID (
parent_id) where the file will be uploaded - Your backend endpoint, e.g.,
POST /upload, which will:- accept file metadata from the client
- call
https://uploader.kinescope.io/v2/init - return the Tus endpoint to the client
Interaction diagram
Here is what the upload process looks like:
sequenceDiagram
participant Client as Client_Browser
participant Backend as Backend_UserServer
participant API as Kinescope_API
participant TusEndpoint as Kinescope_TusEndpoint
Note over Backend: "File does not pass through backend"
Client->>Backend: "POST /upload (metadata,size)"
Backend->>API: "POST /v2/init (Bearer TOKEN, parent_id, filename, filesize, type)"
API-->>Backend: "201 Created (endpoint)"
Backend-->>Client: "endpoint (Location/redirect or JSON)"
Note over Client,TusEndpoint: "File is uploaded directly to Kinescope"
Client->>TusEndpoint: "PATCH chunk_1 (file bytes)"
TusEndpoint-->>Client: "204 No Content (Upload-Offset)"
Client->>TusEndpoint: "PATCH chunk_2 (file bytes)"
TusEndpoint-->>Client: "204 No Content (Upload-Offset)"
Note over Client,TusEndpoint: "Repeats until upload is complete"
Client->>TusEndpoint: "PATCH last_chunk (file bytes)"
TusEndpoint-->>Client: "204 No Content (Upload complete)"
The /upload method contract
What the client sends → your backend
If you use tus-js-client, a convenient option is to accept standard Tus headers:
Upload-Length: file size in bytesUpload-Metadata: metadata (e.g.,filenamein base64)
For developers: In the examples below, metadata is taken from the
Upload-Metadataheader and the size fromUpload-Length. This is not the only option: these values can also be accepted in JSON if that is more convenient for your frontend.
What your backend returns → client
There are two working options:
- Option A (as in the examples below):
201 Createdresponse +Location: <tus-endpoint>header, or redirect to endpoint - Option B:
200 OK+ JSON{ "endpoint": "<tus-endpoint>" }(and on the frontend you useuploadURL)
If you use Option A, make sure CORS allows the client to read the Location header (Access-Control-Expose-Headers: Location is required).
Example request to Kinescope /v2/init
Your backend should send an upload initialization request:
curl -X POST 'https://uploader.kinescope.io/v2/init' \
-H 'Authorization: Bearer <KINESCOPE_API_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"parent_id": "<PROJECT_OR_FOLDER_ID>",
"type": "video",
"filename": "example.mp4",
"title": "example.mp4",
"filesize": 123456789
}'
In response, Kinescope will return 201 Created and an object containing data.endpoint — this is the Tus endpoint for uploading.
Backend implementation example
Here is a sample backend handler in Go:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
)
const (
kinescopeAPIToken = "11111111-1111-1111-1111-111111111111"
kinescopeUploadInitURL = "https://uploader.kinescope.io/v2/init"
)
type KinescopeInitResponse struct {
Data struct {
ID string `json:"id"`
Endpoint string `json:"endpoint"`
} `json:"data"`
}
// Handler for upload initialization request
func handleUploadInit(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Set CORS headers
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers",
"Origin, Content-Type, Tus-Resumable, Upload-Length, Upload-Metadata")
w.Header().Set("Access-Control-Allow-Methods",
"POST, GET, HEAD, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Expose-Headers", "Location")
}
// Handle OPTIONS request
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusOK)
return
}
// Parse metadata from Upload-Metadata header
metadata := parseMetadataHeader(r.Header.Get("Upload-Metadata"))
// Parse file size from Upload-Length header
filesize, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
if err != nil || filesize <= 0 {
http.Error(w, "bad header Upload-Length", http.StatusBadRequest)
return
}
// Build request to Kinescope API
requestBody := map[string]interface{}{
"client_ip": r.RemoteAddr,
"parent_id": "your project or folder ID here",
"type": "video",
"title": metadata["filename"],
"filename": metadata["filename"],
"filesize": filesize,
}
// Call Kinescope API to initialize upload
body, _ := json.Marshal(requestBody)
req, _ := http.NewRequest("POST", kinescopeUploadInitURL, strings.NewReader(string(body)))
req.Header.Set("Authorization", "Bearer "+kinescopeAPIToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusCreated {
http.Error(w, fmt.Sprintf("kinescope api response status=%d", resp.StatusCode),
http.StatusBadRequest)
return
}
defer resp.Body.Close()
var result KinescopeInitResponse
json.NewDecoder(resp.Body).Decode(&result)
// Return redirect to Tus endpoint
w.Header().Set("Location", result.Data.Endpoint)
w.WriteHeader(http.StatusCreated)
}
// Parse Upload-Metadata header
func parseMetadataHeader(header string) map[string]string {
meta := make(map[string]string)
if header == "" {
return meta
}
elements := strings.Split(header, ",")
for _, element := range elements {
parts := strings.Fields(strings.TrimSpace(element))
if len(parts) != 2 {
continue
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
continue
}
meta[parts[0]] = string(decoded)
}
return meta
}
Frontend implementation example
Use the tus-js-client library to work with the Tus protocol in the browser.
HTML example:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo Upload Tus</title>
</head>
<body>
<input type="file" id="file-input">
</body>
<script src="https://cdn.jsdelivr.net/npm/tus-js-client@latest/dist/tus.js"></script>
<script src="upload.js"></script>
</html>
JavaScript code example (upload.js):
function initializeUpload(file, backendEndpoint) {
const upload = new tus.Upload(file, {
// Option 1: endpoint on your backend (recommended)
endpoint: backendEndpoint,
// Option 2: direct uploadURL (if you already have a Tus endpoint)
// uploadURL: "https://uploader.kinescope.io/v2/upload/0966958f-638b-4aab-bf4a-7f9860a57a93",
retryDelays: [0, 3000, 5000, 10000, 20000],
chunkSize: 10000000, // 10 MB per chunk
metadata: {
filename: file.name,
filetype: file.type
},
onError: function(error) {
console.error("Upload error:", error);
},
onProgress: function(bytesUploaded, bytesTotal) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
console.log(`Uploaded: ${bytesUploaded} / ${bytesTotal} (${percentage}%)`);
},
onSuccess: function() {
console.log(`File ${upload.file.name} uploaded successfully. URL: ${upload.url}`);
}
});
// Find previous uploads for resuming
upload.findPreviousUploads()
.then(function(previousUploads) {
if (previousUploads.length > 0) {
upload.resumeFromPreviousUpload(previousUploads[0]);
}
upload.start();
})
.catch(function(error) {
console.error("Error finding previous uploads:", error);
upload.start();
});
}
document.addEventListener('DOMContentLoaded', function() {
const fileInput = document.getElementById('file-input');
if (!fileInput) {
console.error('Element file-input not found');
return;
}
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) {
return;
}
const backendEndpoint = 'https://your-backend.com/upload';
initializeUpload(file, backendEndpoint);
});
});
Troubleshooting
CORS error and Location header not visible
Problem: CORS error in the browser and the Location header is not visible.
Solution: Check that your backend sets Access-Control-Expose-Headers: Location.
403/401 from Kinescope when calling /v2/init
Problem: Kinescope returns an authorization error.
Solution: Check the token and access rights, make sure the token is not expired or revoked.
Upload does not continue after a drop
Problem: After a connection drop, the upload does not resume.
Solution: Enable findPreviousUploads()/resumeFromPreviousUpload() and do not change endpoint/uploadURL for the same file.
Frequent network errors
Problem: Constant network errors during upload.
Solution: Reduce chunkSize and configure retryDelays.
What to send to support
If you need help from technical support, attach:
- URL of the page/application where the upload occurs
- Error time and timezone
- Error log from the browser console (see Copying errors from the browser console )
- HAR file if the problem seems network-related (see Saving browser-server interaction to a HAR file )
- Example
curlrequest from your backend toPOST https://uploader.kinescope.io/v2/init(without tokens or personal data)
Support channel: the support chat within the Kinescope interface.
Done! You can now set up large file uploads via the Tus protocol.
What’s next?
- File upload via API — other methods for uploading video
- General API guidelines — authorization and request format
- Kinescope API — full API documentation
Still have questions? Write to the support chat within the Kinescope interface — our specialists will help!