Build with the VideoForce API
Upload source videos, then generate winning ads and highlight clips from them — programmatically. Generation is asynchronous: you submit a job, poll it, and fetch the result. Every endpoint below is testable right here in your browser.
Overview
The VideoForce API is organized around REST. It has resource-oriented URLs, accepts and returns JSON, and uses standard HTTP verbs and status codes. Errors are returned as RFC 9457 application/problem+json.
/public/v1 — e.g. https://api.videoforce.ai/public/v1/projects
AuthAuthorization: Bearer <api-key> on every request
AsyncAd & highlight generation return a job you poll, then fetch its result
Authentication
Authenticate by sending your secret API key as a Bearer token on every /public/v1 request. Keep keys server-side; never ship them in browser or mobile client code.
| Header | Value |
|---|---|
| Authorization | Bearer vf_live_… |
| Content-Type | application/json (on requests with a body) |
Getting a key
API keys are created and managed in the VideoForce dashboard under Organization → API Management (organization owner or admin only). A key belongs to your organization and is scoped to one workspace.
vf_live_…) exactly once. Copy it immediately and store it in a secret manager — it cannot be retrieved again. Rotating a key revokes the previous one immediately.Failed authentication
A missing, malformed, revoked, or expired key returns 401 with a uniform "Invalid API key" body — the API does not reveal which of those it was.
Quickstart
End to end in five steps: set your key, upload a source video, confirm it's ready, generate highlights, and generate an ad. All examples use production.
1 · Set your API key
Store the key in an environment variable so it never lands in source control.
# macOS / Linux export VIDEOFORCE_API_KEY="vf_live_your_key_here" # Windows PowerShell (current session) $env:VIDEOFORCE_API_KEY = "vf_live_your_key_here"
2 · Upload a source video
Uploading is three moves: create the record and get a short-lived signed URL, PUT the raw bytes to that URL (echoing every header in upload_headers verbatim), then poll the video until its status is ready.
import os, time, requests BASE = "https://api.videoforce.ai/public/v1" KEY = os.environ["VIDEOFORCE_API_KEY"] auth = {"Authorization": f"Bearer {KEY}"} PROJECT_ID = "<your-project-id>" PATH = "/path/to/video.mp4" # 1. create the record + get a signed upload URL r = requests.post(f"{BASE}/videos/upload", headers=auth, json={ "filename": os.path.basename(PATH), "content_type": "video/mp4", "size_bytes": os.path.getsize(PATH), "project_id": PROJECT_ID, }) r.raise_for_status() v = r.json(); video_id = v["video_id"] # 2. PUT the bytes straight to storage — send upload_headers verbatim with open(PATH, "rb") as f: requests.put(v["upload_url"], headers=v["upload_headers"], data=f).raise_for_status() # 3. poll until ready: uploading -> processing -> ready while True: d = requests.get(f"{BASE}/videos/{video_id}", headers=auth).json() print("status:", d["status"]) if d["status"] in ("ready", "failed"): break time.sleep(10) print(video_id, d["status"])
video_id — you'll pass it to highlights and ads. A video must be ready before you can generate from it.3 · Generate highlights, then poll & fetch
Trigger extraction, then follow the async pattern: poll the job to a terminal status, then fetch its result. See Job lifecycle for the full model.
# submit — returns 202 with a job_id + poll_url curl "$BASE/highlights" \ -H "Authorization: Bearer $VIDEOFORCE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"project_id":"<project>","video_id":"<video>"}' # poll until status is terminal, then fetch the result curl "$BASE/jobs/<job_id>" -H "Authorization: Bearer $VIDEOFORCE_API_KEY" curl "$BASE/jobs/<job_id>/result" -H "Authorization: Bearer $VIDEOFORCE_API_KEY"
4 · Generate a winning ad
Send an inline brief (all seven fields required, non-empty) plus one or more source video_ids. Same async flow — poll the returned job, then fetch its result. See POST /ads for the full field reference.
Environments
Point requests at the host for your environment. The base path /public/v1 is identical across all of them.
| Environment | Base URL | Status |
|---|---|---|
| Production | https://api.videoforce.ai/public/v1 | available |
| Staging | https://api.stg.videoforce.ai/public/v1 | available |
| Development | https://api.dev.videoforce.ai/public/v1 | available |
/public/v1) returns a 404 by design — it isn't an endpoint. To check connectivity, call GET /public/v1/ping, which returns {"status":"ok"} without authentication.Job lifecycle
Ad and highlight generation are long-running, so they're asynchronous. The trigger call doesn't return media — it returns a job handle you poll until it reaches a terminal state, then you fetch the result.
POST /ads or POST /highlights — 202 with job_id, status: "queued", and poll_url.GET /jobs/{job_id} until status is terminal. Non-terminal responses carry a Retry-After header (seconds) — use it as your backoff.GET /jobs/{job_id}/result returns the produced resources with fresh signed download URLs. Call it again anytime to mint new URLs.Job statuses
| Status | Terminal? | Meaning |
|---|---|---|
| queued | no | Accepted, waiting for a worker. |
| processing | no | Actively generating. |
| succeeded | yes | Done — /result returns the produced resources. |
| succeeded_empty | yes | Completed but produced no output (e.g. no highlights found). /result returns an empty resources array. |
| failed | yes | Generation errored — see the job's error object. |
| aborted | yes | Stopped before starting (e.g. insufficient credits) — never billed. |
Poll until status is one of succeeded, succeeded_empty, failed, or aborted — these are final and will not change.
A complete poll loop
import time, requests def run_and_wait(base, key, path, body): auth = {"Authorization": f"Bearer {key}"} # submit job = requests.post(f"{base}{path}", headers={**auth, "Idempotency-Key": __import__("uuid").uuid4().hex}, json=body).json() job_id = job["job_id"] # poll with backoff, honoring Retry-After delay = 3 while True: r = requests.get(f"{base}/jobs/{job_id}", headers=auth) d = r.json() if d["status"] in {"succeeded","succeeded_empty","failed","aborted"}: break time.sleep(int(r.headers.get("Retry-After", delay))) delay = min(delay * 2, 30) if d["status"] == "failed": raise RuntimeError(d["error"]) if d["status"] == "succeeded": return requests.get(f"{base}/jobs/{job_id}/result", headers=auth).json() return {"resources": []} # succeeded_empty / aborted
expires_at). Don't store the URL — store the resource_id/media_file_id and re-call /result to mint fresh links. Webhook callbacks (to avoid polling) are planned but not available in v1.Rate limits
Generation jobs are limited per organization across three windows. Exceeding one returns 429 with IETF RateLimit policy headers.
| Window | Limit | Retry-After |
|---|---|---|
| Concurrent jobs (queued + processing) | 5 | — (slots free as jobs finish) |
| Jobs per hour | 30 | 3600 |
| Jobs per UTC day | 150 | seconds to midnight |
| Header | Description |
|---|---|
| RateLimit-Policy | All policies, e.g. "concurrency";q=5, "hourly";q=30;w=3600, "daily";q=150;w=86400. |
| RateLimit | The blocked policy with remaining r=0. |
| Retry-After | Seconds until the window resets (hourly/daily blocks only; absent for concurrency). |
Errors
Standard HTTP status codes. Every failure is RFC 9457 application/problem+json. The machine-readable identifier is the type URL's slug; include request_id when contacting support.
{
"type": "https://videoforce.ai/problems/video-not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "One or more input videos do not exist in the selected workspace.",
"instance": "/public/v1/highlights",
"request_id": "…#123",
"context": { "invalid_video_ids": ["…"] }
}Validation errors (422) additionally carry an errors array of { field, message }.
| Status | When it happens |
|---|---|
| 200 / 201 / 202 / 204 | Success. 202 = async job accepted; 204 = no content. |
| 400 | Missing/invalid Idempotency-Key on a generation call. |
| 401 | Missing, malformed, revoked, or expired API key (Invalid API key). |
| 402 | Insufficient credits — the job is aborted and never billed. |
| 404 | Resource not found or not in your workspace (indistinguishable, by design). |
| 409 | Job not finished yet or failed/aborted (on /result), highlights already generated for that video, or an idempotent retry (same key + body) is still in flight. |
| 422 | Request validation failed, or an Idempotency-Key was reused with a different body. |
| 429 | An organization rate-limit window is exhausted — see Rate limits. |
| 503 | A dependency is temporarily unavailable — retry after Retry-After seconds. |
Idempotency
Generation calls (POST /ads, POST /highlights) require an Idempotency-Key header — a unique value (e.g. a UUID) per logical request. It makes network retries safe and prevents duplicate jobs and double-billing.
| Case | Result |
|---|---|
| Header missing / empty / > 255 chars | 400 idempotency-key-required |
| Same key + same body — original still running | 409 idempotency-key-in-flight — back off and re-poll the job |
| Same key + same body — original finished | Replays the original job (header Idempotent-Replayed: true) — no duplicate created or billed |
| Same key + different body | 422 idempotency-body-mismatch |
Pagination
List endpoints paginate. Two styles, depending on the resource.
| Style | Used by | How |
|---|---|---|
| Cursor | projects, ads, highlights | Pass limit (1–100). Read has_more and next_cursor; pass next_cursor back as cursor for the next page. |
| Page | videos | Pass page (≥1) and page_size (1–100). Response includes total, page, page_size. |
Versioning
The API is versioned in the URL path (/public/v1). Backwards-incompatible changes ship under a new path segment (/public/v2); additive changes (new endpoints, new optional fields, new enum values) can appear within v1 without notice, so write tolerant clients that ignore unknown fields.
Changelog
Notable changes, newest first.
| Date | Change |
|---|---|
| 2026-08 | Added Production environment (https://api.videoforce.ai) is now publicly available. |
| 2026-07 | Added Per-organization concurrent-job limit raised from 2 to 5. |
| 2026-07 | Added Public API v1: projects, video upload, ad & highlight generation, async jobs, and result retrieval. |
Support
Need help integrating?
- Email support@videoforce.ai — include the
request_idfrom any error response so we can trace it. - Manage keys and workspaces in the VideoForce dashboard.