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
- Sign up at /api-keys.
- Create a token. Copy the plaintext — you can't view it again.
- POST a file or URL to
https://dfd.ironcatlabs.com/api/v1/detect/{modality}with headerAuthorization: Bearer dfd_…. - 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
| status | meaning |
|---|---|
| 400 | missing file/url, mismatched modality, or fetch failure |
| 401 | missing or invalid Bearer token |
| 402 | token quota exhausted — create a new one or get the existing one's quota raised |
| 500 | internal 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.