# 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)**

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:

```mermaid
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:

```bash
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:

```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](https://github.com/tus/tus-js-client)** library to work with the Tus protocol in the browser.

**HTML example:**

```html
<!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):**

```javascript
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](https://docs.kinescope.com/troubleshooting/copying-browser-console-errors/))
- HAR file if the problem seems network-related (see [Saving browser-server interaction to a HAR file](https://docs.kinescope.com/troubleshooting/saving-har-file/))
- Example `curl` request from your backend to `POST 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?

1. **[File upload via API](https://docs.kinescope.com/developer-guides/file-upload-via-api/)** — other methods for uploading video
2. **[General API guidelines](https://docs.kinescope.com/developer-guides/api-general-rules/)** — authorization and request format
3. **[Kinescope API](https://docs.kinescope.com/developer-guides/kinescope-api/)** — full API documentation

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

