Skip to content

Official SDKs ​

We publish official clients so you don't have to write the HTTP, multipart, and polling yourself. Both wrap the same submit → poll → download loop and handle a single photo, a video, or a whole carousel. From 2.1.0 they also wrap the inspect routes.

LanguageInstallPackage
TypeScript / JavaScriptnpm install fluidghostnpmjs.com/package/fluidghost
Pythonpip install fluidghostpypi.org/project/fluidghost

Generate an API key in the dashboard and keep it server-side.

The inspect methods need version 2.1.0 or newer (npm install fluidghost@^2.1.0, pip install "fluidghost>=2.1.0").

Uniquify a photo or video ​

run() submits a job and waits for it to finish. Photos and videos both go through the same call — the type is detected from the file. Results are one-time links: downloadVariant streams the bytes and the object is deleted on a successful download.

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

const fg = new FluidGhost({ apiKey: process.env.FLUIDGHOST_KEY! });

const job = await fg.run({
  image: { path: "photo.jpg" },     // or a video: { path: "clip.mp4" }
  recipeId: "full-refresh",
  copies: 3,
});

for (const v of job.variants) {
  if (v.status !== "completed") continue;
  const bytes = await fg.downloadVariant(job.jobId, v.index);
  await writeFile(v.fileName ?? `variant-${v.index}.jpg`, bytes);
}
python
import os
from fluidghost import FluidGhost

fg = FluidGhost(api_key=os.environ["FLUIDGHOST_KEY"])

job = fg.run(image="photo.jpg", recipe_id="full-refresh", copies=3)  # or "clip.mp4"

for v in job["variants"]:
    if v["status"] != "completed":
        continue
    data = fg.download_variant(job["jobId"], v["index"])
    with open(v.get("fileName") or f"variant-{v['index']}.jpg", "wb") as f:
        f.write(data)

A carousel is a set of photos posted together. spoofCarousel submits one linked job per file — sharing a session and a running filename sequence so the outputs read as one cohesive post — and waits for all of them.

ts
const jobs = await fg.spoofCarousel(
  [{ path: "slide1.jpg" }, { path: "slide2.jpg" }, { path: "slide3.jpg" }],
  { recipeId: "full-refresh" },
);
python
jobs = fg.spoof_carousel(
    ["slide1.jpg", "slide2.jpg", "slide3.jpg"],
    recipe_id="full-refresh",
)

For a fully cohesive set (the same device across every slide), pass a recipeConfig or presetId that pins a device — otherwise each slide picks its own device family.

Inspect metadata ​

The inspect methods read a file's metadata without running a job: nothing is stored and no balance is spent. They need a key with the inspect:read scope (secret keys have it by default) and share one per-owner rate limit of 60 requests a minute. The file argument is the same one run() accepts. See Read image metadata and the Inspect reference for the response shapes.

RouteTypeScriptPython
POST /inspectinspect(file)inspect(image)
POST /inspect/forensicsinspectForensics(file)inspect_forensics(image)
POST /inspect/compareinspectCompare(original, variant)inspect_compare(original, variant)
GET /inspect/result/{jobId}/{index}inspectResult(jobId, index)inspect_result(job_id, index)
ts
const meta = await fg.inspect({ path: "photo.jpg" });
console.log(meta.format, meta.tagCount, Object.keys(meta.groups));

const report = await fg.inspectForensics({ path: "photo.jpg" });
if (report.status === "ok") console.log(report.verdict, report.findings.length);

// Perceptual distance between a source and its variant
const diff = await fg.inspectCompare({ path: "photo.jpg" }, { path: "variant.jpg" });
console.log(diff.detectionRisk, diff.band);

// A finished variant of one of your jobs: inspect BEFORE the one-time download
const variantMeta = await fg.inspectResult(job.jobId, 0);
python
meta = fg.inspect("photo.jpg")
print(meta["format"], meta["tagCount"], list(meta["groups"]))

report = fg.inspect_forensics("photo.jpg")
if report["status"] == "ok":
    print(report.get("verdict"), len(report["findings"]))

# Perceptual distance between a source and its variant
diff = fg.inspect_compare("photo.jpg", "variant.jpg")
print(diff["detectionRisk"], diff["band"])

# A finished variant of one of your jobs: inspect BEFORE the one-time download
variant_meta = fg.inspect_result(job["jobId"], 0)

Failures raise FluidGhostError like every other method: 401 for a bad or revoked key, 403 insufficient_scope when the key lacks inspect:read, 404 for an unknown or unfinished variant (a one-time result is deleted by its download, so inspect it first), 413 for an upload over 100 MB and 429 rate_limited.

Errors ​

Every non-2xx response throws / raises a FluidGhostError mirroring the API's { error, message, details } envelope — see Errors. Handle rate_limited (back off for retryAfter) and insufficient_balance (top up in the dashboard).

What a key can do ​

A secret API key can submit jobs, read and download their results, and inspect metadata (management keys only provision). The SDKs wrap both the job surface and the inspect routes (2.1.0 or newer). See Authentication.

FluidGhost API — part of the Fluidvip ecosystem.