AI video editing API for developers
Valmera's API is its MCP server. Send JSON-RPC 2.0 requests to https://valmera.io/mcp/server over Streamable HTTP with a bearer token or OAuth, and call the same tools Claude and ChatGPT use: read word-level transcripts, cut silences and filler words, add captions, reframe, render previews and export a full-resolution MP4.
There is no separate REST API or Valmera SDK. Any MCP client library works, and your code doesn't need an LLM: every tool takes plain JSON arguments. Edits are non-destructive and versioned, slow work returns a job ID, and the final export renders from your original file.
- Endpoint
- https://valmera.io/mcp/server
- Protocol
- Model Context Protocol over Streamable HTTP, JSON-RPC 2.0. Protocol version 2025-06-18 (2025-03-26 and 2024-11-05 also accepted).
- Auth
- Authorization: Bearer vlm_mcp_… (create a token at valmera.io/mcp/connect), or OAuth 2.1 with dynamic client registration and PKCE (S256), scope valmera.edit.
- Tools
- 143 tools: 129 editing tools plus 14 session tools (live server card, October 2026). tools/list returns their JSON Schemas.
- Long work
- Renders and other slow calls return a job ID. Poll wait_for_job.
- Input
- MP4, MOV, M4V, MKV or WebM, up to 100 GB and 3 hours per file.
- Output
- Full-resolution H.264 MP4 from export_final, rendered from the original file.
- Access
- Creating an account and uploading are free. Editing requires a subscription.
Authenticate
Bearer token (scripts, CI, your own backend)
Sign in at valmera.io/mcp/connect, open Advanced setup · bearer token and create a private token. It starts with vlm_mcp_, is shown once, is stored only as a hash and can be revoked on the same page. Send it on every request as Authorization: Bearer vlm_mcp_… and keep it in an environment variable or secret store.
OAuth 2.1 (apps acting for other Valmera users)
An unauthenticated request gets the standard challenge, which points to the discovery documents:
HTTP/2 401
www-authenticate: Bearer realm="valmera", resource_metadata="https://valmera.io/.well-known/oauth-protected-resource"- Protected resource metadata: /.well-known/oauth-protected-resource
- Authorization server metadata: /.well-known/oauth-authorization-server
- Dynamic client registration (RFC 7591) at
/mcp/oauth/register; clients are public, with no secret. - Authorization code with PKCE (S256) at
/mcp/oauth/authorize, tokens and refresh tokens at/mcp/oauth/token, revocation at/mcp/oauth/revoke. Scope:valmera.edit.
The OAuth helpers in the official MCP SDKs run this flow for you. Both credential types reach the same account, projects and tools.
Your first calls with curl
Three requests: initialize, list the tools, call one.
export VALMERA_MCP_TOKEN="vlm_mcp_..." # create one at https://valmera.io/mcp/connect
# 1. Initialize
curl -sS https://valmera.io/mcp/server \
-H "Authorization: Bearer $VALMERA_MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-pipeline","version":"1.0.0"}}}'
# 2. List the tools and their input schemas
curl -sS https://valmera.io/mcp/server \
-H "Authorization: Bearer $VALMERA_MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 3. Call a tool: word-level timestamps for the first 30 seconds of project 4127
curl -sS https://valmera.io/mcp/server \
-H "Authorization: Bearer $VALMERA_MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_words","arguments":{"project_id":4127,"start":0,"end":30}}}'The initialize response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "valmera", "title": "Valmera Video Editor", "version": "0.1.0" },
"instructions": "…"
}
}- Valmera doesn't issue an
Mcp-Session-Id. Each request stands alone; send the Authorization header every time. - Responses are plain JSON when your Accept header lists
application/jsonfirst, or a one-event SSE stream whentext/event-streamcomes first. - A tool that can't do what you asked returns a normal result with
"isError": trueand the reason incontent. Unknown methods and malformed JSON return JSON-RPC errors. - Results include
structuredContentwhere it helps code: the newedl_versionafter an edit, and the job'sstateandprogressfromwait_for_job.
TypeScript: the official MCP SDK
Version 2 of the MCP TypeScript SDK ships the client as @modelcontextprotocol/client. An AuthProvider with a token() function attaches your bearer token to every request.
// npm install @modelcontextprotocol/client
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
import type { AuthProvider } from "@modelcontextprotocol/client";
const authProvider: AuthProvider = { token: async () => process.env.VALMERA_MCP_TOKEN! };
const transport = new StreamableHTTPClientTransport(new URL("https://valmera.io/mcp/server"), { authProvider });
const client = new Client({ name: "my-pipeline", version: "1.0.0" });
await client.connect(transport); // runs the initialize handshake
const { tools } = await client.listTools();
console.log(tools.length, "tools");
const projects = await client.callTool({ name: "list_projects", arguments: {} });
console.log(projects.content);
const words = await client.callTool({
name: "get_words",
arguments: { project_id: 4127, start: 0, end: 30 },
});
console.log(words.content);
await client.close();Still on the v1 package, @modelcontextprotocol/sdk? Only the imports and the header option differ:
// npm install @modelcontextprotocol/sdk (v1.x)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://valmera.io/mcp/server"), {
requestInit: { headers: { Authorization: "Bearer " + process.env.VALMERA_MCP_TOKEN } },
});Python: the official mcp package
In mcp 2.x, headers live on an httpx2.AsyncClient that you pass to streamable_http_client. Keep the 300-second read timeout so long waits don't cut off.
# pip install mcp (2.x)
import asyncio
import os
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
URL = "https://valmera.io/mcp/server"
async def main() -> None:
headers = {"Authorization": f"Bearer {os.environ['VALMERA_MCP_TOKEN']}"}
async with httpx2.AsyncClient(headers=headers, timeout=httpx2.Timeout(30.0, read=300.0)) as http:
# Valmera speaks the 2025-06-18 handshake; the client falls back to it automatically.
async with Client(streamable_http_client(URL, http_client=http)) as client:
tools = await client.list_tools()
print(len(tools.tools), "tools")
result = await client.call_tool("get_words", {"project_id": 4127, "start": 0, "end": 30})
print(result.content[0].text)
asyncio.run(main())On mcp 1.x, the transport was streamablehttp_client and took headers directly. It was removed in 2.0:
# mcp 1.x (same URL and headers as above)
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("list_projects", {})Projects, clocks and word-level transcripts
- Explicit projects. Every project-scoped tool requires an integer
project_idfromlist_projectsorcreate_project. The server never guesses from an "active" project. - Two clocks. Source seconds address the original upload (
get_words,cut_range,restore_range). Output seconds address the edited program (cut_output_range,look_atwithoutput_times). Cuts make the two diverge. - Word-level transcript.
get_wordsreturns each word with its start and end time for a source-time range, up to 400 words per response.get_transcriptgives sentences with timestamps,search_transcriptfinds where something is said, andget_kept_transcriptshows what the current edit keeps, in program time, with repeated phrases flagged. - Visual and audio evidence.
get_shots,find_silences,get_audio_analysisandget_editorial_mapread the index built at upload;look_atreturns real frames.
A versioned edit decision list
Tools never touch pixels. Each edit writes a new numbered version of a JSON edit decision list (EDL), and the renderer compiles that document into video. The upload is never modified.
get_edlreturns the current EDL and its version. Request sections such askeep,captions,musicoreffects; large timelines come back as a compact index you can page withoffsetandlimit.restore_rangeputs a cut source range back without touching the rest of the edit.reset_editstarts again from the full source; earlier versions stay in history.apply_edit_batchapplies up to 64 operations atomically against abase_version, with anoperation_idyou reuse on retry, so a repeated request can't write twice.
Jobs: slow tools return a job ID
Fast tools answer inline. Anything that would take longer than about 25 seconds, such as renders, pixel repair, website recording or a long edit, returns a job ID instead of a fake completion:
STILL RUNNING — render_preview is job 8814 (running, 12%). Nothing has failed;
call wait_for_job(job_id=8814) to pick the result up.Call wait_for_job(job_id) in a loop. Each call waits a bounded time and returns either the result or the current state (queued, running, done or failed) with a progress percentage in structuredContent.job. The job keeps running whether or not you are waiting, so poll the same ID rather than calling the original tool again.
Upload a file
Video bytes never travel inside MCP messages. upload_start returns a presigned storage URL; you PUT the file there and then call upload_finish, which starts indexing.
upload_start(project_id=4127, filename="talk.mp4", size_bytes=734003200, kind="original")
→ up to 16 MiB: one presigned PUT URL (valid 12 hours) plus the exact curl command
→ larger files: multipart, with one presigned URL per part and the part size
curl -sS -f -X PUT -H 'Content-Type: video/mp4' --upload-file talk.mp4 '<presigned URL>'
# multipart: PUT each part and keep the ETag header from every response
upload_finish(project_id=4127, storage_key="…", filename="talk.mp4", kind="original")
# multipart: also pass upload_id and parts=[{"part_number": 1, "etag": "…"}, …]
index_status(project_id=4127) # repeat until it reports 'done'kind is original for the main video, or clip, music or image for supporting media. Indexing builds the word-level transcript with speaker labels, shots, silences and frame evidence once; every later call reads it. To pull media from a link instead, fetch_url imports clips, music and images (link imports allow clips up to 500 MB and 60 minutes).
Render, review and export
render_previewrenders the current version.quality="draft"is an inexpensive 480px preview;"approval"renders from the original at up to 720×1280 with the final composition path.watch_videoreturns a direct MP4 link to the current edit (rendering it first if needed), anddownload_url(kind="preview")returns a preview link.export_final(project_id, edl_version)renders the full-resolution H.264 MP4 from the original file. Pollwait_for_job, then calldownload_url(kind="final", edl_version)for a temporary link. Repeating the request reuses the existing export or its running job, so a retry never renders twice.
Exports end with a brief Valmera end card.
A complete pipeline
From a local talk recording to a captioned vertical MP4. Each line is one tools/call:
create_project(title="Weekly update") → project 4127
upload_start / PUT / upload_finish → analysis starts
index_status(project_id=4127) → 'done'
cut_silences(project_id=4127) → EDL v2
remove_filler_words(project_id=4127) → EDL v3
add_captions(project_id=4127, mode="from_transcript") → EDL v4
auto_reframe(project_id=4127, ratio="9:16") → EDL v5
get_kept_transcript(project_id=4127) → check what survived
render_preview(project_id=4127, quality="draft") → job → wait_for_job
export_final(project_id=4127, edl_version=5) → job 9120
wait_for_job(job_id=9120) → repeat until done
download_url(project_id=4127, kind="final", edl_version=5) → MP4 linkCheck get_kept_transcript and the preview before you export, and confirm that the version you pass to export_final is the one you reviewed. If an assistant drives the same pipeline, the MCP setup page connects Claude, ChatGPT, Cursor and other clients in a minute.
Valmera or a template render API?
They solve different problems, and many teams use both.
| Template render APIs (Shotstack, Creatomate, JSON2Video) | Valmera | |
|---|---|---|
| You send | A JSON timeline or template plus data | Footage, then edit operations as tool calls |
| It knows about your media | What you put in the JSON | Word-level transcript, silences, shots and frames |
| Cut points come from | Your code | Your code or a model reading the transcript and frames |
| Revisions | Send a new composition | Versioned EDL; restore any cut |
| Completion | Status polling or webhooks | wait_for_job polling |
| Best for | Data-driven templates at volume | Editing recorded talks, podcasts, demos and interviews |
Competitor details were checked on September 6, 2026 against Shotstack's Edit API, Creatomate's render endpoint and JSON2Video's movie endpoint. Shotstack also offers an MCP server for its renderer.
Limits
- No webhooks: poll
wait_for_job. - Output is an H.264 MP4. No codec or bitrate settings, no NLE export (CMX EDL, XML, AAF or OpenTimelineIO) and no SRT or VTT; captions are burned in.
- One editor per project at a time: editing the same project from Studio and over MCP at once is refused in both directions.
- Inputs up to 100 GB and 3 hours per file. English is the best-tested transcription language.
- It edits footage you provide; it doesn't generate a whole video from a prompt.
- Editing requires a subscription. See plans.
Frequently Asked Questions
Sources
Valmera facts come from the live server card and OAuth metadata, checked October 6, 2026. SDK import paths come from the official TypeScript and Python SDK documentation on the same date. The full tool list is in the tool reference.
- Valmera live server card
- Valmera OAuth authorization server metadata
- Valmera protected resource metadata
- Valmera MCP docs on GitHub
- MCP specification: tools (2025-06-18)
- MCP TypeScript SDK: connect over HTTP
- MCP Python SDK: client transports
- Shotstack Edit API
- Creatomate render endpoint
- JSON2Video create-movie endpoint
Made with Valmera: real exports
Get an API token
Creating an account and uploading are free. Editing requires a subscription. Create a private token on the connect page and make your first call.
Create a token →

