Background Removal in Flutter and React Native Apps: the Safe API Pattern
The r/FlutterDev thread asking for a background remover API is still one of Google's top results for the query, and most answers skip the part that matters: a key embedded in a mobile app is public within a day of release. This guide shows the pattern that survives — a small proxy on your backend that holds the bgclear key, plus the Flutter and React Native code that talks to it. The proxy is 30 lines; the mobile code is mostly a multipart upload.
In this guide
Why a proxy, and what it looks like
Anyone can unzip an APK or IPA and grep for bgc_live_. Once the key leaks, strangers spend your credits. So the app never talks to bgclear directly: it uploads to /cutout on your server, which forwards the file with the key and streams the PNG back. That endpoint also lets you enforce login and per-user quotas. A Node/Express version (the same idea works in any stack — the PHP and Python guides have equivalents):
import express from "express";
import multer from "multer";
const app = express();
const upload = multer({ limits: { fileSize: 25 * 1024 * 1024 } });
app.post("/cutout", requireUser, upload.single("image"), async (req, res) => {
const form = new FormData();
form.append("image_file", new Blob([req.file.buffer]), req.file.originalname);
form.append("size", req.query.size === "full" ? "full" : "preview"); // preview is free
form.append("format", "png");
const r = await fetch("https://www.bgclear.ai/api/v1/remove", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BGCLEAR_API_KEY}`,
"Idempotency-Key": `${req.user.id}-${req.file.size}-${req.file.originalname}`,
},
body: form,
});
if (!r.ok) return res.status(r.status).json(await r.json());
res.type("image/png").send(Buffer.from(await r.arrayBuffer()));
});Serve previews (free, ≤800 px) by default and full resolution only when the user confirms an export — that single decision keeps the credit bill proportional to real usage.
Flutter: http.MultipartRequest
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
Future<Uint8List> removeBackground(XFile picked, {bool full = false}) async {
final uri = Uri.parse('https://api.yourapp.com/cutout?size=${full ? "full" : "preview"}');
final req = http.MultipartRequest('POST', uri)
..headers['Authorization'] = 'Bearer $userSessionToken' // YOUR auth, not the bgclear key
..files.add(await http.MultipartFile.fromPath('image', picked.path, filename: picked.name));
final streamed = await req.send().timeout(const Duration(seconds: 60));
final res = await http.Response.fromStream(streamed);
if (res.statusCode != 200) {
// The proxy passes bgclear's JSON error through: {"error": {"code": ..., "message": ...}}
throw Exception('cutout failed ${res.statusCode}: ${res.body}');
}
return res.bodyBytes; // show with Image.memory(bytes), save with image_gallery_saver etc.
}Compress before upload with image_picker's imageQuality or flutter_image_compress; a 12 MP phone photo is 4–6 MB and the preview result is 800 px anyway. Show the preview instantly, then offer "Export HD" which re-requests with full=true.
React Native: fetch + FormData
import * as FileSystem from "expo-file-system";
export async function removeBackground(uri, { full = false } = {}) {
const form = new FormData();
form.append("image", { uri, name: "photo.jpg", type: "image/jpeg" });
const res = await fetch(`https://api.yourapp.com/cutout?size=${full ? "full" : "preview"}`, {
method: "POST",
headers: { Authorization: `Bearer ${sessionToken}` }, // your session, never the bgclear key
body: form, // let fetch set the multipart boundary
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error?.code}: ${error?.message}`);
}
// Persist the PNG so <Image source={{ uri }} /> can show it
const blob = await res.blob();
const base64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(",")[1]);
reader.readAsDataURL(blob);
});
const out = `${FileSystem.cacheDirectory}cutout-${Date.now()}.png`;
await FileSystem.writeAsStringAsync(out, base64, { encoding: FileSystem.EncodingType.Base64 });
return out;
}On Android, the file:// URI from the image picker works directly in FormData; on iOS, ph:// URIs need copying to a file first (expo-image-picker does this for you).
Errors, offline and cost
Surface three cases in the UI: 402 insufficient_credits from the proxy means your account is empty — that is your alert, not the user's; 413 or 400 image_too_large means compress harder (limit is 25 MB and 50 MP); a timeout means retry with the same file — the proxy's idempotency key makes that free. Queue requests when offline instead of failing; the result URL semantics do not matter here because the proxy returns bytes.
Cost is one credit per full-resolution export, from $0.065 to $0.09 depending on the pack; previews cost nothing, so a freemium app can give unlimited previews and charge for HD. Pricing. If your app used remove.bg, note its API ends on 1 December 2026; the proxy above is field-compatible — migration page.
Frequently asked questions
Can I skip the proxy for a prototype?
For a local prototype, yes — call https://www.bgclear.ai/api/v1/remove directly with the Bearer header. Never ship that build; rotate the key from the dashboard before release.
Is there an on-device option?
On-device models exist (TFLite/ONNX U²-Net variants) but cost app size, battery and quality on hair. The API pattern gives full-quality results and a 5-second round trip on mobile data.
How do I show progress?
Show the picked image immediately, overlay a spinner, and swap in the preview when it arrives (typically 2–5 s). Request full resolution only on export.
What does it cost per user?
Previews are free. A full-resolution export is one credit ($0.065–0.09). Most apps charge users for HD exports and keep previews free.