← Code Recipes
Image & Video

Black Forest Labs FLUX Free code examples in Python and JavaScript

FLUX image models — currently the strongest open-weight image family.

C# Python Java JavaScript

WHAT THIS PAGE COVERS

The async submit-then-poll job shape most image APIs use.

Model flux-2-pro
Package requests
Checked

Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.

Install

A plain REST API — requests is all it needs. The key header is x-key, not Authorization, which is the first thing to check on a 401.

pip install requests

export BFL_API_KEY="your-key"     # macOS / Linux
setx BFL_API_KEY "your-key"       # Windows, then open a new shell
Submit a job

THE ENDPOINT IS THE MODEL. There is no model field in the body — /flux-2-pro and /flux-2-flex are different URLs. The response is an id and a polling_url, not an image.

import os
import requests

submitted = requests.post(
    "https://api.bfl.ai/v1/flux-2-pro",
    headers={
        "accept": "application/json",
        "x-key": os.environ["BFL_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "prompt": "A studio photograph of a brass microphone on a dark desk",
        "width": 1024,
        "height": 1024,
    },
    timeout=30,
).json()

print(submitted["id"], submitted["polling_url"])
Polling it correctly

Poll the URL THEY RETURNED. Results are served from region-specific hosts, so a rebuilt URL asks the wrong region and never sees the job finish. And handle all four failure states, or a moderated prompt loops forever.

import time

TERMINAL_FAILURES = {"Error", "Failed", "Request Moderated", "Content Moderated"}

def wait_for(polling_url: str, timeout_seconds: int = 300) -> str:
    deadline = time.monotonic() + timeout_seconds

    while time.monotonic() < deadline:
        time.sleep(0.5)

        result = requests.get(
            polling_url,
            headers={
                "accept": "application/json",
                "x-key": os.environ["BFL_API_KEY"],
            },
            timeout=30,
        ).json()

        status = result["status"]

        if status == "Ready":
            return result["result"]["sample"]

        if status in TERMINAL_FAILURES:
            raise RuntimeError(f"FLUX returned {status}")

    raise TimeoutError("FLUX job did not finish in time")
Downloading before the link dies

TEN MINUTES. The signed URL expires, so fetch the bytes in the same breath as the poll. Persisting the URL instead of the image is the mistake that only shows up the next day.

sample_url = wait_for(submitted["polling_url"])

# Do this now, not later. The URL is signed and short-lived.
image = requests.get(sample_url, timeout=60)
image.raise_for_status()

with open("microphone.png", "wb") as handle:
    handle.write(image.content)
A webhook instead of polling

For anything running in production, ask BFL to call you. Polling a queue of jobs at two requests a second per job is how a worker pool ends up doing nothing else.

submitted = requests.post(
    "https://api.bfl.ai/v1/flux-2-pro",
    headers={
        "accept": "application/json",
        "x-key": os.environ["BFL_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "prompt": "A studio photograph of a brass microphone on a dark desk",
        "width": 1024,
        "height": 1024,
        "webhook_url": "https://example.com/hooks/flux",
    },
    timeout=30,
).json()

# The callback carries the same status values as the poll, so the
# TERMINAL_FAILURES check above applies unchanged. Download the
# sample inside the handler — the ten-minute clock starts when the
# job finishes, not when you get round to it.
Install

Nothing to install on modern Node. The header is x-key; an Authorization header is ignored and you get a 401 with no hint.

# Node 18+ has fetch built in.
export BFL_API_KEY="your-key"
Submit a job

The endpoint is the model — /flux-2-pro, /flux-2-flex, /flux-kontext-pro are different URLs rather than a field. The response is a job, not an image.

const submit = async (prompt) => {
  const response = await fetch("https://api.bfl.ai/v1/flux-2-pro", {
    method: "POST",
    headers: {
      accept: "application/json",
      "x-key": process.env.BFL_API_KEY,
      "content-type": "application/json",
    },
    body: JSON.stringify({ prompt, width: 1024, height: 1024 }),
  });

  if (!response.ok) throw new Error(await response.text());

  // { id, polling_url }
  return response.json();
};

const job = await submit(
  "A studio photograph of a brass microphone on a dark desk",
);
Polling it correctly

USE job.polling_url. Results live on region-specific hosts, so a reconstructed URL polls somewhere the job will never appear. Four statuses are terminal failures, not one.

const TERMINAL_FAILURES = new Set([
  "Error", "Failed", "Request Moderated", "Content Moderated",
]);

async function waitFor(pollingUrl, timeoutMs = 300_000) {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    await new Promise((resolve) => setTimeout(resolve, 500));

    const result = await fetch(pollingUrl, {
      headers: {
        accept: "application/json",
        "x-key": process.env.BFL_API_KEY,
      },
    }).then((r) => r.json());

    if (result.status === "Ready") return result.result.sample;

    if (TERMINAL_FAILURES.has(result.status)) {
      throw new Error(`FLUX returned ${result.status}`);
    }
  }

  throw new Error("FLUX job did not finish in time");
}
Downloading before the link dies

Ten minutes from ready. Fetch the bytes as soon as you have the URL — storing the link and resolving it later gives you a gallery of expired signatures.

import { writeFile } from "node:fs/promises";

const sampleUrl = await waitFor(job.polling_url);

// Immediately. The signature expires in ten minutes.
const image = await fetch(sampleUrl);
if (!image.ok) throw new Error(`Download failed: ${image.status}`);

await writeFile("microphone.png", Buffer.from(await image.arrayBuffer()));
Editing with an input image

FLUX.2 takes input_image, input_image_2 and so on. PASS A URL WHERE YOU CAN — the API fetches it itself, and base64 makes the request body enormous for no benefit.

const edit = await fetch("https://api.bfl.ai/v1/flux-kontext-pro", {
  method: "POST",
  headers: {
    accept: "application/json",
    "x-key": process.env.BFL_API_KEY,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    prompt: "Replace the desk with dark walnut, keep the microphone",
    // A URL is fetched server-side; base64 also works and is bigger.
    input_image: "https://example.com/assets/microphone.png",
  }),
}).then((r) => r.json());

const editedUrl = await waitFor(edit.polling_url);
const edited = await fetch(editedUrl);
await writeFile("edited.png", Buffer.from(await edited.arrayBuffer()));

QUESTIONS

Which languages are the Black Forest Labs FLUX examples in?

Python and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.

Which package do I install for Black Forest Labs FLUX?

The samples use requests. The install step on each language tab gives the exact command.

Which Black Forest Labs FLUX model do these examples use?

flux-2-pro, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.

Where is the official Black Forest Labs FLUX documentation?

https://docs.bfl.ai/ — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.

ALSO IN IMAGE & VIDEO

The authority

Official Black Forest Labs FLUX documentation

Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.

Read the docs ↗

https://docs.bfl.ai/

New drops, every week

Free tools & Claude templates, landing regularly.

We keep shipping new free tools, code recipes and Claude Code templates. Follow @freeyantra on Instagram or join the newsletter so you never miss one.

Follow @freeyantra

or get them by email

Double opt-in. Unsubscribe any time. Privacy