Go IPFS Upload: Pin Files from Golang Applications

Upload and pin files to IPFS from Go applications. Complete guide with net/http examples and error handling patterns.

Nacho Collby Updated: 7 min read
Upload and pin files to IPFS from Go applications. Complete guide with net/http examples and error handling patterns.

Go’s standard library already has everything you need to talk to an IPFS pinning service — net/http, encoding/json, and encoding/base64 cover the whole surface. No SDK, no cgo bindings to a local IPFS node, no extra dependency to vet.

This guide walks through uploading JSON and binary files to IPFS from Go, listing and pinning existing content, defining response structs, and fanning out concurrent uploads with goroutines.

IPFS Ninja

Upload JSON to IPFS in Go (30 seconds)#

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

const apiKey = "bws_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"

func main() {
	payload := map[string]any{
		"content": map[string]any{
			"name": "Alice",
			"role": "Go developer",
		},
		"description": "profile.json",
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest(http.MethodPost, "https://api.ipfs.ninja/upload/new", bytes.NewReader(body))
	req.Header.Set("X-Api-Key", apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}

That’s a full, runnable upload. The rest of this guide builds a reusable client around this same pattern.

Struct Definitions for API Responses#

Define the response shape once and reuse it across every call:

package ipfsninja

type UploadResponse struct {
	CID    string `json:"cid"`
	SizeMB float64 `json:"sizeMB"`
	URIs   struct {
		IPFS string `json:"ipfs"`
		URL  string `json:"url"`
	} `json:"uris"`
}

type FileInfo struct {
	CID         string `json:"cid"`
	Description string `json:"description"`
	SizeMB      float64 `json:"sizeMB"`
	CreatedAt   string `json:"createdAt"`
}

type ListResponse struct {
	Total int        `json:"total"`
	Files []FileInfo `json:"files"`
}

type APIError struct {
	StatusCode int
	Message    string
}

func (e *APIError) Error() string {
	return e.Message
}

A Minimal Go Client#

Wrap the HTTP calls in a small client type so callers don’t repeat header and error-handling boilerplate:

package ipfsninja

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type Client struct {
	APIKey  string
	BaseURL string
	HTTP    *http.Client
}

func New(apiKey string) *Client {
	return &Client{
		APIKey:  apiKey,
		BaseURL: "https://api.ipfs.ninja",
		HTTP:    http.DefaultClient,
	}
}

func (c *Client) do(method, path string, payload any) (*http.Response, error) {
	var body io.Reader
	if payload != nil {
		b, err := json.Marshal(payload)
		if err != nil {
			return nil, err
		}
		body = bytes.NewReader(b)
	}

	req, err := http.NewRequest(method, c.BaseURL+path, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Api-Key", c.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		defer resp.Body.Close()
		out, _ := io.ReadAll(resp.Body)
		return nil, &APIError{StatusCode: resp.StatusCode, Message: fmt.Sprintf("%d: %s", resp.StatusCode, string(out))}
	}
	return resp, nil
}

Uploading JSON Data#

func (c *Client) UploadJSON(content any, description string) (*UploadResponse, error) {
	resp, err := c.do(http.MethodPost, "/upload/new", map[string]any{
		"content":     content,
		"description": description,
	})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var out UploadResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return &out, nil
}

Uploading Binary Files#

Files upload the same way JSON does — base64-encode the bytes and send them as content:

import (
	"encoding/base64"
	"os"
)

func (c *Client) UploadFile(path, description string) (*UploadResponse, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	encoded := base64.StdEncoding.EncodeToString(raw)

	return c.UploadJSON(encoded, description)
}

For large files, avoid loading the whole thing into memory with os.ReadFile — stream through io.Copy into a base64.NewEncoder writer instead, or split ingestion out of the request body entirely for anything over a few hundred MB.

Listing and Pinning Files#

func (c *Client) ListFiles(fromMs, toMs int64) (*ListResponse, error) {
	resp, err := c.do(http.MethodGet, fmt.Sprintf("/upload/list?from=%d&to=%d", fromMs, toMs), nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var out ListResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) PinExisting(cid, description string) (*UploadResponse, error) {
	resp, err := c.do(http.MethodPost, "/pin", map[string]any{
		"cid":         cid,
		"description": description,
	})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var out UploadResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return &out, nil
}

Concurrent Uploads with Goroutines#

When uploading a batch of files — build artifacts, a directory of images — fan the requests out with goroutines and collect results over a channel instead of uploading one at a time:

import "sync"

type uploadResult struct {
	Path     string
	Response *UploadResponse
	Err      error
}

func (c *Client) UploadDirectory(paths []string, concurrency int) []uploadResult {
	jobs := make(chan string)
	results := make(chan uploadResult, len(paths))

	var wg sync.WaitGroup
	for i := 0; i < concurrency; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for path := range jobs {
				resp, err := c.UploadFile(path, path)
				results <- uploadResult{Path: path, Response: resp, Err: err}
			}
		}()
	}

	go func() {
		for _, p := range paths {
			jobs <- p
		}
		close(jobs)
	}()

	go func() {
		wg.Wait()
		close(results)
	}()

	out := make([]uploadResult, 0, len(paths))
	for r := range results {
		out = append(out, r)
	}
	return out
}

Cap concurrency at something reasonable (4-8) — the API applies per-key rate limits, and an unbounded goroutine-per-file fan-out will just trip them instead of finishing faster.

Error Handling#

Check resp.StatusCode explicitly rather than only checking err — a non-2xx response from net/http is not a Go error by default:

resp, err := client.UploadFile("./report.pdf", "Q3 report")
if err != nil {
	var apiErr *ipfsninja.APIError
	if errors.As(err, &apiErr) {
		switch apiErr.StatusCode {
		case 401:
			log.Fatal("check your API key")
		case 402:
			log.Fatal("plan limit reached — see the response body for upgrade details")
		case 429:
			log.Fatal("rate limited, back off and retry")
		default:
			log.Fatalf("upload failed: %v", apiErr)
		}
	}
	log.Fatal(err)
}
fmt.Println("pinned:", resp.CID, resp.URIs.URL)

Next Steps#

You now have a dependency-free Go client covering uploads, listing, pinning, and concurrent batch processing. For the full endpoint reference, see the IPFS upload API tutorial. If you’re new to pinning concepts, what is IPFS pinning covers the fundamentals this guide builds on.

Ready to start pinning? Create a free account — 50 files, 1 GB storage, 2 GB bandwidth/mo. No credit card required.

About this article: This article was drafted by an AI assistant using IPFS.NINJA’s content generation workflow, then reviewed and approved by Nacho Coll. All code examples were verified against the live IPFS.NINJA API. If you spot an inaccuracy, please open an issue at https://github.com/ipfs-ninja/feedback. Read more about how we use AI in our content and meet the people behind IPFS.NINJA.

Nacho Coll

About the author

Founder & Engineer at IPFS.NINJA

Nacho founded IPFS.NINJA to make content-addressed storage feel as simple as an S3 PUT — a single API call, a permanent CID, no wallets or peer discovery to reason about. Writes about IPFS internals, decentralized storage patterns, and the pinning-service landscape from the operator side of the wire.

Back to Blog

Related Posts