Skip navigation

Tus Protocol Implementation Example

Updated: 28.04.2026
Open as Markdown

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)

  1. Client selects a file and makes a request to your backend (e.g., POST /upload), passing the filename and size (without the file itself).
  2. Backend calls the Kinescope API POST /v2/init and receives a unique Tus endpoint in response.
  3. Backend returns this endpoint to the client (as a redirect or in JSON).
  4. Client uploads the file via the Tus protocol directly to Kinescope using the received endpoint.
Important: The backend does not participate in the file transfer. The file and main upload traffic do not go through your server: the backend is only needed for 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
Important: The Kinescope token must not be passed to the frontend. The client should only communicate with your backend and the Tus endpoint.

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 bytes
  • Upload-Metadata: metadata (e.g., filename in base64)

For developers: In the examples below, metadata is taken from the Upload-Metadata header and the size from Upload-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 Created response + Location: <tus-endpoint> header, or redirect to endpoint
  • Option B: 200 OK + JSON { "endpoint": "<tus-endpoint>" } (and on the frontend you use uploadURL)

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:

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?

  1. File upload via API — other methods for uploading video
  2. General API guidelines — authorization and request format
  3. Kinescope API — full API documentation

Still have questions? Write to the support chat within the Kinescope interface — our specialists will help!