Secure Upload

Upload a media file once and reference it on downstream jobs using a short-lived token, instead of hosting the file at a public URL. The Secure Upload API pairs with endpoints like Create Detection and Run Intelligence, which both accept a media_token in place of url.

Use a secure upload when:

  • You do not want to host source media on the public internet.
  • You need to send a local file to the API without spinning up your own storage.
  • You are submitting a large batch and want uploads and detection jobs to run in parallel.

Workflow

  1. Upload the file via POST /secure_uploads and capture the returned token.
  2. Call the downstream endpoint (e.g. POST /detect) with media_token set to that token.
  3. Poll or wait on the returned job as usual. Tokens are single-purpose and tied to the uploaded file; the media stays private to your team.

Tokens expire 1 hour after issuance. If you need to re-run detection after expiry, upload the file again.

Upload Endpoint

POST https://app.resemble.ai/api/v2/secure_uploads

Headers

HeaderValue
AuthorizationBearer YOUR_API_TOKEN
Content-Typemultipart/form-data (set automatically by curl -F / most HTTP clients)

Request Body

FieldTypeRequiredDescription
filefileYesThe media file to upload. Audio, image, and video are supported.

Example Request

curl --request POST 'https://app.resemble.ai/api/v2/secure_uploads' \
-H 'Authorization: Bearer YOUR_API_TOKEN' \
-F 'file=@/path/to/media.mp4'

Response

{
"success": true,
"token": "eyJhbGciOiJIUzI1NiJ9..."
}

The returned token is a JWT that securely references the uploaded file. Pass it as media_token on any endpoint that accepts it.

Using the Token with Detect

For a video such as the .mp4 uploaded above, pass face_only: true to focus visual detection on faces:

curl --request POST 'https://app.resemble.ai/api/v2/detect' \
-H 'Authorization: Bearer YOUR_API_TOKEN' \
-H 'Content-Type: application/json' \
--data '{
"media_token": "eyJhbGciOiJIUzI1NiJ9...",
"face_only": true,
"intelligence": true,
"detect_watermark": true
}'

All other single-file /detect parameters (callback_url, visualize, frame_length, face_only, detect_watermark, zero_retention_mode, etc.) behave exactly as they do with a url input. Face-only mode is effective only for video inputs that include visual analysis; incompatible inputs return an effective value of false.

When detect_watermark=true, the secure-upload token does not bypass Watermark decoder limits: the referenced source must be no larger than 25 MB for audio/image or 100 MB for video. The returned Detect includes a conditional watermark object, and callbacks or Prefer: wait wait until that analysis is completed or failed. Watermark analysis is not supported by POST /detect/batch or zip uploads.

Python Example — Batch Detect with Secure Upload

The following script walks a folder of local media files, uploads each via the Secure Upload API, submits a detect job against the returned token, and polls until every job reaches a terminal state. Uploads and polling run in a small thread pool so a batch of dozens of files completes well within the 1-hour token window.

"""
Resemble AI -- Batch Deepfake Detection with Secure Upload
Walk a folder of local media files, upload each via the Secure Upload API,
run deepfake detection against the returned media token, and poll until each
job reaches a terminal state. Uploads and polling run concurrently so batches
of dozens/hundreds of files finish in practical wall-clock time (and well
within the 1-hour secure-upload token expiration).
Non-media files and subdirectories are skipped. Per-file failures are
collected and reported in the final summary; the script never aborts early.
Prerequisites:
pip install requests
Usage:
export RESEMBLE_API_KEY="your_api_key"
python detect_with_secure_uploads.py <absolute-folder-path> [output-json-path]
If `output-json-path` is omitted, results are written to `<folder>/results.json`.
"""
import json
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
API_KEY = os.environ.get("RESEMBLE_API_KEY", "")
BASE_URL = "https://app.resemble.ai/api/v2"
MAX_WORKERS = 4 # tune to your rate limit / upload bandwidth
# Extend with any audio/video/image extension you care about.
MEDIA_EXTS = {".wav", ".mp3", ".mp4", ".mov", ".png", ".jpg", ".jpeg"}
def json_headers() -> dict:
return {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def auth_only_headers() -> dict:
# For multipart/form-data, requests sets Content-Type (with boundary) itself.
return {"Authorization": f"Bearer {API_KEY}"}
def secure_upload(path: str) -> str:
"""Upload a local file to the Secure Upload API and return its media_token."""
with open(path, "rb") as f:
resp = requests.post(
f"{BASE_URL}/secure_uploads",
headers=auth_only_headers(),
files={"file": (os.path.basename(path), f)},
)
if not resp.ok:
raise RuntimeError(f"POST /secure_uploads returned {resp.status_code}: {resp.text}")
token = resp.json().get("token")
if not token:
raise RuntimeError(f"no token in secure upload response: {resp.text}")
return token
def submit_detect(media_token: str) -> str:
"""Submit a detect job referencing a secure-upload token and return the uuid."""
payload = {
"media_token": media_token,
# Prefer webhooks over polling for large batches:
# "callback_url": "https://your-server.example.com/resemble-webhook",
}
resp = requests.post(f"{BASE_URL}/detect", headers=json_headers(), json=payload)
if not resp.ok:
raise RuntimeError(f"POST /detect returned {resp.status_code}: {resp.text}")
uuid = resp.json().get("item", {}).get("uuid")
if not uuid:
raise RuntimeError("no uuid in detect response")
return uuid
def poll_for_result(uuid: str, timeout: int = 600, interval: int = 5) -> dict:
"""Poll GET /detect/{uuid} until the job reaches a terminal state."""
url = f"{BASE_URL}/detect/{uuid}"
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(url, headers=json_headers())
if not resp.ok:
raise RuntimeError(f"GET /detect/{uuid} returned {resp.status_code}: {resp.text}")
item = resp.json().get("item", {})
status = item.get("status", "unknown")
if status in ("completed", "failed"):
return item
time.sleep(interval)
raise RuntimeError(f"polling timed out after {timeout}s")
def iter_media_files(folder: str):
for name in sorted(os.listdir(folder)):
full = os.path.join(folder, name)
if not os.path.isfile(full):
continue
if os.path.splitext(name)[1].lower() not in MEDIA_EXTS:
continue
yield full
def process_file(path: str) -> dict:
"""Upload, submit, and poll one file."""
token = secure_upload(path)
uuid = submit_detect(token)
result = poll_for_result(uuid)
status = result.get("status", "unknown")
if status != "completed":
raise RuntimeError(f"detect job {uuid} ended with status={status}")
return {"file": path, "detect": result}
def main():
if not API_KEY:
sys.exit("Error: set RESEMBLE_API_KEY environment variable before running.")
if len(sys.argv) not in (2, 3):
sys.exit(
f"Usage: python {os.path.basename(sys.argv[0])} "
f"<absolute-folder-path> [output-json-path]"
)
folder = sys.argv[1]
output_path = sys.argv[2] if len(sys.argv) == 3 else os.path.join(folder, "results.json")
files = list(iter_media_files(folder))
if not files:
print("No media files to process.")
return
succeeded, failed = [], []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
future_to_path = {pool.submit(process_file, p): p for p in files}
for future in as_completed(future_to_path):
path = future_to_path[future]
name = os.path.basename(path)
try:
record = future.result()
succeeded.append(record)
metrics = record["detect"].get("metrics") or {}
print(
f" [OK] {name} "
f"label={metrics.get('label')} score={metrics.get('aggregated_score')}"
)
except Exception as e:
failed.append({"file": path, "error": str(e)})
print(f" [ERR] {name} {e}")
with open(output_path, "w") as f:
json.dump({"folder": folder, "succeeded": succeeded, "failed": failed}, f, indent=2)
print(f"\nDone. {len(succeeded)} succeeded, {len(failed)} failed (of {len(files)}).")
print(f"Results written to {output_path}")
if __name__ == "__main__":
main()

Security Considerations

  • Tokens are valid for 1 hour after creation; use them promptly or re-upload.
  • Always use HTTPS when uploading so files are encrypted in transit.
  • Keep your API key secret — treat it like a password.
  • Uploaded files are never publicly accessible; only holders of a valid token (and your team) can reference them.

Error Handling

Both the upload and downstream endpoints return standard HTTP status codes with JSON error bodies. Common failures include:

StatusMeaning
400Invalid parameters (e.g. missing file, malformed token).
401Missing or invalid API token.
403API key lacks permission for this endpoint.
404Referenced resource does not exist.
500Unexpected server error.

Always check the response status and body for detailed error information before retrying.