API

Remove Image Backgrounds in Go with an API (net/http, mime/multipart, Worker Pool)

September 6, 20266 min readBy BG Clear Editorial

Go services that handle product images usually end up needing background removal for one pipeline step, and nobody wants to bolt a Python model onto a Go binary. An HTTP call is the idiomatic answer, and Go's standard library covers multipart, JSON and concurrency without a single dependency. This tutorial builds a small client for the bgclear API, then a bounded worker pool that stays inside the 60-requests-per-minute limit while processing a catalogue.

In this guide

The client: multipart upload

package bgclear

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

const api = "https://www.bgclear.ai/api/v1"

type Client struct {
	Key  string
	HTTP *http.Client
}

type APIError struct {
	Status  int
	Code    string `json:"code"`
	Message string `json:"message"`
	Docs    string `json:"docs"`
}

func (e *APIError) Error() string { return fmt.Sprintf("bgclear %d %s: %s", e.Status, e.Code, e.Message) }

// Remove uploads a file and returns the cutout bytes. size: preview (free, ≤800px) | full | auto.
func (c *Client) Remove(path, size, format, idemKey string) ([]byte, http.Header, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, nil, err
	}
	defer f.Close()

	var body bytes.Buffer
	w := multipart.NewWriter(&body)
	part, _ := w.CreateFormFile("image_file", path)
	if _, err := io.Copy(part, f); err != nil {
		return nil, nil, err
	}
	_ = w.WriteField("size", size)
	_ = w.WriteField("format", format)
	_ = w.Close()

	req, _ := http.NewRequest(http.MethodPost, api+"/remove", &body)
	req.Header.Set("Authorization", "Bearer "+c.Key)
	req.Header.Set("Content-Type", w.FormDataContentType())
	if idemKey != "" {
		req.Header.Set("Idempotency-Key", idemKey)
	}

	res, err := c.HTTP.Do(req)
	if err != nil {
		return nil, nil, err
	}
	defer res.Body.Close()
	data, _ := io.ReadAll(res.Body)

	if res.StatusCode != http.StatusOK {
		var env struct{ Error APIError `json:"error"` }
		_ = json.Unmarshal(data, &env)
		env.Error.Status = res.StatusCode
		return nil, res.Header, &env.Error
	}
	return data, res.Header, nil
}

Usage: c := &bgclear.Client{Key: os.Getenv("BGCLEAR_API_KEY"), HTTP: &http.Client{Timeout: 90 * time.Second}}, then png, hdr, err := c.Remove("photo.jpg", "auto", "png", ""); hdr.Get("X-Credits-Remaining") tells you the balance. Add w.WriteField("bg_color", "ffffff") for a white background.

JSON mode for URL inputs

When the image is already hosted, skip the multipart and ask for JSON back — the response carries a result URL valid for 24 hours, which is what you want when the next step is an S3 upload rather than a local file.

type RemoveResult struct {
	ID               string `json:"id"`
	URL              string `json:"url"`
	Width            int    `json:"width"`
	Height           int    `json:"height"`
	CreditsCharged   int    `json:"credits_charged"`
	CreditsRemaining int    `json:"credits_remaining"`
	ProcessingMs     int    `json:"processing_ms"`
}

func (c *Client) RemoveURL(imageURL, size string) (*RemoveResult, error) {
	payload, _ := json.Marshal(map[string]string{
		"image_url": imageURL, "size": size, "format": "webp", "bg_color": "ffffff",
	})
	req, _ := http.NewRequest(http.MethodPost, api+"/remove", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer "+c.Key)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	res, err := c.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		var env struct{ Error APIError `json:"error"` }
		_ = json.NewDecoder(res.Body).Decode(&env)
		env.Error.Status = res.StatusCode
		return nil, &env.Error
	}
	var out RemoveResult
	return &out, json.NewDecoder(res.Body).Decode(&out)
}

A worker pool that respects the rate limit

The limit is 60 requests a minute per key. A pool of 4 workers with a shared ticker at one request per second stays under it with headroom, and a 429 (rare at that pace) is handled by sleeping for X-RateLimit-Reset seconds and retrying the same file — safe because the idempotency key is the file's hash.

func processAll(c *Client, paths []string) {
	tick := time.NewTicker(time.Second) // 60/min budget
	defer tick.Stop()
	jobs := make(chan string)
	var wg sync.WaitGroup

	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for p := range jobs {
				<-tick.C
				key := fileHash(p)
				for attempt := 0; attempt < 3; attempt++ {
					png, hdr, err := c.Remove(p, "full", "png", key)
					var apiErr *APIError
					if errors.As(err, &apiErr) {
						switch apiErr.Status {
						case 429:
							wait, _ := strconv.Atoi(hdr.Get("X-RateLimit-Reset"))
							time.Sleep(time.Duration(max(wait, 1)) * time.Second)
							continue
						case 402:
							log.Fatal("out of credits: https://www.bgclear.ai/api-pricing/")
						}
					}
					if err != nil {
						log.Printf("%s: %v", p, err)
						break
					}
					_ = os.WriteFile(strings.TrimSuffix(p, filepath.Ext(p))+"-no-bg.png", png, 0o644)
					break
				}
			}
		}()
	}
	for _, p := range paths {
		jobs <- p
	}
	close(jobs)
	wg.Wait()
}

fileHash is a SHA-256 of the file contents, hex-encoded. Failed requests are never charged, so a crash mid-run costs nothing but the rerun.

Images over 4 MP: jobs and callbacks

Synchronous full-size requests are capped at 4 megapixels. For larger files, post the same form to /api/v1/jobs; the 202 response carries a job_id. Either poll GET /api/v1/jobs/{id} until status is done (then fetch url, no auth needed, valid 24 hours) or set an X-Callback-Url header and handle the POST in an http.HandlerFunc — acknowledge with 200 first, download afterwards. Batches of up to 50 URLs go through /api/v1/jobs/batch; the bulk guide covers both flows. Pricing is one credit per full-resolution image with no subscription (details); the request fields match remove.bg's, which matters if you are migrating before its 1 December 2026 shutdown.

Frequently asked questions

Is there a Go module for the API?

Not an official one — the client above is 60 lines of standard library and is the recommended integration. You can also generate a client from /api/v1/openapi.json with oapi-codegen.

How do I test without spending credits?

Use size="preview" — free and unlimited, up to 800 px — in your tests and CI.

What is the fastest way to process 10,000 images?

Submit 50 image URLs per call to /api/v1/jobs/batch with a callback URL; that is 200 requests, well inside the rate limit, and the GPU queue drains without your process blocking.

What does a full-resolution image cost?

One credit — $9 for 100, $39 for 500, $129 for 2,000; credits never expire.

Ship it with the bgclear API

remove.bg-compatible endpoints, credits from $9 for 100 images, no subscription. Pay, get a key, make your first call in under two minutes.

Get API credits →

Tools for this guide

Background Removal API Free

background removal api free

Canva Background Remover Alternative

canva background remover alternative

Cheapest Background Remover API

cheapest background remover api

Keep reading

API

Remove Image Backgrounds in Java and Spring Boot with an API (HttpClient, RestClient)

Java 11+ HttpClient with a JSON/base64 body (no multipart boilerplate), Spring's RestClient with MultipartBodyBuilder, error mapping, and a @Async service for catalogues.

API

Remove Image Backgrounds in Python with an API (requests, 20 Lines)

A complete Python walkthrough: single image, URL input, JSON responses, a folder script with safe retries, and async jobs for large files — no ML libraries, just requests.

API

Bulk Background Removal via API: Async Jobs, Batches of 50 and Webhooks

How to process a catalogue of thousands of images without babysitting a script: batch submission, callbacks instead of polling, credit accounting, and what to do when the queue is full.

API

7 remove.bg API Alternatives Compared (2026): Price per Image, Free Tier, Drop-in Compatibility

With remove.bg closing on 1 Dec 2026, here is what the alternatives actually charge per image, what you get free, and which ones let you keep your existing request code.