These examples use
?wait=N, which holds the request open until the job finishes — one or two requests per job instead of one every few seconds. If your service can receive HTTP callbacks, prefer webhooks and skip waiting altogether. See Getting your results.Check whether footage is usable
Answers in the same request. Call it before anything expensive — areject here
saves the whole run.
import httpx
API_KEY = "fd_live_your_key_here"
BASE_URL = "https://api.fastdrop.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}
response = httpx.post(
f"{BASE_URL}/readiness",
headers=HEADERS,
json={
"video_url": "https://example.com/take_04.mp4",
"context": "motion_analysis",
},
timeout=30,
)
response.raise_for_status()
result = response.json()
verdict = result["verdict"]
print(verdict["action"]) # proceed | fix_first | reject
if verdict["action"] == "reject":
# No transformation helps. Do not spend anything else on this clip.
for reason in verdict["reasons"]:
print(f" {reason['check']}: {reason['detail']}")
elif verdict["action"] == "fix_first":
# Something is wrong and can be fixed. The price is already here.
print(f" fixable for {result['fix_quote']['credits']} credits")
const API_KEY = "fd_live_your_key_here";
const BASE_URL = "https://api.fastdrop.io/api/v1";
const response = await fetch(`${BASE_URL}/readiness`, {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
video_url: "https://example.com/take_04.mp4",
context: "motion_analysis",
}),
});
const { verdict, fix_quote } = await response.json();
if (verdict.action === "reject") {
// Nothing helps. Stop here rather than paying to find out again.
verdict.reasons.forEach((r) => console.log(` ${r.check}: ${r.detail}`));
} else if (verdict.action === "fix_first") {
console.log(` fixable for ${fix_quote.credits} credits`);
}
GET /api/v1/samples lists two videos that are broken on purpose, and checking
or fixing those costs no credits. Point the examples on this page at one of them
to watch the whole flow work before spending anything.Fix it, and check the fix took
Returns a job, because a transcode takes tens of seconds and an unverified answer is not worth having early.import time
import httpx
API_KEY = "fd_live_your_key_here"
BASE_URL = "https://api.fastdrop.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}
submit = httpx.post(
f"{BASE_URL}/fix",
headers={**HEADERS, "Idempotency-Key": "take-04-fix-1"},
json={
"video_url": "https://example.com/take_04.mp4",
"context": "motion_analysis",
},
timeout=30,
)
submit.raise_for_status()
job = submit.json()
print(f"quoted {job['quote']['credits']} credits")
# Poll until terminal. Every failure state has charged nothing.
while True:
time.sleep(10)
status = httpx.get(
f"{BASE_URL}/fix/{job['fix_job_id']}", headers=HEADERS, timeout=30
).json()
if status["terminal"]:
break
if status["status"] == "succeeded":
print(status["result_url"])
else:
# verification_failed means a file came back and we would not stand behind
# it. The report says which check failed and what was measured.
print(status["status"], status["error"])
for check in status["verification"]["checks"]:
if not check["passed"]:
print(f" {check['name']}: {check['detail']}")
const API_KEY = "fd_live_your_key_here";
const BASE_URL = "https://api.fastdrop.io/api/v1";
const headers = { "X-API-Key": API_KEY, "Content-Type": "application/json" };
const submit = await fetch(`${BASE_URL}/fix`, {
method: "POST",
// Makes a retry safe: the same key returns the original job rather than
// charging and transcoding twice.
headers: { ...headers, "Idempotency-Key": "take-04-fix-1" },
body: JSON.stringify({
video_url: "https://example.com/take_04.mp4",
context: "motion_analysis",
}),
});
const job = await submit.json();
let status;
do {
await new Promise((r) => setTimeout(r, 10000));
const poll = await fetch(`${BASE_URL}/fix/${job.fix_job_id}`, {
headers: { "X-API-Key": API_KEY },
});
status = await poll.json();
} while (!status.terminal);
if (status.status === "succeeded") {
console.log(status.result_url);
} else {
status.verification?.checks
.filter((c) => !c.passed)
.forEach((c) => console.log(` ${c.name}: ${c.detail}`));
}
Send an
Idempotency-Key. Retries are the normal case on a network call that
takes a minute, and without one a retry charges and transcodes a second time.
With one, it returns the original job and does not even re-read the video.Classify a single video
import httpx
API_KEY = "fd_live_your_key_here"
BASE_URL = "https://api.fastdrop.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}
WAIT_SECONDS = 60 # server holds the connection this long (max 90)
CLIENT_TIMEOUT = 75 # must exceed WAIT_SECONDS
MAX_ATTEMPTS = 10 # ~10 minutes at 60s per wait
def classify_video(video_url: str, tier: str = "basic") -> dict:
"""Classify a video and wait for results."""
# Submit job
resp = httpx.post(
f"{BASE_URL}/classify",
headers=HEADERS,
json={
"video_url": video_url,
"capabilities": ["classify", "thumbnails", "diagnostics"],
"processing_tier": tier,
},
)
resp.raise_for_status()
job = resp.json()
job_id = job["job_id"]
print(f"Job submitted: {job_id} ({job['credits_charged']} credits)")
# Wait for results. Each request returns the moment the job finishes,
# so there is no sleep here — the wait is the sleep.
for _ in range(MAX_ATTEMPTS):
resp = httpx.get(
f"{BASE_URL}/classify/{job_id}",
params={"wait": WAIT_SECONDS},
headers=HEADERS,
timeout=CLIENT_TIMEOUT,
)
resp.raise_for_status()
status = resp.json()
if status["status"] == "completed":
return status["results"]
if status["status"] == "failed":
raise Exception(f"Job failed: {status['error']}")
print(f" Status: {status['status']} ({status['progress']}%)")
raise TimeoutError(f"Job {job_id} did not finish in time")
# Usage
results = classify_video("https://example.com/my-video.mp4")
print(f"Role: {results['role']} ({results['role_confidence']:.0%})")
print(f"Filename: {results['suggested_filename']}")
print(f"Folder: {results['suggested_folder']}")
const API_KEY = "fd_live_your_key_here";
const BASE_URL = "https://api.fastdrop.io/api/v1";
const WAIT_SECONDS = 60; // server holds the connection this long (max 90)
const CLIENT_TIMEOUT = 75_000; // must exceed WAIT_SECONDS
const MAX_ATTEMPTS = 10; // ~10 minutes at 60s per wait
async function classifyVideo(videoUrl, tier = "basic") {
// Submit job
const submitResp = await fetch(`${BASE_URL}/classify`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify({
video_url: videoUrl,
capabilities: ["classify", "thumbnails", "diagnostics"],
processing_tier: tier,
}),
});
if (!submitResp.ok) throw new Error(await submitResp.text());
const job = await submitResp.json();
console.log(`Job submitted: ${job.job_id} (${job.credits_charged} credits)`);
// Wait for results. Each request returns the moment the job finishes,
// so there is no sleep here — the wait is the sleep.
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const statusResp = await fetch(
`${BASE_URL}/classify/${job.job_id}?wait=${WAIT_SECONDS}`,
{
headers: { "X-API-Key": API_KEY },
signal: AbortSignal.timeout(CLIENT_TIMEOUT),
}
);
const status = await statusResp.json();
if (status.status === "completed") return status.results;
if (status.status === "failed") throw new Error(status.error.message);
console.log(` Status: ${status.status} (${status.progress}%)`);
}
throw new Error(`Job ${job.job_id} did not finish in time`);
}
// Usage
const results = await classifyVideo("https://example.com/my-video.mp4");
console.log(`Role: ${results.role} (${Math.round(results.role_confidence * 100)}%)`);
console.log(`Filename: ${results.suggested_filename}`);
console.log(`Folder: ${results.suggested_folder}`);
Batch classification
import httpx
API_KEY = "fd_live_your_key_here"
BASE_URL = "https://api.fastdrop.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}
WAIT_SECONDS = 90 # the maximum; batches are the slowest thing here
CLIENT_TIMEOUT = 105
MAX_ATTEMPTS = 20 # ~30 minutes
def classify_batch(video_urls: list[str], tier: str = "basic") -> list[dict]:
"""Submit a batch of videos and wait for all results."""
# Submit batch
videos = [
{"video_url": url, "capabilities": ["classify", "thumbnails", "diagnostics"]}
for url in video_urls
]
resp = httpx.post(
f"{BASE_URL}/batch",
headers=HEADERS,
json={"videos": videos, "processing_tier": tier},
)
resp.raise_for_status()
batch = resp.json()
batch_id = batch["batch_id"]
print(f"Batch submitted: {batch_id} ({batch['total_credits']} credits, {batch['total_videos']} videos)")
# Wait for results. Batch status is recomputed from every job row on each
# request, so this is the endpoint you least want to poll blindly.
for _ in range(MAX_ATTEMPTS):
resp = httpx.get(
f"{BASE_URL}/batch/{batch_id}",
params={"wait": WAIT_SECONDS},
headers=HEADERS,
timeout=CLIENT_TIMEOUT,
)
resp.raise_for_status()
status = resp.json()
done = status["completed_videos"] + status["failed_videos"]
print(f" Progress: {done}/{status['total_videos']} ({status['status']})")
if status["status"] in ("completed", "partial", "failed"):
return status["jobs"]
raise TimeoutError(f"Batch {batch_id} did not finish in time")
# Usage
urls = [
"https://example.com/video1.mp4",
"https://example.com/video2.mp4",
"https://example.com/video3.mp4",
]
jobs = classify_batch(urls)
for job in jobs:
if job["status"] == "completed":
r = job["results"]
print(f"{job['video_url']} -> {r['role']} ({r['role_confidence']:.0%})")
else:
print(f"{job['video_url']} -> FAILED: {job.get('error', 'unknown')}")
const API_KEY = "fd_live_your_key_here";
const BASE_URL = "https://api.fastdrop.io/api/v1";
async function classifyBatch(videoUrls, tier = "basic") {
const videos = videoUrls.map((url) => ({
video_url: url,
capabilities: ["classify", "thumbnails", "diagnostics"],
}));
const submitResp = await fetch(`${BASE_URL}/batch`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify({ videos, processing_tier: tier }),
});
if (!submitResp.ok) throw new Error(await submitResp.text());
const batch = await submitResp.json();
console.log(`Batch: ${batch.batch_id} (${batch.total_credits} credits)`);
const WAIT_SECONDS = 90; // the maximum; batches are the slowest thing here
const CLIENT_TIMEOUT = 105_000;
const MAX_ATTEMPTS = 20; // ~30 minutes
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const resp = await fetch(
`${BASE_URL}/batch/${batch.batch_id}?wait=${WAIT_SECONDS}`,
{
headers: { "X-API-Key": API_KEY },
signal: AbortSignal.timeout(CLIENT_TIMEOUT),
}
);
const status = await resp.json();
const done = status.completed_videos + status.failed_videos;
console.log(` Progress: ${done}/${status.total_videos}`);
if (["completed", "partial", "failed"].includes(status.status)) {
return status.jobs;
}
}
throw new Error(`Batch ${batch.batch_id} did not finish in time`);
}
Webhook listener
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here"
seen_events: set[str] = set() # use Redis or a table in production
@app.route("/webhooks/fastdrop", methods=["POST"])
def handle_webhook():
# Verify signature against the RAW body. Re-serialising the parsed JSON
# will not reproduce our bytes and the signature will not match.
signature = request.headers.get("X-FastDrop-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
request.data,
hashlib.sha256,
).hexdigest()
received = signature.replace("sha256=", "")
if not hmac.compare_digest(expected, received):
return jsonify({"error": "Invalid signature"}), 401
payload = request.json
event = payload["event"]
data = payload["data"]
# Delivery is at-least-once: retries and manual replays reuse the same id.
if payload["id"] in seen_events:
return jsonify({"received": True, "duplicate": True}), 200
seen_events.add(payload["id"])
if event == "job.completed":
print(f"Job {data['job_id']} completed: {data['results']['role']}")
elif event == "job.failed":
print(f"Job {data['job_id']} failed")
elif event == "batch.completed":
print(f"Batch completed with {len(data.get('jobs', []))} videos")
# Answer within 15s. Anything slower counts as a failed delivery and is
# retried — queue slow work instead of doing it here.
return jsonify({"received": True}), 200
const express = require("express");
const crypto = require("crypto");
const app = express();
const WEBHOOK_SECRET = "your_webhook_secret_here";
const seen = new Set(); // use Redis in production
// express.raw so the body stays byte-identical and verifiable.
app.post("/webhooks/fastdrop", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-fastdrop-signature"] || "";
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const received = signature.replace("sha256=", "");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(req.body);
const { id, event, data } = payload;
// Delivery is at-least-once: retries and manual replays reuse the same id.
if (seen.has(id)) return res.json({ received: true, duplicate: true });
seen.add(id);
if (event === "job.completed") {
console.log(`Job ${data.job_id} completed: ${data.results.role}`);
} else if (event === "job.failed") {
console.log(`Job ${data.job_id} failed`);
}
// Answer within 15s. Anything slower counts as a failed delivery and is
// retried — queue slow work instead of doing it here.
res.json({ received: true });
});
app.listen(3000);
Transcribe and translate a video
import httpx
API_KEY = "fd_live_your_key_here"
BASE_URL = "https://api.fastdrop.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}
WAIT_SECONDS = 90 # transcription runs 45s+; use a long wait
CLIENT_TIMEOUT = 105
MAX_ATTEMPTS = 20
def transcribe_video(video_url: str, output_formats: list[str] | None = None) -> dict:
"""Transcribe a video with translation and optional subtitle files."""
body = {"video_url": video_url}
if output_formats:
body["output_formats"] = output_formats
# Submit job
resp = httpx.post(f"{BASE_URL}/transcribe", headers=HEADERS, json=body)
resp.raise_for_status()
job = resp.json()
job_id = job["job_id"]
print(f"Job submitted: {job_id} ({job['credits_charged']} credits)")
# Wait for results
for _ in range(MAX_ATTEMPTS):
resp = httpx.get(
f"{BASE_URL}/classify/{job_id}",
params={"wait": WAIT_SECONDS},
headers=HEADERS,
timeout=CLIENT_TIMEOUT,
)
resp.raise_for_status()
status = resp.json()
if status["status"] == "completed":
return status["results"]["transcription"]
if status["status"] == "failed":
raise Exception(f"Job failed: {status['error']}")
print(f" Status: {status['status']} ({status['progress']}%)")
raise TimeoutError(f"Job {job_id} did not finish in time")
# JSON only
result = transcribe_video("https://example.com/interview-arabic.mp4")
print(f"Language: {result['language_name']}")
print(f"Transcript: {result['text'][:100]}...")
if "translation" in result:
print(f"English: {result['translation']['text'][:100]}...")
# With subtitle files
result = transcribe_video(
"https://example.com/interview-arabic.mp4",
output_formats=["srt", "txt"],
)
if "files" in result:
print(f"Source SRT: {result['files']['source_srt']}")
print(f"English SRT: {result['files']['english_srt']}")
const API_KEY = "fd_live_your_key_here";
const BASE_URL = "https://api.fastdrop.io/api/v1";
async function transcribeVideo(videoUrl, outputFormats = null) {
const body = { video_url: videoUrl };
if (outputFormats) body.output_formats = outputFormats;
// Submit job
const submitResp = await fetch(`${BASE_URL}/transcribe`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify(body),
});
if (!submitResp.ok) throw new Error(await submitResp.text());
const job = await submitResp.json();
console.log(`Job submitted: ${job.job_id} (${job.credits_charged} credits)`);
const WAIT_SECONDS = 90; // transcription runs 45s+; use a long wait
const CLIENT_TIMEOUT = 105_000;
const MAX_ATTEMPTS = 20;
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const statusResp = await fetch(
`${BASE_URL}/classify/${job.job_id}?wait=${WAIT_SECONDS}`,
{
headers: { "X-API-Key": API_KEY },
signal: AbortSignal.timeout(CLIENT_TIMEOUT),
}
);
const status = await statusResp.json();
if (status.status === "completed") return status.results.transcription;
if (status.status === "failed") throw new Error(status.error.message);
console.log(` Status: ${status.status} (${status.progress}%)`);
}
throw new Error(`Job ${job.job_id} did not finish in time`);
}
// JSON only
const result = await transcribeVideo("https://example.com/interview-arabic.mp4");
console.log(`Language: ${result.language_name}`);
console.log(`Transcript: ${result.text.slice(0, 100)}...`);
if (result.translation) {
console.log(`English: ${result.translation.text.slice(0, 100)}...`);
}
// With subtitle files
const withFiles = await transcribeVideo(
"https://example.com/interview-arabic.mp4",
["srt", "txt"]
);
if (withFiles.files) {
console.log(`Source SRT: ${withFiles.files.source_srt}`);
console.log(`English SRT: ${withFiles.files.english_srt}`);
}
Check credit balance
import httpx
resp = httpx.get(
"https://api.fastdrop.io/api/v1/usage",
headers={"X-API-Key": "fd_live_your_key_here"},
)
usage = resp.json()
print(f"Plan: {usage['plan_tier']}")
print(f"Credits: {usage['credits_remaining']}/{usage['credits_monthly']}")
print(f"Used this month: {usage['usage_this_month']['total_credits']}")
const resp = await fetch("https://api.fastdrop.io/api/v1/usage", {
headers: { "X-API-Key": "fd_live_your_key_here" },
});
const usage = await resp.json();
console.log(`Plan: ${usage.plan_tier}`);
console.log(`Credits: ${usage.credits_remaining}/${usage.credits_monthly}`);
Error handling
import httpx
def safe_classify(video_url: str, api_key: str) -> dict | None:
"""Classify with proper error handling."""
resp = httpx.post(
"https://api.fastdrop.io/api/v1/classify",
headers={"X-API-Key": api_key},
json={"video_url": video_url},
)
if resp.status_code == 401:
print("Invalid API key")
return None
elif resp.status_code == 402:
error = resp.json()["error"]
print(f"Insufficient credits: need {error['details']['credits_required']}, "
f"have {error['details']['credits_available']}")
return None
elif resp.status_code == 429:
retry_after = resp.json()["error"]["details"]["retry_after_seconds"]
print(f"Rate limited. Retry after {retry_after}s")
return None
resp.raise_for_status()
return resp.json()
async function safeClassify(videoUrl, apiKey) {
const resp = await fetch("https://api.fastdrop.io/api/v1/classify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
},
body: JSON.stringify({ video_url: videoUrl }),
});
if (resp.status === 401) {
console.error("Invalid API key");
return null;
}
if (resp.status === 402) {
const { error } = await resp.json();
console.error(`Need ${error.details.credits_required} credits, have ${error.details.credits_available}`);
return null;
}
if (resp.status === 429) {
const { error } = await resp.json();
console.error(`Rate limited. Retry after ${error.details.retry_after_seconds}s`);
return null;
}
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
}