← Home
DEVELOPERS

By Valmera Editorial · Published · Updated

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:

RESPONSE
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.

SHELL
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:

JSON
{
  "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/json first, or a one-event SSE stream when text/event-stream comes first.
  • A tool that can't do what you asked returns a normal result with "isError": true and the reason in content. Unknown methods and malformed JSON return JSON-RPC errors.
  • Results include structuredContent where it helps code: the new edl_version after an edit, and the job's state and progress from wait_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.

TYPESCRIPT · @modelcontextprotocol/client
// 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:

TYPESCRIPT · @modelcontextprotocol/sdk v1
// 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.

PYTHON · mcp 2.x
# 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:

PYTHON · mcp 1.x
# 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_id from list_projects or create_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_at with output_times). Cuts make the two diverge.
  • Word-level transcript. get_words returns each word with its start and end time for a source-time range, up to 400 words per response. get_transcript gives sentences with timestamps, search_transcript finds where something is said, and get_kept_transcript shows what the current edit keeps, in program time, with repeated phrases flagged.
  • Visual and audio evidence. get_shots, find_silences, get_audio_analysis and get_editorial_map read the index built at upload; look_at returns 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_edl returns the current EDL and its version. Request sections such as keep, captions, music or effects; large timelines come back as a compact index you can page with offset and limit.
  • restore_range puts a cut source range back without touching the rest of the edit. reset_edit starts again from the full source; earlier versions stay in history.
  • apply_edit_batch applies up to 64 operations atomically against a base_version, with an operation_id you 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:

TOOL RESULT
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.

SEQUENCE
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_preview renders 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_video returns a direct MP4 link to the current edit (rendering it first if needed), and download_url(kind="preview") returns a preview link.
  • export_final(project_id, edl_version) renders the full-resolution H.264 MP4 from the original file. Poll wait_for_job, then call download_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:

PIPELINE
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 link

Check 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 sendA JSON timeline or template plus dataFootage, then edit operations as tool calls
It knows about your mediaWhat you put in the JSONWord-level transcript, silences, shots and frames
Cut points come fromYour codeYour code or a model reading the transcript and frames
RevisionsSend a new compositionVersioned EDL; restore any cut
CompletionStatus polling or webhookswait_for_job polling
Best forData-driven templates at volumeEditing 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

Yes. A script or agent can create a project, upload with upload_start and upload_finish, wait for index_status, apply edits with the editing tools or apply_edit_batch, render and check a preview, then call export_final, wait_for_job and download_url. Slow steps return job ids, so a pipeline polls wait_for_job instead of blocking.
Yes. Valmera's API is its MCP server at https://valmera.io/mcp/server. Your code sends JSON-RPC 2.0 requests over Streamable HTTP with a bearer token or OAuth and calls the same tools an AI assistant uses: transcripts, cuts, captions, reframing, b-roll, music, previews and MP4 export.
No. MCP is a protocol, so a script can call tools/list and tools/call directly. Many teams still put a model in the loop to choose cut points, but every tool works with arguments your own code supplies.
There is no separate REST API or Valmera SDK. MCP is the API, so the official MCP client libraries for TypeScript and Python (or plain curl) are the SDK. tools/list returns a JSON Schema for every tool's arguments.
Call get_words with a project_id and a source-time range. It returns each word with its start and end time, up to 400 words per response, so page through longer ranges. get_transcript returns sentence-level text, search_transcript finds where something is said, and get_kept_transcript shows what the current edit keeps.
Yes. Call export_final with the project_id and the edl_version you approved, poll wait_for_job until the job is done, then call download_url with kind "final" for a temporary link. The MP4 is full resolution, rendered from the original upload, and ends with a brief Valmera end card.
No. Slow tools return a job ID and you poll wait_for_job, which waits up to about 25 seconds per call and reports state and progress. Polling the same job is always safe.
get_edl returns the current EDL as JSON with its version number, and every edit creates a new version you can roll back from. There is no CMX 3600, XML, AAF or OpenTimelineIO export; the deliverable is the rendered MP4.
Creating an account and uploading are free. Editing requires a subscription, and MCP access is included with eligible Valmera subscriptions; see the plans at valmera.io/subscribe.

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.

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 →
See pricing →

Related Articles

Valmera MCP Server
Connect Claude, ChatGPT, Cursor or any MCP client in a minute.
MCP Tool Reference
Every tool grouped by job, with what each one does.
Edit Video From Claude Code
The same API driven by an agent in your terminal.
Edit Decision List (EDL)
The non-destructive, versioned document every edit writes.
FFmpeg AI Alternative
When a shell pipeline is the better tool, and when it isn't.