Upload Files
Turn a local file into a public URL FFHub can read.
FFHub workers run in the cloud and only accept inputs that are public URLs. If your input lives on your laptop, your phone, or a private bucket, upload it to FFHub Storage first and pass the returned URL into your ffmpeg command.
How uploads work
Two steps, and the file never touches our server. Your client gets a one-time signed URL and PUTs directly to R2.
- Sign.
POST /v1/uploads/signreturns a presigned URL valid for 15 minutes. - PUT. Upload the bytes to that URL with
curl,fetch, or any HTTP client.
This means you can upload files up to 5 GB without our servers being involved in the data path.
Step 1 — Get a signed URL
curl -X POST https://api.ffhub.io/v1/uploads/sign \
-H "Authorization: Bearer $FFHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "input.mp4",
"size": 12345678,
"content_type": "video/mp4"
}'
Response:
{
"upload_url": "https://...r2.cloudflarestorage.com/...?X-Amz-Signature=...",
"public_url": "https://storage.ffhub.io/tmp/uploads/<user>/<rand>.mp4",
"key": "tmp/uploads/<user>/<rand>.mp4",
"content_type": "video/mp4",
"expires_at": "2026-05-12T05:30:00Z"
}
Step 2 — Upload the file
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary "@./input.mp4"
The Content-Type header must exactly match the content_type you sent in step 1 — R2 rejects the signature otherwise.
After the PUT returns 200/204, public_url from step 1 is immediately readable.
End-to-end example
FILE=./input.mp4
SIZE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE")
# 1. Sign
SIGNED=$(curl -s -X POST https://api.ffhub.io/v1/uploads/sign \
-H "Authorization: Bearer $FFHUB_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"filename\":\"input.mp4\",\"size\":$SIZE,\"content_type\":\"video/mp4\"}")
UPLOAD_URL=$(echo "$SIGNED" | jq -r '.upload_url')
PUBLIC_URL=$(echo "$SIGNED" | jq -r '.public_url')
# 2. Upload directly to R2
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary "@$FILE"
# 3. Submit a task using public_url as the input
curl -X POST https://api.ffhub.io/v1/tasks \
-H "Authorization: Bearer $FFHUB_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"command\":\"-i $PUBLIC_URL -c:v libx264 -crf 28 output.mp4\"}"
The CLI does this whole dance automatically — see CLI.
Limits
- Max size: 5 GB per upload (R2 single-PUT limit)
- Signed URL lifetime: 15 minutes from
expires_at; sign again if you go past it - File lifetime: 7 days, then deleted from R2
If you need files to live longer than 7 days, copy them to your own bucket as soon as the task finishes.
Already have a public URL?
Skip the whole flow. The task API accepts any public URL as an -i input:
curl -X POST https://api.ffhub.io/v1/tasks \
-H "Authorization: Bearer $FFHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command":"-i https://example.com/clip.mp4 -c:v libx264 output.mp4"}'
Your S3, your CDN, our storage — all equivalent. The task module doesn't care where bytes live.