...
We are aware of the recently disclosed PHP2Shell vulnerabilities (CVE-2026-109618 and CVE-2026-109619) and can confirm Filestack is not impacted by these vulnerabilities.

Go File Upload with the Filestack Go SDK

Overview

The Filestack Go SDK is the official Go client library for Filestack, a file upload, storage, transformation and delivery API. It lets a Go application accept and store files without writing its own chunking, storage or CDN code.

The SDK covers four areas:

  • Uploads – send a local file, a file from an HTTP form, or a remote URL to Filestack storage and receive a filelink.
  • Filelinks – reference a stored file by its handle to get its URL, download it or delete it.
  • Transformations – build URLs that apply image and file operations such as resize, flip, upscale and black-and-white.
  • Workflows – run a sequence of tasks defined on your Filestack account and read the results.

Before you start: you need Go 1.16 or later and a Filestack API key from the Filestack Developer Portal. The examples read their settings from environment variables:

  • FILESTACK_API_KEY – your API key (all examples).
  • FILESTACK_APP_SECRET – your app secret, used to sign security policies (sections 8 and 13).
  • FILESTACK_HANDLE – the handle of a file you have already uploaded (sections 3, 8, 9, 12 and 14).
  • FILESTACK_WORKFLOW_ID – the ID of a workflow on your account (section 13).

Installing the Filestack Go SDK

Run this inside your Go module:

go get github.com/filestack/filestack-go

If your project does not have a go.mod file yet, create one first:

go mod init example.com/myapp

Import only the packages you use. The client package is always needed; the others are options and helpers:

import (
    "github.com/filestack/filestack-go/client"
    clientOptions "github.com/filestack/filestack-go/options/client"
    uploadOptions "github.com/filestack/filestack-go/options/upload"
    uploadURLOptions "github.com/filestack/filestack-go/options/uploadurl"
    "github.com/filestack/filestack-go/resource"
    "github.com/filestack/filestack-go/security"
    "github.com/filestack/filestack-go/store"
    "github.com/filestack/filestack-go/transformation/args"
)

Creating a Filestack client

client.NewClient takes your API key and optional client options (a security policy, a custom HTTP client or a limit on parallel uploads) and returns a client and an error. Always check the error before using the client.

A filelink is a reference to a stored file. It is identified by a handle, the short ID Filestack assigns to every uploaded file. fileLink.AsString() returns the file’s CDN URL.

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    fileLink, err := cli.NewFileLink(os.Getenv("FILESTACK_HANDLE"))
    if err != nil {
        log.Fatalf("failed to create filelink: %v", err)
    }

    fmt.Println(fileLink.AsString())
}

Uploading a local file

Open the file with os.Open and pass it to cli.Upload. Upload accepts any io.ReadSeeker, so an *os.File, a multipart.File from a form or a *bytes.Reader all work. It returns a filelink; AsString() returns the URL of the stored file.

Complete example – upload a local file

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
)

func main() {
    file, err := os.Open("photo.jpg")
    if err != nil {
        log.Fatalf("cannot open the file: %v", err)
    }
    defer file.Close()

    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    fileLink, err := cli.Upload(context.Background(), file)
    if err != nil {
        log.Fatalf("upload failed: %v", err)
    }

    fmt.Println(fileLink.AsString())
}

Run it with your API key set:

FILESTACK_API_KEY=your_api_key go run main.go

Upload options: file name, MIME type and large files

Without store parameters, the SDK stores the file as unnamed_file with the MIME type application/octet-stream. Set the name and type with uploadOptions.StoreParams. store.Params also accepts Path, Location, Region, Container, Access and UploadTags to control where and how the file is stored.

For large files, add uploadOptions.Intelligent(). Intelligent upload splits the file into parts and uploads up to five parts at a time by default. Change the limit with clientOptions.MaxConcurrentUploads when you create the client.

Complete example – set the file name and MIME type and upload a large file

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
    uploadOptions "github.com/filestack/filestack-go/options/upload"
    "github.com/filestack/filestack-go/store"
)

func main() {
    file, err := os.Open("report.pdf")
    if err != nil {
        log.Fatalf("cannot open the file: %v", err)
    }
    defer file.Close()

    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    fileLink, err := cli.Upload(
        context.Background(),
        file,
        uploadOptions.StoreParams(&store.Params{
            FileName: "report.pdf",
            MimeType: "application/pdf",
        }),
        uploadOptions.Intelligent(), // split large files into parts and upload them in parallel
    )
    if err != nil {
        log.Fatalf("upload failed: %v", err)
    }

    fmt.Println(fileLink.AsString())
}

Golang file upload server: handling uploads with net/http

To accept uploads from a browser or an API client, read the file from the multipart form with r.FormFile and pass it straight to cli.Upload. multipart.File implements io.ReadSeeker, so no temporary copy on disk is needed. Passing r.Context() stops the upload if the client disconnects.

Complete example – single-file upload handler

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/filestack/filestack-go/client"
    uploadOptions "github.com/filestack/filestack-go/options/upload"
    "github.com/filestack/filestack-go/store"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }

        // Reject request bodies larger than 32 MB.
        r.Body = http.MaxBytesReader(w, r.Body, 32<<20)

        file, header, err := r.FormFile("file")
        if err != nil {
            http.Error(w, "invalid file field", http.StatusBadRequest)
            return
        }
        defer file.Close()

        params := &store.Params{
            FileName: header.Filename,
            MimeType: header.Header.Get("Content-Type"),
        }

        fileLink, err := cli.Upload(r.Context(), file, uploadOptions.StoreParams(params))
        if err != nil {
            http.Error(w, "upload failed", http.StatusBadGateway)
            return
        }

        fmt.Fprintln(w, fileLink.AsString())
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Test it with curl:

curl -F "file=@photo.jpg" http://localhost:8080/upload

Or post to it from an HTML form. The form must use enctype=”multipart/form-data”, and the input name must match the field name in r.FormFile:

<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <button type="submit">Upload</button>
</form>

Uploading multiple files from one form

Send every file under the same field name, call r.ParseMultipartForm, then upload each entry in r.MultipartForm.File. This handler returns one URL per line after all files have been uploaded.

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/filestack/filestack-go/client"
    uploadOptions "github.com/filestack/filestack-go/options/upload"
    "github.com/filestack/filestack-go/store"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    http.HandleFunc("/upload-multiple", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }

        // Limit the whole request to 100 MB; keep up to 32 MB in memory.
        r.Body = http.MaxBytesReader(w, r.Body, 100<<20)
        if err := r.ParseMultipartForm(32 << 20); err != nil {
            http.Error(w, "request too large or invalid", http.StatusBadRequest)
            return
        }

        headers := r.MultipartForm.File["files"]
        if len(headers) == 0 {
            http.Error(w, "no files in the files field", http.StatusBadRequest)
            return
        }

        var links []string
        for _, header := range headers {
            file, err := header.Open()
            if err != nil {
                http.Error(w, "cannot read file", http.StatusBadRequest)
                return
            }

            fileLink, err := cli.Upload(r.Context(), file, uploadOptions.StoreParams(&store.Params{
                FileName: header.Filename,
                MimeType: header.Header.Get("Content-Type"),
            }))
            file.Close()
            if err != nil {
                http.Error(w, "upload failed", http.StatusBadGateway)
                return
            }

            links = append(links, fileLink.AsString())
        }

        // Write the response only after every file has been uploaded.
        for _, link := range links {
            fmt.Fprintln(w, link)
        }
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Test it with curl:

curl -F "files=@a.jpg" -F "files=@b.pdf" http://localhost:8080/upload-multiple

In an HTML form, add the multiple attribute to the file input:

<input type="file" name="files" multiple>

Restricting file size and file type

Do not rely on the Content-Type the browser sends; it can be wrong or spoofed. Limit the request size with http.MaxBytesReader, detect the real type from the first 512 bytes with http.DetectContentType, and reject anything not on your allow list before uploading.

package main

import (
    "fmt"
    "io"
    "log"
    "mime/multipart"
    "net/http"
    "os"

    "github.com/filestack/filestack-go/client"
    uploadOptions "github.com/filestack/filestack-go/options/upload"
    "github.com/filestack/filestack-go/store"
)

var allowedTypes = map[string]bool{
    "image/jpeg":      true,
    "image/png":       true,
    "application/pdf": true,
}

// detectType reads the first 512 bytes to find the real content type,
// then rewinds the file so the whole file can be uploaded.
func detectType(file multipart.File) (string, error) {
    buf := make([]byte, 512)
    n, err := file.Read(buf)
    if err != nil && err != io.EOF {
        return "", err
    }
    if _, err := file.Seek(0, io.SeekStart); err != nil {
        return "", err
    }
    return http.DetectContentType(buf[:n]), nil
}

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }

        r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10 MB limit

        file, header, err := r.FormFile("file")
        if err != nil {
            http.Error(w, "invalid file field", http.StatusBadRequest)
            return
        }
        defer file.Close()

        contentType, err := detectType(file)
        if err != nil || !allowedTypes[contentType] {
            http.Error(w, "file type not allowed", http.StatusUnsupportedMediaType)
            return
        }

        fileLink, err := cli.Upload(r.Context(), file, uploadOptions.StoreParams(&store.Params{
            FileName: header.Filename,
            MimeType: contentType,
        }))
        if err != nil {
            http.Error(w, "upload failed", http.StatusBadGateway)
            return
        }

        fmt.Fprintln(w, fileLink.AsString())
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Using Gin, Echo or another framework? The pattern is the same. In Gin and Echo, c.FormFile(“file”) returns a *multipart.FileHeader; call its Open method to get a multipart.File, then pass that file to cli.Upload with the request context.

Uploading a file from a URL

cli.UploadURL asks Filestack to fetch a file from a public URL and store it, so the file never passes through your server. Use uploadURLOptions.StoreParams to set the stored file name or other store parameters.

Complete example – upload a file from a URL

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
    uploadURLOptions "github.com/filestack/filestack-go/options/uploadurl"
    "github.com/filestack/filestack-go/store"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    fileLink, err := cli.UploadURL(
        context.Background(),
        "https://example.com/images/photo.jpg",
        uploadURLOptions.StoreParams(&store.Params{
            FileName: "photo.jpg",
        }),
    )
    if err != nil {
        log.Fatalf("upload from URL failed: %v", err)
    }

    fmt.Println(fileLink.AsString())
}

Upload sends the bytes of a file you have open; UploadURL asks Filestack to fetch a file from a URL. Both return a filelink.

Securing uploads with a security policy

If security is enabled for your application in the Developer Portal, every request must carry a signed policy. Create one with security.NewSecurity, passing your app secret and a security.Policy. Expiry, a Unix timestamp, is the only required field.

Pass the policy to the client with clientOptions.SecurityPolicy to sign every request, or to a single upload with uploadOptions.SecurityPolicy (uploadURLOptions.SecurityPolicy for UploadURL). Keep the app secret on your server and never send it to a browser or mobile app.

Complete example – client with a 24-hour security policy

package main

import (
    "fmt"
    "log"
    "os"
    "time"

    "github.com/filestack/filestack-go/client"
    clientOptions "github.com/filestack/filestack-go/options/client"
    "github.com/filestack/filestack-go/security"
)

func main() {
    sec := security.NewSecurity(os.Getenv("FILESTACK_APP_SECRET"), &security.Policy{
        Expiry: time.Now().Add(24 * time.Hour).Unix(), // the only required field
    })

    cli, err := client.NewClient(
        os.Getenv("FILESTACK_API_KEY"),
        clientOptions.SecurityPolicy(sec),
    )
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    fileLink, err := cli.NewFileLink(os.Getenv("FILESTACK_HANDLE"))
    if err != nil {
        log.Fatalf("failed to create filelink: %v", err)
    }

    fmt.Println(fileLink.AsString())
}

Replacing the default HTTP client

The SDK uses http.DefaultClient, which has no timeout. Pass your own *http.Client with clientOptions.HTTPClient to set a timeout, a proxy or a custom transport.

Complete example – client with a one-minute timeout

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/filestack/filestack-go/client"
    clientOptions "github.com/filestack/filestack-go/options/client"
)

func main() {
    httpClient := &http.Client{Timeout: time.Minute}

    cli, err := client.NewClient(
        os.Getenv("FILESTACK_API_KEY"),
        clientOptions.HTTPClient(httpClient),
    )
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    fileLink, err := cli.NewFileLink(os.Getenv("FILESTACK_HANDLE"))
    if err != nil {
        log.Fatalf("failed to create filelink: %v", err)
    }

    fmt.Println(fileLink.AsString())
}

Plain net/http vs the Filestack SDK

Can I upload files in Go without an SDK?

Yes. Go’s standard library can receive multipart uploads and save them to disk. The Filestack SDK adds cloud storage, CDN delivery and file processing on top.

Need Plain net/http Filestack Go SDK
Receive a form upload Yes (r.FormFile) Yes (same handler, then cli.Upload)
Store files Your own disk or storage code Filestack storage or your own cloud storage
Public file URL and CDN Build it yourself Returned as a filelink
Large files in parallel parts Build it yourself uploadOptions.Intelligent()
Upload from a remote URL Download and re-upload yourself cli.UploadURL
Image and file transformations Separate libraries Transformation URLs
Signed access rules Build it yourself Security policies

Saving an uploaded file to disk

For comparison, this handler uses only the standard library and saves each upload to a local uploads folder:

package main

import (
    "io"
    "log"
    "net/http"
    "os"
    "path/filepath"
)

func main() {
    // Create the target folder once at startup.
    if err := os.MkdirAll("uploads", 0o755); err != nil {
        log.Fatalf("cannot create uploads folder: %v", err)
    }

    http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }

        r.Body = http.MaxBytesReader(w, r.Body, 32<<20)

        file, header, err := r.FormFile("file")
        if err != nil {
            http.Error(w, "invalid file field", http.StatusBadRequest)
            return
        }
        defer file.Close()

        // filepath.Base strips any directory parts from the client-supplied name.
        dst, err := os.Create(filepath.Join("uploads", filepath.Base(header.Filename)))
        if err != nil {
            http.Error(w, "cannot save file", http.StatusInternalServerError)
            return
        }
        defer dst.Close()

        if _, err := io.Copy(dst, file); err != nil {
            http.Error(w, "cannot save file", http.StatusInternalServerError)
            return
        }

        w.Write([]byte("uploaded\n"))
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Building and chaining transformations

A transformation builds a Filestack processing URL for a stored file (resource.NewHandle), a public URL (resource.NewExternalURL) or a file in your own storage (resource.NewStorageAlias). Each task method returns the transformation, so tasks can be chained. BuildURL returns the final URL; Filestack processes the file when that URL is requested. To get the result as bytes in Go, call cli.ExecuteTransformation(ctx, transformation).

Complete example – flip and negate an image from a URL

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
    "github.com/filestack/filestack-go/resource"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    transformation, err := cli.NewTransformation(
        resource.NewExternalURL("[https://example.com/images/photo.jpg](https://example.com/images/photo.jpg)"),
    )
    if err != nil {
        log.Fatalf("failed to create transformation: %v", err)
    }

    transformation.Flip().Negative() // chain several tasks

    fmt.Println(transformation.BuildURL())
}

Transformation arguments

Tasks that take parameters accept an argument value from the transformation/args package. Create it with the matching constructor, such as args.NewUpscaleArgs(), and set options with its setter methods.

Complete example – upscale and convert to black and white

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
    "github.com/filestack/filestack-go/resource"
    "github.com/filestack/filestack-go/transformation/args"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    transformation, err := cli.NewTransformation(
        resource.NewHandle(os.Getenv("FILESTACK_HANDLE")),
    )
    if err != nil {
        log.Fatalf("failed to create transformation: %v", err)
    }

    transformation.
        Upscale(args.NewUpscaleArgs().SetNoise("low")).
        BlackWhite(args.NewBlackWhiteArgs().SetThreshold(5))

    fmt.Println(transformation.BuildURL())
}

Running a workflow and checking its status

How do I run a Filestack workflow from Go and read the result?

Workflows are sequences of tasks, with optional conditions, defined on your Filestack account and referenced by a workflow ID (see the workflows overview).

cli.RunWorkflow starts a workflow for a file and returns a job ID. Call cli.CheckWorkflowStatus with that ID until the status is Finished, then read the results. Workflows require a security policy on the client; without one, RunWorkflow returns an error.

Complete example – run a workflow and wait for the result

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/filestack/filestack-go/client"
    clientOptions "github.com/filestack/filestack-go/options/client"
    "github.com/filestack/filestack-go/resource"
    "github.com/filestack/filestack-go/security"
)

func main() {
    // Workflows require a security policy on the client.
    sec := security.NewSecurity(os.Getenv("FILESTACK_APP_SECRET"), &security.Policy{
        Expiry: time.Now().Add(24 * time.Hour).Unix(),
    })

    cli, err := client.NewClient(
        os.Getenv("FILESTACK_API_KEY"),
        clientOptions.SecurityPolicy(sec),
    )
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    ctx := context.Background()
    input := resource.NewExternalURL("[https://example.com/images/photo.jpg](https://example.com/images/photo.jpg)")

    runResponse, err := cli.RunWorkflow(ctx, os.Getenv("FILESTACK_WORKFLOW_ID"), input)
    if err != nil {
        log.Fatalf("failed to run workflow: %v", err)
    }
    fmt.Println("job id:", runResponse.JobID)

    // Check every 5 seconds, for up to 5 minutes.
    for attempt := 0; attempt < 60; attempt++ {
        status, err := cli.CheckWorkflowStatus(ctx, runResponse.JobID)
        if err != nil {
            log.Fatalf("failed to check workflow status: %v", err)
        }

        if status.Status == "Finished" {
            for name, result := range status.Results {
                fmt.Println(name, result.URL, result.MimeType, result.Size)
            }
            return
        }

        time.Sleep(5 * time.Second)
    }

    log.Fatal("workflow did not finish within 5 minutes")
}

Reading metadata and downloading files

cli.GetMetadata returns details of a stored file, including its file name, MIME type, size, upload time and storage location. A filelink can also download the file to a local *os.File with Download, or delete it with Delete(ctx); deleting requires a security policy.

Complete example – read metadata and download a stored file

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/filestack/filestack-go/client"
    "github.com/filestack/filestack-go/resource"
)

func main() {
    cli, err := client.NewClient(os.Getenv("FILESTACK_API_KEY"))
    if err != nil {
        log.Fatalf("failed to initialize client: %v", err)
    }

    ctx := context.Background()
    handle := resource.NewHandle(os.Getenv("FILESTACK_HANDLE"))

    metadata, err := cli.GetMetadata(ctx, handle)
    if err != nil {
        log.Fatalf("failed to read metadata: %v", err)
    }
    fmt.Printf("%+v\n", metadata)

    fileLink, err := cli.NewFileLink(os.Getenv("FILESTACK_HANDLE"))
    if err != nil {
        log.Fatalf("failed to create filelink: %v", err)
    }

    out, err := os.Create("downloaded-file")
    if err != nil {
        log.Fatalf("cannot create output file: %v", err)
    }
    defer out.Close()

    if _, err := fileLink.Download(out); err != nil {
        log.Fatalf("download failed: %v", err)
    }
}

Frequently asked questions

How do I upload a file in Go?

Install the SDK with go get github.com/filestack/filestack-go, create a client with client.NewClient, open the file with os.Open, and call cli.Upload(ctx, file). Upload returns a filelink, and fileLink.AsString() returns the URL of the stored file.

How do I handle a file upload from an HTML form in Go?

In a net/http handler, call r.FormFile(“file”) to get the uploaded multipart.File and its header, then pass the file to cli.Upload with r.Context(). multipart.File implements io.ReadSeeker, so no temporary copy on disk is needed.

How do I upload large files in Go?

Add uploadOptions.Intelligent() to cli.Upload. Intelligent upload splits the file into parts and uploads up to five parts at a time by default. Change the limit with clientOptions.MaxConcurrentUploads.

Why is my uploaded file named unnamed_file?

The SDK uses unnamed_file and application/octet-stream when no file name or MIME type is given. Pass uploadOptions.StoreParams(&store.Params{FileName: …, MimeType: …}) to cli.Upload.

What is the difference between Upload and UploadURL?

Upload(ctx, file) sends the bytes of an open file or any io.ReadSeeker. UploadURL(ctx, url) tells Filestack to fetch a file from a public URL. Both return a filelink and both accept store parameters and a security policy as options.