> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bfl.ml/llms.txt
> Use this file to discover all available pages before exploring further.

# FLUX Video Upscale

> Upscale videos to 1080p, 2K, or 4K via API. FLUX 3 powered super-resolution with a precise mode and a creative detail-enhancement mode.

export const VideoComparisonSlider = ({beforeVideo, afterVideo, beforeLabel = "Before", afterLabel = "After", height = "500px", objectFit = "cover", poster}) => {
  const [position, setPosition] = useState(50);
  const [playing, setPlaying] = useState(true);
  const beforeRef = useRef(null);
  const afterRef = useRef(null);
  const DRIFT_S = 0.04;
  const SEEK_S = 0.5;
  const getPosition = (e, container) => {
    const rect = container.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const x = clientX - rect.left;
    return Math.max(0, Math.min(100, x / rect.width * 100));
  };
  const onPointerDown = e => {
    e.preventDefault();
    e.stopPropagation();
    const container = e.currentTarget;
    setPosition(getPosition(e, container));
    const onMove = ev => {
      ev.preventDefault();
      setPosition(getPosition(ev, container));
    };
    const onUp = () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      window.removeEventListener("touchmove", onMove);
      window.removeEventListener("touchend", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    window.addEventListener("touchmove", onMove, {
      passive: false
    });
    window.addEventListener("touchend", onUp);
  };
  const loopPoint = () => {
    const a = beforeRef.current;
    const b = afterRef.current;
    if (!a || !b) return 0;
    const da = Number.isFinite(a.duration) ? a.duration : 0;
    const db = Number.isFinite(b.duration) ? b.duration : 0;
    if (!da || !db) return da || db;
    return Math.min(da, db);
  };
  const resync = force => {
    const a = beforeRef.current;
    const b = afterRef.current;
    if (!a || !b) return;
    if (b.readyState < 1) return;
    const delta = b.currentTime - a.currentTime;
    if (force || Math.abs(delta) > SEEK_S) {
      try {
        b.currentTime = a.currentTime;
      } catch (e) {}
      b.playbackRate = 1;
    } else if (Math.abs(delta) > DRIFT_S) {
      const rate = 1 - Math.max(-0.08, Math.min(0.08, delta * 0.5));
      b.playbackRate = rate;
    } else {
      b.playbackRate = 1;
    }
    if (a.paused && !b.paused) b.pause();
    if (!a.paused && b.paused) {
      const p = b.play?.();
      if (p && typeof p.catch === "function") p.catch(() => {});
    }
  };
  const restart = () => {
    const a = beforeRef.current;
    const b = afterRef.current;
    if (!a) return;
    try {
      a.currentTime = 0;
    } catch (e) {}
    if (b) {
      try {
        b.currentTime = 0;
      } catch (e) {}
    }
    const p = a.play?.();
    if (p && typeof p.catch === "function") p.catch(() => {});
    if (b) {
      const q = b.play?.();
      if (q && typeof q.catch === "function") q.catch(() => {});
    }
  };
  const setBeforeRef = el => {
    beforeRef.current = el;
    if (!el || el.dataset.vcsWired === "1") return;
    el.dataset.vcsWired = "1";
    el.addEventListener("timeupdate", () => {
      const end = loopPoint();
      if (end && el.currentTime >= end - 0.12) {
        restart();
        return;
      }
      resync(false);
    });
    el.addEventListener("ended", restart);
    el.addEventListener("seeked", () => resync(true));
    el.addEventListener("play", () => {
      setPlaying(true);
      resync(true);
    });
    el.addEventListener("pause", () => {
      setPlaying(false);
      resync(true);
    });
    el.addEventListener("loadeddata", () => resync(true));
  };
  const setAfterRef = el => {
    afterRef.current = el;
    if (!el || el.dataset.vcsWired === "1") return;
    el.dataset.vcsWired = "1";
    el.addEventListener("loadeddata", () => resync(true));
  };
  const togglePlay = e => {
    e.preventDefault();
    e.stopPropagation();
    const a = beforeRef.current;
    if (!a) return;
    if (a.paused) {
      const p = a.play?.();
      if (p && typeof p.catch === "function") p.catch(() => {});
    } else {
      a.pause();
    }
    resync(true);
  };
  const videoStyle = {
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    objectFit,
    pointerEvents: "none"
  };
  const labelStyle = {
    position: "absolute",
    top: "12px",
    padding: "4px 10px",
    borderRadius: "6px",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    fontSize: "0.75rem",
    fontWeight: 600,
    letterSpacing: "0.02em",
    pointerEvents: "none",
    zIndex: 4
  };
  return <div className="not-prose" style={{
    borderRadius: "1rem",
    overflow: "hidden",
    height,
    width: "100%"
  }}>
      <div onMouseDown={onPointerDown} onTouchStart={onPointerDown} onClick={e => {
    e.preventDefault();
    e.stopPropagation();
  }} style={{
    position: "relative",
    width: "100%",
    height,
    overflow: "hidden",
    cursor: "ew-resize",
    userSelect: "none",
    WebkitUserSelect: "none",
    background: "#000"
  }}>
        {}
        <video ref={setAfterRef} src={afterVideo} poster={poster} autoPlay muted playsInline preload="auto" aria-label={afterLabel} style={videoStyle} />

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    clipPath: `inset(0 ${100 - position}% 0 0)`
  }}>
          <video ref={setBeforeRef} src={beforeVideo} poster={poster} autoPlay muted playsInline preload="auto" aria-label={beforeLabel} style={videoStyle} />
        </div>

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: `${position}%`,
    transform: "translateX(-50%)",
    width: "3px",
    height: "100%",
    background: "rgba(255,255,255,0.85)",
    pointerEvents: "none",
    zIndex: 2
  }} />

        {}
        <div style={{
    position: "absolute",
    top: "50%",
    left: `${position}%`,
    transform: "translate(-50%, -50%)",
    width: "44px",
    height: "44px",
    borderRadius: "50%",
    background: "rgba(255,255,255,0.95)",
    border: "2px solid rgba(0,0,0,0.15)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: "6px",
    zIndex: 3,
    pointerEvents: "none",
    boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
  }}>
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginRight: "-2px"
  }}>
            <path d="M8 1L2 7L8 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginLeft: "-2px"
  }}>
            <path d="M2 1L8 7L2 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>

        {}
        <div style={{
    ...labelStyle,
    left: "12px"
  }}>{beforeLabel}</div>
        <div style={{
    ...labelStyle,
    right: "12px"
  }}>{afterLabel}</div>

        {}
        <div onMouseDown={e => e.stopPropagation()} onTouchStart={e => e.stopPropagation()} onClick={togglePlay} title={playing ? "Pause" : "Play"} style={{
    position: "absolute",
    bottom: "12px",
    left: "12px",
    width: "36px",
    height: "36px",
    borderRadius: "50%",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "pointer",
    zIndex: 5
  }}>
          {playing ? <svg width="12" height="14" viewBox="0 0 12 14" fill="none">
              <rect x="1" y="1" width="3.5" height="12" rx="1" fill="#fff" />
              <rect x="7.5" y="1" width="3.5" height="12" rx="1" fill="#fff" />
            </svg> : <svg width="12" height="14" viewBox="0 0 12 14" fill="none">
              <path d="M2 1.5L11 7L2 12.5V1.5Z" fill="#fff" />
            </svg>}
        </div>
      </div>
    </div>;
};

export const ImageComparisonSlider = ({beforeImage, afterImage, beforeLabel = "Before", afterLabel = "After", height = "500px", objectFit = "cover", garmentImage, garmentLabel = "Garment reference"}) => {
  const [position, setPosition] = useState(50);
  const dialogRef = useRef(null);
  const openLightbox = () => {
    if (dialogRef.current && typeof dialogRef.current.showModal === "function") {
      dialogRef.current.showModal();
    }
  };
  const closeLightbox = () => {
    if (dialogRef.current && typeof dialogRef.current.close === "function") {
      dialogRef.current.close();
    }
  };
  const getPosition = (e, container) => {
    const rect = container.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const x = clientX - rect.left;
    return Math.max(0, Math.min(100, x / rect.width * 100));
  };
  const onPointerDown = e => {
    e.preventDefault();
    e.stopPropagation();
    const container = e.currentTarget;
    setPosition(getPosition(e, container));
    const onMove = ev => {
      ev.preventDefault();
      setPosition(getPosition(ev, container));
    };
    const onUp = () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      window.removeEventListener("touchmove", onMove);
      window.removeEventListener("touchend", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    window.addEventListener("touchmove", onMove, {
      passive: false
    });
    window.addEventListener("touchend", onUp);
  };
  return <div className="not-prose" style={{
    borderRadius: "1rem",
    overflow: "hidden",
    height,
    width: "100%"
  }}>
      <div onMouseDown={onPointerDown} onTouchStart={onPointerDown} onClick={e => {
    e.preventDefault();
    e.stopPropagation();
  }} style={{
    position: "relative",
    width: "100%",
    height,
    overflow: "hidden",
    cursor: "ew-resize",
    userSelect: "none",
    WebkitUserSelect: "none"
  }}>
        {}
        <img src={afterImage} alt={afterLabel} draggable={false} style={{
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    objectFit,
    pointerEvents: "none"
  }} />

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    clipPath: `inset(0 ${100 - position}% 0 0)`
  }}>
          <img src={beforeImage} alt={beforeLabel} draggable={false} style={{
    display: "block",
    width: "100%",
    height: "100%",
    objectFit,
    pointerEvents: "none"
  }} />
        </div>

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: `${position}%`,
    transform: "translateX(-50%)",
    width: "3px",
    height: "100%",
    background: "rgba(255,255,255,0.85)",
    pointerEvents: "none",
    zIndex: 2
  }} />

        {}
        <div style={{
    position: "absolute",
    top: "50%",
    left: `${position}%`,
    transform: "translate(-50%, -50%)",
    width: "44px",
    height: "44px",
    borderRadius: "50%",
    background: "rgba(255,255,255,0.95)",
    border: "2px solid rgba(0,0,0,0.15)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: "6px",
    zIndex: 3,
    pointerEvents: "none",
    boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
  }}>
          {}
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginRight: "-2px"
  }}>
            <path d="M8 1L2 7L8 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          {}
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginLeft: "-2px"
  }}>
            <path d="M2 1L8 7L2 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>

        {}
        <div style={{
    position: "absolute",
    top: "12px",
    left: "12px",
    padding: "4px 10px",
    borderRadius: "6px",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    fontSize: "0.75rem",
    fontWeight: 600,
    letterSpacing: "0.02em",
    pointerEvents: "none",
    zIndex: 4
  }}>
          {beforeLabel}
        </div>
        <div style={{
    position: "absolute",
    top: "12px",
    right: "12px",
    padding: "4px 10px",
    borderRadius: "6px",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    fontSize: "0.75rem",
    fontWeight: 600,
    letterSpacing: "0.02em",
    pointerEvents: "none",
    zIndex: 4
  }}>
          {afterLabel}
        </div>

        {}
        {garmentImage && <div onMouseDown={e => e.stopPropagation()} onTouchStart={e => e.stopPropagation()} onClick={e => {
    e.preventDefault();
    e.stopPropagation();
    openLightbox();
  }} title={`${garmentLabel} — click to enlarge`} style={{
    position: "absolute",
    bottom: "12px",
    right: "12px",
    height: "140px",
    maxWidth: "40%",
    borderRadius: "8px",
    overflow: "hidden",
    cursor: "zoom-in",
    background: "rgba(255,255,255,0.95)",
    border: "2px solid rgba(255,255,255,0.9)",
    boxShadow: "0 2px 12px rgba(0,0,0,0.45)",
    zIndex: 5,
    display: "flex",
    alignItems: "center",
    justifyContent: "center"
  }}>
            <img src={garmentImage} alt={garmentLabel} draggable={false} style={{
    height: "100%",
    width: "auto",
    objectFit: "contain",
    pointerEvents: "none"
  }} />
          </div>}
      </div>

      {}
      {garmentImage && <dialog ref={dialogRef} onClick={closeLightbox} onClose={closeLightbox} style={{
    padding: 0,
    border: "none",
    background: "transparent",
    maxWidth: "100vw",
    maxHeight: "100vh",
    width: "100vw",
    height: "100vh",
    overflow: "hidden"
  }}>
          <div onClick={closeLightbox} style={{
    width: "100vw",
    height: "100vh",
    background: "rgba(0,0,0,0.88)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "zoom-out",
    padding: "40px",
    boxSizing: "border-box"
  }}>
            <img src={garmentImage} alt={garmentLabel} onClick={e => e.stopPropagation()} style={{
    maxWidth: "90vw",
    maxHeight: "90vh",
    objectFit: "contain",
    borderRadius: "8px",
    boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
    cursor: "default"
  }} />
          </div>
        </dialog>}
    </div>;
};

## Example output

Drag the slider to compare the source clip (left) against the same clip after upscaling (right). Pause to study a single frame.

<VideoComparisonSlider beforeVideo="https://cdn.sanity.io/files/2gpum2i6/production/e04e86d4e261330229d0c4157e3ddbe557813b6f.mp4" afterVideo="https://cdn.sanity.io/files/2gpum2i6/production/43e5e5288436ab545ea0a07d429f74759703f8f0.mp4" beforeLabel="Source 1280×704" afterLabel="Upscaled 3×" height="560px" />

<ImageComparisonSlider beforeImage="https://cdn.sanity.io/images/2gpum2i6/production/98a6df74058736627d69a0d7c20eaa9b47b93ca4-1280x704.jpg" afterImage="https://cdn.sanity.io/images/2gpum2i6/production/1e1b7b713a475ae74e437073bf3a63e4fc2959cb-1920x1056.jpg" beforeLabel="Source frame" afterLabel="Upscaled frame" height="560px" objectFit="contain" />

## Quick start

Upscaling a clip is one POST and a poll: send up to 20 seconds of video as an HTTP(S) URL or base64, then fetch the result from the `polling_url` you get back.

<CodeGroup>
  ```bash cURL theme={null}
  # Submit a clip by URL (or send a base64-encoded mp4 in input_video instead)
  curl -sS -X POST "https://api.bfl.ai/v1/flux-tools/video-upscale-v1" \
    -H "Content-Type: application/json" \
    -H "x-key: $BFL_API_KEY" \
    -d '{
      "input_video": "https://your-storage.example.com/source-clip.mp4",
      "upscale_factor": 2.0,
      "creativity": 1
    }'

  # Poll the polling_url from the response until status is "Ready"
  curl -sS "https://api.bfl.ai/v1/get_result?id=YOUR_TASK_ID" \
    -H "x-key: $BFL_API_KEY"
  ```

  ```python Python theme={null}
  import os, time, requests

  BFL_API_KEY = os.environ["BFL_API_KEY"]

  # 1. Submit — returns an id and a polling_url
  submit = requests.post(
      "https://api.bfl.ai/v1/flux-tools/video-upscale-v1",
      headers={"x-key": BFL_API_KEY, "Content-Type": "application/json"},
      json={
          "input_video": "https://your-storage.example.com/source-clip.mp4",
          "upscale_factor": 2.0,
          "creativity": 1,
      },
  ).json()

  # 2. Poll the returned URL until the job is Ready
  while True:
      time.sleep(5)
      result = requests.get(submit["polling_url"], headers={"x-key": BFL_API_KEY}).json()
      if result["status"] == "Ready":
          print(result["result"]["sample"])   # signed .mp4 URL
          break
      if result["status"] in ("Error", "Request Moderated", "Content Moderated"):
          raise RuntimeError(result["status"])
  ```
</CodeGroup>

The submit call returns the task id and the URL to poll:

```json theme={null}
{
  "id": "8b6a4d16-2b52-4a5e-9f1c-3e7d0a92c48b",
  "polling_url": "https://api.bfl.ai/v1/get_result?id=8b6a4d16-2b52-4a5e-9f1c-3e7d0a92c48b"
}
```

While the clip renders, polls come back with `"status": "Pending"`. When it flips to `Ready`, `result.sample` is a signed URL to the upscaled mp4:

```json theme={null}
{
  "id": "8b6a4d16-2b52-4a5e-9f1c-3e7d0a92c48b",
  "status": "Ready",
  "result": {
    "sample": "https://delivery.bfl.ai/results/8b6a4d16-2b52-4a5e-9f1c-3e7d0a92c48b/sample.mp4?se=..."
  }
}
```

<Warning>
  Signed delivery URLs expire about **1 hour** after the result is ready. Download your video within this timeframe.
</Warning>

`Error`, `Request Moderated`, and `Content Moderated` are terminal: stop polling and check the payload. The [Errors reference](/api_integration/errors) lists every status.

## Choosing a mode

The `creativity` parameter selects how the upscaler treats your footage:

* **`creativity: 0` (precise)** preserves the source exactly and sharpens it. Use it when identity matters: faces, products, brand assets, footage of real people.
* **`creativity: 1` (creative)** restores and invents fine detail more aggressively. Use it on generated footage, textures, crowds, and scenery. It does not preserve identity as strictly as precise mode, so faces and products can drift.

## Request parameters

Use `input_video` as the minimum payload.

| Parameter          | Type    | Required | Description                                                                                                                            |
| ------------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `input_video`      | string  | Yes      | The clip to upscale: base64-encoded mp4 (max 50MB) or an HTTP(S) URL. At most 20 seconds of source footage                             |
| `upscale_factor`   | number  | No       | Output scaling relative to the source resolution, between `1.5` and `3`. Defaults to `2`. The output preserves the source aspect ratio |
| `creativity`       | integer | No       | `0` preserves the source precisely; `1` (default) allows creative detail enhancement                                                   |
| `prompt`           | string  | No       | Optional description of the clip's content, steering the enhanced detail. Leave empty for a neutral upscale                            |
| `safety_tolerance` | integer | No       | `0-4`, defaults to `2`. Moderation strictness for the prompt and the delivered frames                                                  |
| `webhook_url`      | URL     | No       | Async callback                                                                                                                         |
| `webhook_secret`   | string  | No       | Signature secret                                                                                                                       |

<Info>
  Output frames are capped at about 14.4 megapixels (4K and beyond): very large sources are upscaled by less than the requested factor.

  Sources longer than 20 seconds are rejected before processing; they are not truncated, and no charge applies.
</Info>

The output keeps the source clip's audio track.

## Pricing

Upscaling is priced per megapixel-second of delivered output: the megapixels per output frame multiplied by the output duration in seconds. You are charged for delivered output only.

| Variant                    | Price                        |
| -------------------------- | ---------------------------- |
| Precise (`creativity: 0`)  | \$0.075 per megapixel-second |
| Creative (`creativity: 1`) | \$0.105 per megapixel-second |

One megapixel is 1,048,576 pixels (1024 x 1024), the same definition as FLUX image pricing. Per second of output, that works out to approximately:

| Output resolution | Precise  | Creative |
| ----------------- | -------- | -------- |
| 1080p             | \$0.15/s | \$0.21/s |
| 2K                | \$0.26/s | \$0.37/s |
| 4K                | \$0.59/s | \$0.83/s |

Example: upscaling a 10-second clip to 1080p (1920 x 1080 = 1.98 megapixels) is 19.8 megapixel-seconds, so \$1.48 in precise mode or \$2.08 in creative mode. You are charged for the delivered output only. See the [pricing page](/quick_start/pricing#flux-tools-video) for the full rate card.

## Tips for best results

* Start from the least compressed source you have. Compression artifacts limit how much real detail the upscaler can recover.
* Use creative mode for generated footage, landscapes, textures, and crowds; use precise mode when faces, products, or brand assets must stay exactly as they are.
* A short `prompt` describing the clip's content can steer creative mode toward the right kind of detail.
* Run upscaling as your final step, after editing and trimming, so you only pay for footage you keep.

## Limitations

* Source clips are limited to 20 seconds and 50MB.
* Output frames are capped at about 14.4 megapixels: requesting 3x on a source that is already high resolution lands below the requested factor.
* Creative mode does not strictly preserve identity for faces and products. Use precise mode when that matters.

## Troubleshooting

* **`403 Forbidden`** - your API key is missing or your project does not have access to this endpoint.
* **`402 Payment Required`** - your credit balance cannot cover the request. Top up and retry.
* **`422` / validation errors** - check base64 encoding, video URL accessibility, the 50MB payload limit, and that `upscale_factor` is between 1.5 and 3.
* **Status `Error` mentioning input length** - the source clip is longer than 20 seconds. Trim it and resubmit; rejected clips are not charged.
* **Status `Error` mentioning output size** - the requested output exceeds what the endpoint can serve. Reduce `upscale_factor` or the source resolution.
* **Faces or products look different** - switch to `creativity: 0` for a source-faithful upscale.

For the full list of HTTP status codes and polling response types returned by the API, see the [Errors reference](/api_integration/errors).
