// developer docs

API REFERENCE

Run deepfake detection on video / image / audio files programmatically. Token-gated, per-token quotas, standard REST. Start by creating a token at /api-keys.

Quickstart

  1. Sign up at /api-keys.
  2. Create a token. Copy the plaintext — you can't view it again.
  3. POST a file or URL to https://dfd.ironcatlabs.com/api/v1/detect/{modality} with header Authorization: Bearer dfd_….
  4. Each successful call decrements your quota by 1.

POST /api/v1/detect/{modality}

Modality is one of video, image, or audio. Send either:

  • multipart/form-data with file=<binary> — supported for all modalities
  • multipart/form-data with url=<https-url> — currently video only, fetched via yt-dlp. Image and audio URL ingestion is on the roadmap.

// curl

# Upload a local image
curl -X POST https://dfd.ironcatlabs.com/api/v1/detect/image \
  -H "Authorization: Bearer dfd_YOUR_TOKEN" \
  -F "file=@/path/to/test.jpg" \
  -F "description=press kit photo, sanity check"

# Or pass a URL (video only for now)
curl -X POST https://dfd.ironcatlabs.com/api/v1/detect/video \
  -H "Authorization: Bearer dfd_YOUR_TOKEN" \
  -F "url=https://youtu.be/dQw4w9WgXcQ"

// python (requests)

import requests

TOKEN = "dfd_YOUR_TOKEN"
url = "https://dfd.ironcatlabs.com/api/v1/detect/image"
files = {"file": open("test.jpg", "rb")}
data = {"description": "press kit photo, sanity check"}
r = requests.post(
    url,
    headers={"Authorization": f"Bearer {TOKEN}"},
    files=files,
    data=data,
)
r.raise_for_status()
result = r.json()
print(result["verdict"], result["confidence"])
print("signals:", result["categories"])

// node (undici / fetch)

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

const TOKEN = "dfd_YOUR_TOKEN";
const buf = await readFile("test.jpg");

const form = new FormData();
form.append("file", new Blob([buf]), "test.jpg");
form.append("description", "press kit photo");

const r = await fetch("https://dfd.ironcatlabs.com/api/v1/detect/image", {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}` },
  body: form,
});

if (!r.ok) throw new Error(`HTTP ${r.status}`);
const result = await r.json();
console.log(result.verdict, result.confidence);
console.log("signals:", result.categories);

Response shape

{
  "id": "01HFXXX...",                 // UUID, also addressable at /analyze/{id}
  "ts": "2026-06-30T12:34:56Z",
  "source": "upload",                  // "upload" | "url"
  "source_url": null,
  "filename": "test.jpg",
  "file_hash": "sha256...",
  "file_size_bytes": 124573,
  "duration_seconds": null,
  "modality": "image",
  "metadata": { ... },                 // extracted metadata (EXIF/ffprobe/etc.)
  "extra_info": { ... },               // extractor diagnostics
  "verdict": "FAKE",                   // "FAKE" | "REAL" | "INCONCLUSIVE"
  "confidence": 67.5,                  // 0..100
  "rules_triggered": [],               // always [] on public endpoints (intentional)
  "categories": [                      // public-safe signal categories
    "ai-generator signature",
    "encoder fingerprint"
  ],
  "rules_total": 22,
  "rule_snapshot": {
    "id": "abc12345",                  // truncated; full id is admin-only
    "ts": "2026-06-28T03:00:00Z",
    "triggered_by": "verified_upload"
  }
}

Note: rules_triggered is intentionally always empty on the public API. categories is what we expose — high-level signal labels that don't reveal specific column names, thresholds, or comparison operators. See methodology for the full reasoning around what's shown vs hidden.

Errors

statusmeaning
400missing file/url, mismatched modality, or fetch failure
401missing or invalid Bearer token
402token quota exhausted — create a new one or get the existing one's quota raised
500internal error — does not burn quota

Only successful (2xx/3xx) calls burn quota — auth, validation, and server errors don't count. Same convention as Stripe and OpenAI.

Pricing

Free during the public beta. Default 100 calls per token; create multiple tokens for more. Pricing tiers will land here when the payment provider is wired up. Until then this is a usage-tracking system with no charge attached — the goal is to understand what developers actually want to do with the API.