KINOPIPE WITH GO

Video editing from Go, without FFmpeg on your machine.

Trim, resize, compress, convert, caption and transcribe by calling one HTTP endpoint per operation. Parameters are validated before anything renders, so a bad call fails as a typed error rather than a broken encode.

What running FFmpeg from Go usually costs

  • A single static binary is often why Go was chosen, and shelling out to FFmpeg puts a system dependency back into the deployment.
  • os/exec gives you an exit code and stderr, so a bad parameter is a string to parse rather than a typed error.
  • The encode competes with the service for CPU on the same box, which is felt first by whatever else that box was doing.

Trim a clip and wait for the file

Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

const base = "https://kinopipe.com/api/v1"

type job struct {
	JobID        string `json:"jobId"`
	Status       string `json:"status"`
	ErrorMessage string `json:"errorMessage"`
	Result       struct {
		Output struct {
			DownloadURL string `json:"downloadUrl"`
		} `json:"output"`
	} `json:"result"`
}

func call(method, url string, body any) (job, error) {
	var payload *bytes.Reader
	if body != nil {
		encoded, _ := json.Marshal(body)
		payload = bytes.NewReader(encoded)
	} else {
		payload = bytes.NewReader(nil)
	}
	request, err := http.NewRequest(method, url, payload)
	if err != nil {
		return job{}, err
	}
	request.Header.Set("Authorization", "Bearer "+os.Getenv("KINOPIPE_API_KEY"))
	request.Header.Set("Content-Type", "application/json")

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return job{}, err
	}
	defer response.Body.Close()

	var decoded job
	return decoded, json.NewDecoder(response.Body).Decode(&decoded)
}

func main() {
	started, err := call(http.MethodPost, base+"/tools/trim-video", map[string]any{
		"inputs":  []map[string]string{{"id": "main", "url": "https://example.com/interview.mp4"}},
		"options": map[string]int{"start": 12, "end": 48},
	})
	if err != nil {
		panic(err)
	}

	for {
		current, err := call(http.MethodGet, base+"/jobs/"+started.JobID, nil)
		if err != nil {
			panic(err)
		}
		if current.Status == "failed" {
			panic(current.ErrorMessage)
		}
		if current.Status == "succeeded" {
			fmt.Println(current.Result.Output.DownloadURL)
			return
		}
		time.Sleep(2 * time.Second)
	}
}
  • Standard library only, so the binary stays single and static.
  • A split job returns every segment under outputs rather than output, which is worth handling if you call split-video or split-video-by-scenes.
  • Send an Idempotency-Key header when you retry, and the retry returns the original job instead of paying for a second render.
Or let an agent write the call
The same operations are exposed over MCP, so an agent in your editor discovers them and calls them directly.

FAQ

Why not just call os/exec?

It works when FFmpeg is on the machine and you are happy to own that. The trade is a system dependency in the image and errors that arrive as text on stderr instead of as validation before the render.

Is there a Go SDK?

Not yet, and the API is small enough that the code above is most of one. Everything is JSON over HTTP with a bearer token.

Can I run several edits at once?

Yes. Jobs are independent, so start them in goroutines and poll each. Credits are reserved per job and refunded when a render fails.