If your product is built on Django, adding background removal takes one model, one service function, and one view. This tutorial builds a minimal upload flow that stores both the original image and the transparent result.
1. Store your API key in settings
Keep the key out of source control and read it from the environment.
# settings.py
import os
FAPIHUB_API_KEY = os.environ["FAPIHUB_API_KEY"]
2. Create a model
# models.py
from django.db import models
class ProcessedImage(models.Model):
original = models.ImageField(upload_to="originals/")
cutout = models.ImageField(upload_to="cutouts/", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
3. Write a service function
Put the API call in its own function so views, admin actions, and background tasks can share it.
# services.py
from pathlib import Path
import requests
from django.conf import settings
from django.core.files.base import ContentFile
def remove_background(processed_image, model="falcon"):
processed_image.original.open("rb")
try:
response = requests.post(
"https://fapihub.com/v2/rembg/",
headers={"ApiKey": settings.FAPIHUB_API_KEY},
files={"image": processed_image.original.file},
data={"model": model},
timeout=60,
)
finally:
processed_image.original.close()
response.raise_for_status()
name = f"{Path(processed_image.original.name).stem}.png"
processed_image.cutout.save(name, ContentFile(response.content), save=True)
return processed_image
4. Add a form and view
# views.py
from django import forms
from django.shortcuts import redirect, render
from .models import ProcessedImage
from .services import remove_background
class UploadForm(forms.ModelForm):
class Meta:
model = ProcessedImage
fields = ["original"]
def upload(request):
form = UploadForm(request.POST or None, request.FILES or None)
if form.is_valid():
image = form.save()
remove_background(image)
return redirect("image_detail", pk=image.pk)
return render(request, "upload.html", {"form": form})
Remember enctype="multipart/form-data" on the form in upload.html, or Django will not receive the file.
5. Move processing to a background task
Calling the API inside the request works for prototypes, but users wait for the response and a slow network ties up a worker. In production, save the upload, enqueue a Celery or RQ task that calls remove_background, and show a "processing" state until the cutout is ready.
# tasks.py
from celery import shared_task
from .models import ProcessedImage
from .services import remove_background
@shared_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=3)
def remove_background_task(image_id):
remove_background(ProcessedImage.objects.get(pk=image_id))
Handling errors
- 401: the API key is missing or inactive. Check the environment variable.
- 429: you exceeded the per-minute rate limit. Retry after the
Retry-Afterheader. - 5xx: temporary server issue. Retry with backoff. These do not count against your quota.
For more Python examples, see our Python tutorial and the Python integration guide.
6. Validate uploads before spending a request
The API accepts JPEG, PNG, WebP, BMP, and GIF up to 10 MB. Invalid images return 400 invalid_image, and 4xx responses count against your quota, so reject bad files in the form instead.
# forms.py
from django import forms
from .models import ProcessedImage
MAX_BYTES = 10 * 1024 * 1024
ALLOWED_FORMATS = {"JPEG", "PNG", "WEBP", "BMP", "GIF"}
class UploadForm(forms.ModelForm):
class Meta:
model = ProcessedImage
fields = ["original"]
def clean_original(self):
upload = self.cleaned_data["original"]
if upload.size > MAX_BYTES:
raise forms.ValidationError("Images must be 10 MB or smaller.")
# ImageField has already opened the file with Pillow
image_format = getattr(upload.image, "format", None)
if image_format not in ALLOWED_FORMATS:
raise forms.ValidationError("Upload a JPEG, PNG, WebP, BMP, or GIF image.")
return upload
Django's ImageField already rejects files Pillow cannot open, and it exposes the detected format on upload.image. Checking the actual format is more reliable than trusting the file extension. Move UploadForm from views.py into this file and import it in the view.
7. Track status on the model
Once processing runs in a task, the page needs to know whether the cutout is pending, ready, or failed. Add a status field and the API error details.
# models.py
class ProcessedImage(models.Model):
class Status(models.TextChoices):
PENDING = "pending"
READY = "ready"
FAILED = "failed"
original = models.ImageField(upload_to="originals/")
cutout = models.ImageField(upload_to="cutouts/", blank=True)
status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING)
error_code = models.CharField(max_length=50, blank=True)
request_id = models.CharField(max_length=100, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
Run python manage.py makemigrations and migrate after the change. The detail template can then show a spinner while the status is pending and refresh until it changes.
8. Retry only what is retryable
The task in step 5 retries on any exception. That is fine for a prototype, but it also retries a 400 invalid_image or a 402 insufficient_credits, which will fail the same way each time and still count against your quota. Raise distinct exceptions from the service and retry only the temporary ones.
# services.py
class RetryableAPIError(Exception):
def __init__(self, message, retry_after=None):
super().__init__(message)
self.retry_after = retry_after
class PermanentAPIError(Exception):
def __init__(self, code, request_id):
super().__init__(code)
self.code = code
self.request_id = request_id
def check_response(response):
if response.status_code == 200:
return
if response.status_code == 429:
raise RetryableAPIError("rate limited", int(response.headers.get("Retry-After", 10)))
if response.status_code >= 500:
raise RetryableAPIError(f"server error {response.status_code}")
error = response.json().get("error", {})
raise PermanentAPIError(error.get("code", "unknown"), error.get("request_id", ""))
Call check_response(response) in place of response.raise_for_status() in remove_background, and update the task:
# tasks.py
from celery import shared_task
from .models import ProcessedImage
from .services import PermanentAPIError, RetryableAPIError, remove_background
@shared_task(bind=True, max_retries=5)
def remove_background_task(self, image_id):
image = ProcessedImage.objects.get(pk=image_id)
try:
remove_background(image)
except RetryableAPIError as error:
raise self.retry(exc=error, countdown=error.retry_after or 2 ** self.request.retries)
except PermanentAPIError as error:
image.status = ProcessedImage.Status.FAILED
image.error_code = error.code
image.request_id = error.request_id
image.save(update_fields=["status", "error_code", "request_id"])
return
image.status = ProcessedImage.Status.READY
image.save(update_fields=["status"])
A 401 invalid_api_key or 402 insufficient_credits affects every image, so alert your team when you see either one instead of only marking the image as failed. Keep the request_id: it identifies the exact request if you need to contact support.
9. Reuse the service for other effects
The same pattern works for every endpoint. A small generalization lets admin actions offer white backgrounds, blur, or shadows without new plumbing.
# services.py
EFFECTS = {
"white": ("/v2/rembg/color/", {"bgcolor": "(255, 255, 255, 255)"}),
"blur": ("/v2/rembg/blur/", {"radius": 20}),
"shadow": ("/v2/rembg/shadow/", {"shadow_color": "(0, 0, 0, 150)", "offset": "(10, 10)", "blur_radius": 15}),
}
def apply_effect(file_obj, effect):
path, data = EFFECTS[effect]
response = requests.post(
f"https://fapihub.com{path}",
headers={"ApiKey": settings.FAPIHUB_API_KEY},
files={"image": file_obj},
data=data,
timeout=60,
)
check_response(response)
return response.content
Every endpoint costs the same per request. See the blur background guide and the shadow guide for parameter choices.
10. Test without spending quota
Unit tests should never call the real API. Patch requests.post and return a fake PNG so the test exercises your storage and status logic.
# tests.py
import io
from unittest import mock
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from PIL import Image
from .models import ProcessedImage
from .services import remove_background
def png_bytes():
buffer = io.BytesIO()
Image.new("RGBA", (10, 10), (0, 0, 0, 0)).save(buffer, "PNG")
return buffer.getvalue()
@override_settings(FAPIHUB_API_KEY="test-key")
class RemoveBackgroundTests(TestCase):
@mock.patch("core.services.requests.post")
def test_saves_cutout(self, post):
post.return_value = mock.Mock(status_code=200, content=png_bytes())
upload = SimpleUploadedFile("photo.png", png_bytes(), content_type="image/png")
image = ProcessedImage.objects.create(original=upload)
remove_background(image)
self.assertTrue(image.cutout.name.endswith(".png"))
self.assertEqual(post.call_args.kwargs["headers"]["ApiKey"], "test-key")
Replace core.services with your app's module path. Use a temporary MEDIA_ROOT in test settings so files do not pile up. For a manual end-to-end check, the free plan's 100 requests per month is plenty, and the playground lets you compare models before writing any code.
Frequently asked questions
Should I call the API in the view or in a task?
Calling it in the view is fine for internal tools and prototypes. For user-facing features, use a Celery or RQ task so web workers are not blocked while the image is processed, and show the status field to the user.
Does this work with Django REST Framework?
Yes. The service function does not depend on forms or templates. Call it, or enqueue the task, from an API view or serializer create method and return the image's ID and status in the response.
Does it work with S3 or other remote storage?
Yes. The service saves the result with ContentFile through the model field, so it uses whatever storage backend you configure, including django-storages. Opening the original goes through the same backend.
Which model value should I pass?
falcon is the default, and aurora and ghost are also available for the remove and mask endpoints. Try each on your own images and keep the one that looks best. You can store the choice per image if users can pick.