Remove Image Backgrounds in Go with an API (net/http, mime/multipart, Worker Pool)
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.