Skip to main content

fotovid

PyPI

Typed Python SDK for the Fotovid media API — a serverless ffmpeg API for watermarking video and images, trimming video and audio, extracting audio from video, generating video thumbnails, and probing video metadata. POST a source URL, get back the finished file over one HTTPS call, with zero dependencies (standard library only — no ffmpeg binary, nothing native to compile).

Full docs, guides, and API reference: fotovid.co/docs

Why Fotovid

  • No ffmpeg to install or maintain. No binary in your container/Lambda, no native build step, no version drift across machines — zero runtime dependencies, standard library only.
  • One call, typed end to end. Parameter names match the API 1:1; MediaResult/ProbeResult/Task are dataclasses, nothing to guess.
  • Sync for quick jobs, async for large ones. Small/short media returns in the same call; video over ~720p or 15s goes through the async task API instead of failing outright.
  • Idempotent by default. Every billed call gets a fresh idempotency key automatically — retry safely without a double charge.
  • Hosted output. Every operation returns a URL to the finished file; no storage bucket to provision or clean up yourself.

Install

pip install fotovid

Usage

import os
from fotovid import Fotovid

client = Fotovid(api_key=os.environ["FOTOVID_API_KEY"])

result = client.video.watermark(
    source_url="https://cdn.example.com/clip.mp4",
    watermark_type="image",
    watermark_image_url="https://cdn.example.com/logo.png",
    position="bottom-right",
    opacity=0.8,
)
print(result.url)  # hosted, time-limited URL — see expires_at, store your own copy

Operations

Method Endpoint
client.video.watermark(...) POST /v1/video/watermark
client.image.watermark(...) POST /v1/image/watermark
client.video.trim(...) POST /v1/video/trim
client.video.extract_audio(...) POST /v1/video/extract-audio
client.audio.trim(...) POST /v1/audio/trim
client.video.thumbnail(...) POST /v1/video/extract-cover
client.video.probe(...) POST /v1/video/probe

Parameter names match the API 1:1. Every media operation returns a MediaResult (id, type, url, expires_at, duration); probe returns a ProbeResult with video metadata.

Examples

Video

# Text watermark, bottom-right corner.
client.video.watermark(
    source_url="https://cdn.example.com/clip.mp4",
    watermark_type="text",
    text="© Acme Inc.",
    position="bottom-right",
)

# Cut a 10s clip.
client.video.trim(source_url="https://cdn.example.com/clip.mp4", start=5, end=15)

# Pull out the audio track as an MP3.
client.video.extract_audio(source_url="https://cdn.example.com/clip.mp4")

# Grab a frame at 2.5s as a thumbnail.
client.video.thumbnail(source_url="https://cdn.example.com/clip.mp4", at=2.5)

# Metadata only — no file produced.
meta = client.video.probe(source_url="https://cdn.example.com/clip.mp4")
print(meta.width, meta.height, meta.duration_sec, meta.fps, meta.codec)

Image

# Logo watermark, scaled to 20% of the source width.
client.image.watermark(
    source_url="https://cdn.example.com/photo.jpg",
    watermark_type="image",
    watermark_image_url="https://cdn.example.com/logo.png",
    scale=0.2,
)

Audio

client.audio.trim(source_url="https://cdn.example.com/track.mp3", start=0, end=30)

Async (large or long video)

The sync methods above reject video over ~720p or 15s with a 400 asking you to use the async endpoint (a hard limit — the sync API has a short time budget). For a large video watermark, a long trim, or anything you don't need back in a couple of seconds, submit a task instead and poll for the result:

import time

# A vertical / large video like this is rejected by the sync endpoint.
task = client.tasks.video.watermark(
    source_url="https://cdn.example.com/1080x1920.mp4",
    watermark_type="image",
    watermark_image_url="https://cdn.example.com/logo.png",
)

while not task.is_terminal:
    time.sleep(2)
    task = client.tasks.get(task.id)

if task.status == "succeeded":
    # [TaskOutput(kind="video", url="...")] — read by kind, order not guaranteed
    print(task.outputs)
else:
    # TaskError(code, message, request_id, detail) — not an exception, just data
    print(task.error)

client.tasks.* mirrors the sync methods 1:1 (tasks.video.watermark, tasks.video.trim, tasks.video.extract_audio, tasks.video.thumbnail, tasks.image.watermark, tasks.audio.trim) plus the low-level tasks.create/tasks.get. There's no built-in polling helper — the loop above is the whole pattern. probe has no async form; it's sync-only.

Your own metadata

Pass metadata to tag a task with your own labels. They come back on the Task and in the webhook payload, so a callback can be matched against your records without a second lookup:

task = client.tasks.video.trim(
    source_url="https://cdn.example.com/clip.mp4",
    start=0,
    end=30,
    metadata={"order_id": "A-1001", "tenant": "acme"},
    webhook="https://example.com/hooks/fotovid",
)

task.metadata  # {"order_id": "A-1001", "tenant": "acme"}

The platform never interprets metadata — it takes no part in routing, auth, billing or idempotency. At most 50 keys, keys ≤40 chars (no square brackets), string values ≤500 chars. Don't put secrets in it: it is returned to anyone who can read the task and delivered to your webhook endpoint. On an idempotent replay the stored task's metadata comes back and the one you sent is ignored.

Task also echoes input back exactly as submitted — the source_url plus that task type's params, flattened into one object.

Breaking in 1.0.0: the input envelope

The async wire moved to an input envelope. The typed helpers above are unchanged — tasks.video.watermark(source_url=..., **params) still takes flat keyword arguments. Only two things moved:

p = {"start": 0, "end": 30}

# tasks.create — the low-level escape hatch
client.tasks.create(type="video.trim", source_url=url, params=p)  # ❌ 0.x
client.tasks.create(type="video.trim", input={"source_url": url, **p})  # ✅ 1.0

# Task — the two echo fields collapsed into one
task.source_url, task.params  # ❌ 0.x
task.input  # ✅ 1.0

The server rejects the old request shape with a 400 whose errors[].field is body.input.source_url. The response change is silent — task.source_url and task.params simply become None — so grep for them when you upgrade. Sync methods (client.video.*, client.image.*, client.audio.*) are untouched.

Sync vs async

Sync (client.video.*, …) Async (client.tasks.*)
Returns Finished result, same call A Task — poll tasks.get until terminal
Limits ~720p / 15s 4K / 600s
Use for Small/short media, need the result now Large video, long clips, batch/background jobs

Idempotency

The billed endpoints require an Idempotency-Key header — the client sends a fresh key per call automatically. To safely retry without being charged twice, pass the same key both times:

key = "my-unique-key"
client.video.watermark(source_url=url, idempotency_key=key)
# a retry with the same key replays the original result instead of re-charging
client.video.watermark(source_url=url, idempotency_key=key)

The same applies to client.tasks.* — pass idempotency_key there too (it travels in the request body, not a header, but the client handles that difference for you).

Errors

A non-2xx response raises FotovidError (status, detail, retry_after). For client.tasks.*, that's the only thing that raises — a task that finishes as "failed" is a normal return value, not an exception; check task.error.

Config

Fotovid(
    api_key="p6_<key_id>:<secret>",  # or set FOTOVID_API_KEY
    base_url="https://api.fotovid.co",  # optional
    timeout=60.0,  # optional, seconds
)

Documentation

License

MIT

Metadata

Release files for fotovid 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fotovid 1.0.0
File Size Uploaded
fotovid-1.0.0.tar.gz 11.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fotovid 1.0.0
File Interpreter ABI Platform
fotovid-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 24.9 kB

Release files / fotovid-1.0.0.tar.gz

Download URL fotovid-1.0.0.tar.gz
Size 11.7 kB
Tags Source
SHA-256 checksum
How to use checksums
2210ee8bee66811de3e67450cec7f6e13f42408b7de4a2c00077cac4e0c0edac
BLAKE2b-256 checksum
How to use checksums
d477de38c3030a3e37d198c4fc32ae1ccb91b09bd43ed260938c3650c3ff4218
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

Release files / fotovid-1.0.0-py3-none-any.whl

Download URL fotovid-1.0.0-py3-none-any.whl
Size 13.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f9605d15464f7608ec17e85a9d3cca865fe93c649e26f9f766f090ea33547d13
BLAKE2b-256 checksum
How to use checksums
5ecc639619993674bc6a37b53e26a6c0f1dc6de9eb82c4c043833bf409d4919a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page