Bulk Background Removal via API: Async Jobs, Batches of 50 and Webhooks
One image is a curl command. Ten thousand product images is a queueing problem: rate limits, images over the sync size cap, results that expire, a script that dies at image 6,000 and does not know where it was. This guide is the production pattern for the bgclear API — batch submission, callbacks, back-pressure and exact credit accounting — with code you can paste into a worker.
In this guide
Sync vs jobs: which to use for bulk
POST /api/v1/remove is synchronous: the response is the image, within 30 seconds, for images up to 4 megapixels at full size. It is perfect for interactive uploads. For bulk it has two costs: your worker is blocked while each image processes, and anything over 4 MP is rejected.
POST /api/v1/jobs queues the image and returns 202 {"job_id": …, "status": "queued"} immediately. Jobs take images up to 50 MP, run on the GPU queue in order, and can notify you when done. POST /api/v1/jobs/batch submits up to 50 images in one JSON request. Both share the sync endpoint's parameters (size, format, bg_color, crop). For catalogues, use batch.
Submitting a catalogue 50 at a time
Batch is JSON-only: an images array of objects each carrying an image_url (or image_file_b64), plus top-level parameters that apply to every image. Store the returned job ids against your SKUs — that mapping is what lets you resume after a crash.
import csv, requests, itertools
API = "https://www.bgclear.ai/api/v1"
H = {"Authorization": "Bearer bgc_live_YOUR_KEY", "X-Callback-Url": "https://yourapp.com/hooks/bgclear"}
def chunks(it, n):
it = iter(it)
while batch := list(itertools.islice(it, n)):
yield batch
rows = list(csv.DictReader(open("catalogue.csv"))) # columns: sku, image_url
for batch in chunks(rows, 50):
r = requests.post(f"{API}/jobs/batch", headers=H, json={
"size": "full", "format": "webp", "bg_color": "ffffff",
"images": [{"image_url": row["image_url"]} for row in batch],
})
if r.status_code == 503: # queue full — back off
import time; time.sleep(int(r.headers.get("Retry-After", 30))); continue
r.raise_for_status()
for row, job_id in zip(batch, r.json()["job_ids"]):
save_mapping(row["sku"], job_id) # your DBOrder is preserved: job_ids[i] corresponds to images[i]. Each image URL is fetched server-side with a 25 MB limit; private-network URLs are refused, so use signed public URLs for S3 or GCS.
Callbacks instead of polling
Send an X-Callback-Url header with the batch (or single job) request and the API POSTs a JSON body to it when each job finishes — job_id, status (done or failed), the result url on success and error on failure. Callbacks are signed with an HMAC header so you can reject forged requests; the docs show the verification snippet.
// Express receiver
app.post("/hooks/bgclear", express.json(), async (req, res) => {
const { job_id, status, url, error } = req.body;
res.sendStatus(200); // ack first, work after
const sku = await skuForJob(job_id);
if (status === "done") {
const bytes = Buffer.from(await (await fetch(url)).arrayBuffer());
await uploadToCdn(`${sku}.webp`, bytes); // result URLs expire after 24 h
} else {
await markFailed(sku, error); // failed jobs are not charged
}
});Ack with a 2xx immediately and do the download afterwards; a slow handler is the usual reason callbacks look "lost". If you cannot expose a URL, poll GET /api/v1/jobs/{id} every few seconds — but poll only jobs still in queued or processing.
Back-pressure: 429, 503 and the 24-hour window
Three limits shape a bulk run. The per-key rate limit is 60 requests a minute — batch calls count as one request, which is why 50-image batches matter. The GPU queue has a depth cap; when it is full you get 503 gpu_unavailable with a Retry-After header — sleep and resubmit that batch, nothing was queued or charged. And result files are kept for 24 hours, so download promptly; the callback pattern above does this by design.
A 10,000-image catalogue is 200 batch requests, comfortably inside the rate limit; throughput is then set by queue depth and image size rather than by your client.
Credit accounting you can reconcile
Each full-resolution job costs one credit, charged when the job succeeds — never on failure, never on a rejected submission. The job status response includes credits_charged and credits_remaining; GET /api/v1/usage lists charges by day for your finance export. Before a large run, check GET /api/v1/account for the balance and buy a pack sized to the whole catalogue: credits never expire, so over-buying costs nothing. Preview-size jobs (size=preview, ≤800 px) are free and are a good dry run for a new supplier feed.
Frequently asked questions
How many images can I process per hour?
Submission is limited to 60 requests a minute per key, i.e. up to 3,000 images a minute via 50-image batches. Actual throughput is governed by the shared GPU queue; large runs are best submitted with callbacks and left to drain.
Can I upload files in a batch, or only URLs?
Batch is JSON: image_url or base64 image_file_b64 per item. For local files, either host them at signed URLs or submit single multipart jobs to /api/v1/jobs.
What happens if my callback endpoint is down?
Poll GET /api/v1/jobs/{id} for any job whose callback you did not receive; the status and result URL stay available for 24 hours.
Is batch available on RapidAPI?
No — RapidAPI bills per request, so batch is only available with a direct bgclear key. Single jobs and sync calls work on both.