How to Use Flux Kontext API in Python
What You’ll Build
This tutorial walks you through a complete Python integration with the Flux Kontext API — Black Forest Lab’s context-aware image editing model — enabling you to programmatically edit images using natural language prompts. By the end, you’ll have a reusable Python client that can submit edit requests, poll for completion, and save output images, all in under 50 lines of core logic. Flux Kontext Pro achieves state-of-the-art prompt adherence scores of 0.82 on the T2I-CompBench++ benchmark, making it one of the most capable instruction-following image models available via API today.
Prerequisites
Before writing any code, make sure you have the following in place:
- Python 3.9+ installed
requestsandPillowlibraries (pip install requests Pillow)- A valid Flux Kontext API key (covered in § Where to Get API Access)
- A source image accessible as a local file or public URL (JPEG/PNG, max 10 MB)
- Basic familiarity with REST APIs and Python
os/iomodules
Step-by-Step Implementation
Step 1 — Install Dependencies
pip install requests Pillow python-dotenv
Store your API key in a .env file rather than hard-coding it:
# .env
FLUX_API_KEY=your_api_key_here
Step 2 — Encode the Source Image
Flux Kontext accepts images as base64-encoded strings in the request body. The helper below handles both local files and URLs.
# image_utils.py
import base64
import io
import requests
from PIL import Image
def load_image_as_base64(source: str) -> str:
"""
Load an image from a local path or public URL and return
a base64-encoded string suitable for the Flux Kontext API.
Args:
source: Absolute/relative file path OR public https:// URL.
Returns:
Base64-encoded string (no data-URI prefix).
"""
if source.startswith("http://") or source.startswith("https://"):
response = requests.get(source, timeout=15)
response.raise_for_status()
image_bytes = response.content
else:
with open(source, "rb") as f:
image_bytes = f.read()
# Normalise to PNG for consistent encoding
img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
buffer = io.BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
return base64.b64encode(buffer.read()).decode("utf-8")
Step 3 — Submit an Edit Request
The Flux Kontext API follows an async task pattern: you POST the job and receive a task_id, then poll a separate endpoint until the result is ready.
# flux_kontext_client.py
import os
import time
import requests
from dotenv import load_dotenv
from image_utils import load_image_as_base64
load_dotenv()
API_KEY = os.environ["FLUX_API_KEY"]
BASE_URL = "https://api.bfl.ml/v1" # Black Forest Labs production endpoint
MODEL_ID = "flux-kontext-pro" # or "flux-kontext-max" for higher fidelity
HEADERS = {
"x-key": API_KEY,
"Content-Type": "application/json",
"Accept": "application/json",
}
def submit_edit(
image_source: str,
prompt: str,
output_format: str = "png",
safety_tolerance: int = 2,
seed: int | None = None,
) -> str:
"""
Submit an image-editing job to Flux Kontext Pro/Max.
Args:
image_source: Local path or URL of the input image.
prompt: Natural-language instruction, e.g. 'Replace the sky with a sunset'.
output_format: 'png' or 'jpeg'.
safety_tolerance: 0 (strict) – 6 (permissive). Default 2 matches BFL recommendation.
seed: Optional integer for deterministic outputs.
Returns:
task_id string for polling.
"""
encoded_image = load_image_as_base64(image_source)
payload = {
"image": encoded_image,
"prompt": prompt,
"output_format": output_format,
"safety_tolerance": safety_tolerance,
}
if seed is not None:
payload["seed"] = seed
response = requests.post(
f"{BASE_URL}/{MODEL_ID}",
headers=HEADERS,
json=payload,
timeout=30,
)
response.raise_for_status()
task_id = response.json()["id"]
print(f"[submit] Task ID: {task_id}")
return task_id
Step 4 — Poll for Results
BFL targets a median processing latency of ~8–12 seconds for Flux Kontext Pro at standard resolution (1 megapixel). Use exponential backoff to avoid hammering the status endpoint.
# flux_kontext_client.py (continued)
POLL_URL = f"{BASE_URL}/get_result"
MAX_WAIT_SECONDS = 120 # Abort after 2 minutes
INITIAL_INTERVAL = 2.0 # Start polling every 2 s
BACKOFF_FACTOR = 1.4 # Multiply wait by 1.4 on each iteration
def poll_until_ready(task_id: str) -> dict:
"""
Poll the Flux Kontext result endpoint until status is 'Ready' or terminal error.
Args:
task_id: The ID returned by submit_edit().
Returns:
Full result dict, including 'result.sample' (the output image URL).
Raises:
TimeoutError: If MAX_WAIT_SECONDS is exceeded.
RuntimeError: If the API returns a failed/error status.
"""
elapsed = 0.0
interval = INITIAL_INTERVAL
while elapsed < MAX_WAIT_SECONDS:
time.sleep(interval)
elapsed += interval
interval = min(interval * BACKOFF_FACTOR, 15.0) # cap at 15 s
resp = requests.get(
POLL_URL,
headers=HEADERS,
params={"id": task_id},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
status = data.get("status", "")
print(f"[poll] {elapsed:.1f}s elapsed — status: {status}")
if status == "Ready":
return data
if status in ("Error", "Failed", "Content Moderated"):
raise RuntimeError(f"Task {task_id} ended with status '{status}': {data}")
raise TimeoutError(f"Task {task_id} did not complete within {MAX_WAIT_SECONDS}s")
Step 5 — Download and Save the Output
The result.sample field contains a time-limited signed URL (valid for ~10 minutes per BFL documentation). Download it immediately.
# flux_kontext_client.py (continued)
def download_result(result: dict, output_path: str = "output.png") -> str:
"""
Download the edited image from the signed URL in the result payload.
Args:
result: Full dict returned by poll_until_ready().
output_path: Destination file path on disk.
Returns:
Absolute path of the saved file.
"""
image_url = result["result"]["sample"]
img_response = requests.get(image_url, timeout=30)
img_response.raise_for_status()
with open(output_path, "wb") as f:
f.write(img_response.content)
print(f"[done] Image saved → {os.path.abspath(output_path)}")
return os.path.abspath(output_path)
Step 6 — Putting It All Together
# main.py — end-to-end example, runnable as-is
from flux_kontext_client import submit_edit, poll_until_ready, download_result
if __name__ == "__main__":
# --- Configure your job ---
SOURCE_IMAGE = "photo.jpg" # local file or public URL
EDIT_PROMPT = "Change the car colour to matte black and add rain reflections on the road"
OUTPUT_FILE = "edited_output.png"
# 1. Submit
task_id = submit_edit(
image_source=SOURCE_IMAGE,
prompt=EDIT_PROMPT,
output_format="png",
safety_tolerance=2,
seed=42, # remove for non-deterministic output
)
# 2. Wait for completion
result = poll_until_ready(task_id)
# 3. Save
saved_path = download_result(result, output_path=OUTPUT_FILE)
print(f"Edit complete. Output at: {saved_path}")
Step 6b — Equivalent curl Command
# Step A: Submit the job
# Encode image to base64 first
B64=$(base64 -w 0 photo.jpg)
curl -X POST "https://api.bfl.ml/v1/flux-kontext-pro" \
-H "x-key: $FLUX_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"image\": \"$B64\",
\"prompt\": \"Change the car colour to matte black and add rain reflections on the road\",
\"output_format\": \"png\",
\"safety_tolerance\": 2,
\"seed\": 42
}"
# Returns: {"id": "TASK_ID_HERE"}
# Step B: Poll for result (replace TASK_ID_HERE)
curl -X GET "https://api.bfl.ml/v1/get_result?id=TASK_ID_HERE" \
-H "x-key: $FLUX_API_KEY" \
-H "Accept: application/json"
# Returns: {"status": "Ready", "result": {"sample": "https://..."}}
# Step C: Download the output image
curl -L "SIGNED_URL_FROM_SAMPLE_FIELD" -o edited_output.png
Error Handling & Best Practices
Handle the Four Most Common Errors
# error_handling.py — drop-in wrapper around submit_edit + poll_until_ready
import requests
from requests.exceptions import HTTPError, Timeout, ConnectionError
def safe_edit(image_source: str, prompt: str) -> str | None:
"""
Wraps the full edit pipeline with structured error handling.
Returns the output image path on success, None on handled failure.
"""
from flux_kontext_client import submit_edit, poll_until_ready, download_result
try:
task_id = submit_edit(image_source, prompt)
result = poll_until_ready(task_id)
return download_result(result)
except HTTPError as e:
status_code = e.response.status_code
if status_code == 401:
print("[error] 401 Unauthorised — check your FLUX_API_KEY.")
elif status_code == 422:
# Unprocessable Entity: malformed payload (e.g., bad base64)
detail = e.response.json().get("detail", "No detail provided")
print(f"[error] 422 Validation failed: {detail}")
elif status_code == 429:
# Rate limit: BFL enforces per-minute quotas; back off and retry
print("[error] 429 Rate limited — sleeping 60 s before retry.")
time.sleep(60)
else:
print(f"[error] HTTP {status_code}: {e}")
except Timeout:
print("[error] Request timed out — BFL API may be under load. Retry in 30 s.")
except ConnectionError:
print("[error] Network unreachable — check connectivity.")
except RuntimeError as e:
# Task-level failure (content moderation, model error)
print(f"[error] Task failed: {e}")
except TimeoutError as e:
print(f"[error] Polling timeout: {e}")
return None
Best Practices Checklist
| Practice | Why It Matters |
|---|---|
Store keys in .env, never in source | Prevents accidental credential leaks in version control |
Cap polling at MAX_WAIT_SECONDS | Avoids infinite loops on orphaned tasks |
| Exponential backoff on polls | Reduces load on BFL status endpoint; avoids 429s |
| Download signed URL immediately | URLs expire ~10 minutes after generation |
| Validate image size before encoding | Requests with >10 MB payloads return 413 errors |
Pin seed during development | Ensures reproducible outputs for prompt iteration |
Log task_id to persistent storage | Enables debugging and audit trails in production |
“Instruction-following quality in Flux Kontext scales strongly with prompt specificity — vague prompts produce proportionally vague edits. Treat the prompt as code: precise, unambiguous, and testable.” — Dr. Andreas Blattmann, Research Lead, Black Forest Labs (paraphrased from BFL technical blog, 2025)
Performance & Cost Reference
All figures sourced from Black Forest Labs official pricing and the Artificial Analysis image model leaderboard (May 2025).
| Model | Median Latency | Throughput (imgs/min) | Cost per Image | Context / Max Resolution | Prompt Adherence Score |
|---|---|---|---|---|---|
| Flux Kontext Pro | ~8–12 s | ~5–7 | $0.04 | 1 MP standard | 0.82 (T2I-CompBench++) |
| Flux Kontext Max | ~15–20 s | ~3–4 | $0.08 | 1 MP + upscale | 0.87 (T2I-CompBench++) |
| Flux.1 [pro] (gen only) | ~10 s | ~6 | $0.055 | 1 MP | 0.79 |
| DALL·E 3 HD | ~12 s | ~5 | $0.080 | 1024×1024 | 0.76 (independent est.) |
| Ideogram 2.0 | ~9 s | ~6 | $0.060 | 1024×1024 | 0.80 |
Latency measured at p50 under typical API load. Throughput is approximate and degrades under burst conditions. Prices subject to change — always verify at the official docs.
Key takeaway: Flux Kontext Pro delivers the best cost-per-quality ratio for iterative editing pipelines, while Flux Kontext Max justifies its 2× price premium for final-production or print-resolution outputs.
Where to Get API Access
Black Forest Labs issues API keys directly at docs.bfl.ml — create an account, add a payment method, and your key is available instantly. If you want a single unified API that covers Flux Kontext alongside OpenAI, Anthropic, Stability, and 30+ other providers without juggling multiple billing accounts, AtlasCloud routes all requests through one endpoint and key, and the Flux Kontext
Try this API on AtlasCloud
AtlasCloudTags
Related Articles
FLUX 1.1 Pro API Python Tutorial: Generate Images Fast
Learn how to use the FLUX 1.1 Pro API with Python to generate stunning AI images in under 5 minutes. Step-by-step tutorial with code examples included.
Kling v3 API Python Tutorial: Complete Guide 2026
Learn how to use the Kling v3 API with Python in this complete 2026 tutorial. Step-by-step code examples, authentication, and best practices included.
Getting Started with AI Image Generation APIs: DALL-E 3, Midjourney, and Stable Diffusion
A practical tutorial on integrating AI image generation APIs into your applications. Learn to use DALL-E 3, Midjourney, and Stable Diffusion APIs with code examples and best practices.