API

Remove Image Backgrounds in Rust with an API (reqwest multipart, Tokio, Bounded Concurrency)

September 6, 20266 min readBy BG Clear Editorial

Rust has excellent image crates and no practical in-process background remover — the segmentation models live in Python and ONNX runtimes that are a heavy dependency for a CLI or a service. An HTTP call is the pragmatic answer, and reqwest's multipart support makes it short. This guide builds a small client for the bgclear API with typed errors, then a Tokio worker that clears a folder of product photos without tripping the rate limit or paying twice for a retried file.

In this guide

Dependencies

[dependencies]
reqwest = { version = "0.12", features = ["multipart", "json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
hex = "0.4"

Export the key as BGCLEAR_API_KEY; get one on the API dashboard (10 free full-resolution credits; preview-size results are free without limit).

The client: multipart upload with typed errors

use reqwest::{multipart, Client, StatusCode};
use serde::Deserialize;
use std::path::Path;

const API: &str = "https://www.bgclear.ai/api/v1";

#[derive(Debug, Deserialize)]
pub struct ApiError {
    pub code: String,
    pub message: String,
    #[serde(default)]
    pub docs: String,
}

#[derive(Debug, Deserialize)]
struct ErrorEnvelope {
    error: ApiError,
}

#[derive(Debug)]
pub enum RemoveError {
    Api { status: StatusCode, error: ApiError, reset_secs: Option<u64> },
    Http(reqwest::Error),
    Io(std::io::Error),
}

pub struct BgClear {
    http: Client,
    key: String,
}

impl BgClear {
    pub fn new(key: impl Into<String>) -> Self {
        let http = Client::builder()
            .timeout(std::time::Duration::from_secs(90))
            .build()
            .expect("client");
        Self { http, key: key.into() }
    }

    /// size: "preview" (free, ≤800 px) | "full" (1 credit, ≤4 MP sync) | "auto"
    pub async fn remove(
        &self,
        path: &Path,
        size: &str,
        format: &str,
        idempotency_key: Option<&str>,
    ) -> Result<(Vec<u8>, Option<u64>), RemoveError> {
        let form = multipart::Form::new()
            .file("image_file", path)
            .await
            .map_err(RemoveError::Io)?
            .text("size", size.to_string())
            .text("format", format.to_string());

        let mut req = self
            .http
            .post(format!("{API}/remove"))
            .bearer_auth(&self.key)
            .multipart(form);
        if let Some(k) = idempotency_key {
            req = req.header("Idempotency-Key", k);
        }

        let res = req.send().await.map_err(RemoveError::Http)?;
        let status = res.status();
        let remaining = res
            .headers()
            .get("X-Credits-Remaining")
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.parse().ok());
        let reset_secs = res
            .headers()
            .get("X-RateLimit-Reset")
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.parse().ok());

        if !status.is_success() {
            let env: ErrorEnvelope = res.json().await.map_err(RemoveError::Http)?;
            return Err(RemoveError::Api { status, error: env.error, reset_secs });
        }
        let bytes = res.bytes().await.map_err(RemoveError::Http)?;
        Ok((bytes.to_vec(), remaining))
    }
}

Every error from the API is JSON with a stable codeinsufficient_credits (402), rate_limited (429), image_too_large (400), processing_failed (500/504), gpu_unavailable (503) — so matching on error.code is reliable. Add .text("bg_color", "ffffff") for a white background or .text("crop", "true") to trim to the subject.

JSON mode for hosted images

When the source is already at a URL, skip the multipart and ask for JSON back — the response carries a hosted result URL valid for 24 hours, which suits a pipeline that uploads to S3 next rather than writing a local file.

#[derive(Debug, Deserialize)]
pub struct RemoveResult {
    pub id: String,
    pub url: String,
    pub width: u32,
    pub height: u32,
    pub credits_charged: u32,
    pub credits_remaining: u32,
    pub processing_ms: u32,
}

pub async fn remove_url(client: &Client, key: &str, image_url: &str) -> reqwest::Result<RemoveResult> {
    client
        .post(format!("{API}/remove"))
        .bearer_auth(key)
        .header("Accept", "application/json")
        .json(&serde_json::json!({
            "image_url": image_url,
            "size": "auto",
            "format": "webp",
            "bg_color": "ffffff"
        }))
        .send()
        .await?
        .error_for_status()?
        .json()
        .await
}

A folder worker under the rate limit

The API allows 60 requests a minute per key. A Tokio interval at one request per second paces submissions; a semaphore bounds in-flight work; the idempotency key is the file's SHA-256, so a retried request after a timeout returns the cached result instead of charging again.

use sha2::{Digest, Sha256};
use std::sync::Arc;
use tokio::sync::{Mutex, Semaphore};

fn file_key(bytes: &[u8]) -> String {
    hex::encode(Sha256::digest(bytes))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Arc::new(BgClear::new(std::env::var("BGCLEAR_API_KEY")?));
    let limiter = Arc::new(Mutex::new(tokio::time::interval(std::time::Duration::from_secs(1))));
    let slots = Arc::new(Semaphore::new(4));
    let mut handles = Vec::new();

    for entry in std::fs::read_dir("in")? {
        let path = entry?.path();
        if path.extension().map(|e| e != "jpg" && e != "png").unwrap_or(true) {
            continue;
        }
        let (client, limiter, slots) = (client.clone(), limiter.clone(), slots.clone());
        handles.push(tokio::spawn(async move {
            let _permit = slots.acquire().await.unwrap();
            let key = file_key(&std::fs::read(&path).unwrap());
            for attempt in 0..3 {
                limiter.lock().await.tick().await; // one submission per second
                match client.remove(&path, "full", "png", Some(&key)).await {
                    Ok((png, remaining)) => {
                        let out = format!("out/{}.png", path.file_stem().unwrap().to_string_lossy());
                        std::fs::write(&out, png).unwrap();
                        println!("{out} (credits left: {remaining:?})");
                        return;
                    }
                    Err(RemoveError::Api { status, error, reset_secs }) => match status.as_u16() {
                        429 => tokio::time::sleep(std::time::Duration::from_secs(reset_secs.unwrap_or(5))).await,
                        402 => panic!("out of credits: https://www.bgclear.ai/api-pricing/"),
                        s if s >= 500 && attempt < 2 => continue,
                        _ => { eprintln!("{}: {} {}", path.display(), error.code, error.message); return; }
                    },
                    Err(e) => { eprintln!("{}: {e:?}", path.display()); return; }
                }
            }
        }));
    }
    for h in handles { h.await?; }
    Ok(())
}

Failed requests are never charged, so a crash mid-run costs nothing beyond the rerun. Images above 4 megapixels are rejected by the synchronous endpoint; post them to /api/v1/jobs instead and poll GET /api/v1/jobs/{id} or take a callback — the bulk guide shows both, plus the 50-URL batch endpoint.

Cost and compatibility

Previews are free and unlimited — use size = "preview" in tests. Full-resolution results are one credit each: $9 for 100, $39 for 500, $129 for 2,000; credits never expire (pricing). The field names are remove.bg's, so a Rust service that called api.remove.bg only changes the URL and swaps the X-Api-Key header for bearer_auth before remove.bg's API closes on 1 December 2026 — mapping here.

Frequently asked questions

Is there a crate for the bgclear API?

No; the client above is complete and depends only on reqwest, serde and tokio. A typed client can also be generated from the OpenAPI document at /api/v1/openapi.json with progenitor or openapi-generator.

Can I use the blocking reqwest client?

Yes — reqwest::blocking supports multipart too (Form::file is synchronous there); drop the Tokio worker and pace with std::thread::sleep.

How do I get a transparent PNG versus a white background?

Transparent is the default. Add bg_color=ffffff (any hex) for a solid fill, and format=jpg if you want the smallest marketplace-ready file.

What does a full-resolution image cost?

One credit — $0.065 to $0.09 depending on pack size; previews are free.

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

Standard library only: a multipart upload with mime/multipart, JSON mode for URL inputs, typed errors, a bounded worker pool that respects the 60/min rate limit, and async jobs for big images.

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.