Tutorials

How to Use Flux Kontext API in Python

AI API Playbook · · 9 min read
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
  • requests and Pillow libraries (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/io modules

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

PracticeWhy It Matters
Store keys in .env, never in sourcePrevents accidental credential leaks in version control
Cap polling at MAX_WAIT_SECONDSAvoids infinite loops on orphaned tasks
Exponential backoff on pollsReduces load on BFL status endpoint; avoids 429s
Download signed URL immediatelyURLs expire ~10 minutes after generation
Validate image size before encodingRequests with >10 MB payloads return 413 errors
Pin seed during developmentEnsures reproducible outputs for prompt iteration
Log task_id to persistent storageEnables 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).

ModelMedian LatencyThroughput (imgs/min)Cost per ImageContext / Max ResolutionPrompt Adherence Score
Flux Kontext Pro~8–12 s~5–7$0.041 MP standard0.82 (T2I-CompBench++)
Flux Kontext Max~15–20 s~3–4$0.081 MP + upscale0.87 (T2I-CompBench++)
Flux.1 [pro] (gen only)~10 s~6$0.0551 MP0.79
DALL·E 3 HD~12 s~5$0.0801024×10240.76 (independent est.)
Ideogram 2.0~9 s~6$0.0601024×10240.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

AtlasCloud

Tags

Flux Image Generation Python API Tutorial Black Forest Labs

Related Articles