Skip to main content

Project Management API

kvidAI's Project Management API is a REST interface for organizing video editing work into projects — long-lived containers that hold a Remotion-style composition (tracks, items, assets) plus chat history, rendering status, and per-project metadata.

You use this API when you want to build your own client around the same project model that powers kvid.ai — for example a bulk-import script, a Slack bot that triggers renders, or an integration pipeline that hands a composition to the Agent API for AI editing.

🎯 Service Overview

Concepts

  • Project — a JSON record per video editor session. Owned by a user (email), holds the composition, chat_history, status (draft / rendering / completed / failed), an optional template_id, and a thumbnail_url.
  • Composition — Remotion-compatible JSON: { fps, compositionWidth, compositionHeight, tracks[], items{}, assets{} }. Mutated through PATCH /composition so you don't have to re-send the entire record.
  • Template — a Strapi video-template record selected at creation time. Drives the default voice, tone, color palette, and so on. Set with templateId in create.

Authentication

Every endpoint identifies the caller by email (passed in the request body or as a query parameter) plus an API key delivered in the api-key header. The backend verifies the key resolves to that email — keys cannot read or mutate other users' projects.

Get an API key at kvid.ai/dashboard/api-keys.

Pricing for AI-driven operations (rendering, generation triggered via the agent) is documented in Pricing. Plain CRUD calls on this API are free.

📡 API Endpoints

Base Information

Base URL:       https://api.kvid.ai
Authentication: api-key header
Content-Type: application/json
MethodPathPurpose
POST/api/video-projectCreate a new project
GET/api/video-projectList projects for a user (paginated)
GET/api/video-project/:idFetch one project (includes composition)
PUT/api/video-project/:idUpdate top-level fields (name, status, thumbnail)
PATCH/api/video-project/:id/compositionPatch the composition tree
POST/api/video-project/:id/duplicateClone a project (composition + template)
POST/api/video-project/:id/renderKick off a render job
POST/api/video-project/:id/chatAppend a message to the project's chat history
DELETE/api/video-project/:idDelete a project

1. Create a project

POST /api/video-project

Request body

FieldTypeRequiredNotes
emailstringyesOwner's email (matches API key).
namestringnoDefaults to "Untitled Project".
compositionobjectnoInitial composition. Defaults to an empty 1920×1080 composition.
settingsobjectnoFree-form per-project preferences.
templateIdstringnoStrapi video-template.templateId. null falls back to system_default.

Python

import requests

API_KEY = "YOUR_API_KEY"
EMAIL = "[email protected]"

resp = requests.post(
"https://api.kvid.ai/api/video-project",
headers={
"api-key": API_KEY,
"Content-Type": "application/json",
},
json={
"email": EMAIL,
"name": "Sunset Beach Promo",
"templateId": "review-owl",
},
)
resp.raise_for_status()
project = resp.json()["data"]
print(project["id"], project["status"])

JavaScript (Node)

const res = await fetch("https://api.kvid.ai/api/video-project", {
method: "POST",
headers: {
"api-key": process.env.KVIDAI_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "[email protected]",
name: "Sunset Beach Promo",
templateId: "review-owl",
}),
});
const { data: project } = await res.json();
console.log(project.id, project.status);

Response

{
"success": true,
"data": {
"id": 1234,
"name": "Sunset Beach Promo",
"email": "[email protected]",
"composition": { "fps": 30, "compositionWidth": 1920, "compositionHeight": 1080, "tracks": [], "items": {}, "assets": {} },
"status": "draft",
"thumbnail_url": null,
"template_id": "review-owl",
"last_edited_at": "2026-05-26T09:00:00.000Z"
}
}

2. List a user's projects

GET /api/video-project?email={email}

Query parameters

ParameterDefaultNotes
emailrequiredOwner
page11-based
pageSize12Max 50
searchCase-insensitive partial match on name
sortlatestlatest / oldest / name-asc / name-desc
statusFilter to one of draft / rendering / completed
curl -G "https://api.kvid.ai/api/video-project" \
-H "api-key: $KVIDAI_API_KEY" \
--data-urlencode "[email protected]" \
--data-urlencode "page=1" \
--data-urlencode "pageSize=20" \
--data-urlencode "sort=latest"
{
"success": true,
"data": [
{ "id": 1234, "name": "Sunset Beach Promo", "status": "draft", "thumbnail_url": "https://...", "last_edited_at": "..." }
],
"meta": {
"pagination": { "page": 1, "pageSize": 20, "total": 37, "pageCount": 2 }
}
}

Project summaries omit composition and chat_history to keep responses small. Use the GET /:id endpoint to fetch the full record.


3. Fetch one project

GET /api/video-project/:id?email={email}

Returns the full record including composition, chat_history, settings, and template_id.

resp = requests.get(
f"https://api.kvid.ai/api/video-project/{project_id}",
headers={"api-key": API_KEY},
params={"email": EMAIL},
)
project = resp.json()["data"]
print(len(project["composition"]["tracks"]), "tracks")

4. Update top-level fields

PUT /api/video-project/:id

Use this for non-composition edits — renaming, marking complete, attaching a thumbnail, swapping settings.

await fetch(`https://api.kvid.ai/api/video-project/${id}`, {
method: "PUT",
headers: {
"api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "[email protected]",
name: "Sunset Beach Promo — final cut",
status: "completed",
}),
});

To patch the composition, use PATCH /:id/composition instead — that endpoint understands replace / merge operations and only ships the diff.


5. Patch the composition

PATCH /api/video-project/:id/composition

Request body

FieldTypeNotes
emailstringOwner.
operation"replace" | "merge"replace overwrites the entire composition. merge shallow-merges top-level keys.
data.compositionobjectThe new composition (full or partial depending on operation).
new_composition = {
"fps": 30,
"compositionWidth": 1920,
"compositionHeight": 1080,
"tracks": [{ "id": "track-1", "trackType": "video", "name": "Main", "items": ["item-1"] }],
"items": {
"item-1": { "id": "item-1", "type": "video", "assetId": "asset-1", "from": 0, "durationInFrames": 150 },
},
"assets": {
"asset-1": { "id": "asset-1", "type": "video", "remoteUrl": "https://cdn.kvid.ai/.../clip.mp4", "durationInSeconds": 5 },
},
}

requests.patch(
f"https://api.kvid.ai/api/video-project/{project_id}/composition",
headers={"api-key": API_KEY, "Content-Type": "application/json"},
json={"email": EMAIL, "operation": "replace", "data": {"composition": new_composition}},
)

The endpoint also recomputes thumbnail_url from the first image asset, so you don't need to upload a thumbnail separately.


6. Duplicate a project

POST /api/video-project/:id/duplicate

Clones composition, settings, and template_id. Chat history is not copied. The new project starts as draft.

const res = await fetch(`https://api.kvid.ai/api/video-project/${id}/duplicate`, {
method: "POST",
headers: {
"api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "[email protected]",
name: "Sunset Beach Promo (variant B)",
}),
});
const { data: clone } = await res.json();

7. Start a render

POST /api/video-project/:id/render

Triggers a server-side render. Returns immediately; poll GET /:id for status transitions (draftrenderingcompleted / failed) and read render_url on success.

requests.post(
f"https://api.kvid.ai/api/video-project/{project_id}/render",
headers={"api-key": API_KEY, "Content-Type": "application/json"},
json={"email": EMAIL},
)

# Poll until completed
import time
while True:
project = requests.get(
f"https://api.kvid.ai/api/video-project/{project_id}",
headers={"api-key": API_KEY},
params={"email": EMAIL},
).json()["data"]
if project["status"] in ("completed", "failed"):
break
time.sleep(5)

print(project["render_url"])

8. Append a chat message

POST /api/video-project/:id/chat

Stores a message in the project's persistent chat history. The Agent API calls this automatically; explicit use is mainly for surfacing system notes from your own pipeline.

await fetch(`https://api.kvid.ai/api/video-project/${id}/chat`, {
method: "POST",
headers: { "api-key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
email: "[email protected]",
message: { role: "system", content: "Imported 12 clips from Dropbox folder /sunset-shoot" },
}),
});

9. Delete a project

DELETE /api/video-project/:id?email={email}

Hard delete — the record is removed and cannot be restored. Render outputs already stored on CDN are not deleted automatically; track them separately if you need to clean up media.

curl -X DELETE "https://api.kvid.ai/api/video-project/[email protected]" \
-H "api-key: $KVIDAI_API_KEY"

End-to-End: Build a project, then let the agent edit it

import requests

API_KEY = "YOUR_API_KEY"
EMAIL = "[email protected]"

# 1. Create a project (pick a template up front so the agent gets sensible defaults)
project = requests.post(
"https://api.kvid.ai/api/video-project",
headers={"api-key": API_KEY, "Content-Type": "application/json"},
json={"email": EMAIL, "name": "Tech Review", "templateId": "sod"},
).json()["data"]

# 2. Hand the project to the Agent API — see ./agent-api.md for the streaming protocol
# (the Agent API will mutate the composition through /composition under the hood)

See the Agent API guide for the streaming side of the flow.


Error Responses

HTTPBody exampleWhen
400{ "error": "EMAIL_REQUIRED" }Missing required field
401{ "error": "UNAUTHORIZED" }Bad or missing API key
403{ "error": "FORBIDDEN" }API key does not own the target project
404{ "error": "PROJECT_NOT_FOUND" }id doesn't exist
409{ "error": "CONCURRENT_LIMIT" }Another long-running job (render / agent) is already in flight
500{ "error": "INTERNAL_ERROR" }Unexpected failure — inspect message

All error responses share the shape { "success": false, "error": "...", "message": "...", "data": { ... } } so a single client helper can render them.