# Opus Video Maker: guide for agents

Turns a brief, plus optional images, audio, video, PDFs or links, into a finished MP4: an AI director plans the shots, writes the animation as code, renders it frame by frame and adds voice-over. A video takes about 10-60 minutes and is billed to the account balance.

- MCP: `https://maker.vcorp.ai/mcp` (Streamable HTTP, stateless, POST only)
- REST: `https://maker.vcorp.ai/v1`, described by [OpenAPI](https://maker.vcorp.ai/openapi.json)
- Overview for LLMs: [llms.txt](https://maker.vcorp.ai/llms.txt) · Agent Skill: [SKILL.md](https://maker.vcorp.ai/agents/SKILL.md)

## Accounts and auth

Accounts are made by the site's admin, and each has a USD balance that jobs are billed to. Two ways to authenticate:

1. **OAuth 2.1** (MCP only): the MCP endpoint answers 401 with `WWW-Authenticate: Bearer resource_metadata="https://maker.vcorp.ai/.well-known/oauth-protected-resource/mcp"`. Authorization server metadata is at `https://maker.vcorp.ai/.well-known/oauth-authorization-server`; clients register at `/oauth/register` (RFC 7591), use the code flow with PKCE S256, and get one-hour access tokens with rotating refresh tokens. Scope: `videos`. The user can revoke an app in the web app.
2. **API key** (MCP and REST): `Authorization: Bearer ovm_...`, made in the web app under 接入 Agent (up to 10, shown once, revocable).

## Setup by client

### Clients that log in with OAuth (no key to copy)

- **Claude.ai / Claude Desktop**: Settings → Connectors → Add custom connector, URL `https://maker.vcorp.ai/mcp`. You are sent here to log in and approve.
- **ChatGPT**: add a custom connector (MCP) with URL `https://maker.vcorp.ai/mcp` and OAuth authentication.
- **Cursor**: `~/.cursor/mcp.json`
  ```json
  { "mcpServers": { "opus-video": { "url": "https://maker.vcorp.ai/mcp" } } }
  ```
  Cursor opens the login page on first use. Use OAuth here: Cursor starts OAuth whenever a server offers it, even with an Authorization header configured.
- **VS Code**: `.vscode/mcp.json`
  ```json
  { "servers": { "opus-video": { "type": "http", "url": "https://maker.vcorp.ai/mcp" } } }
  ```

### Clients that use an API key

Create a key in the web app under 接入 Agent. It is shown once.

- **Claude Code**: `claude mcp add --transport http opus-video https://maker.vcorp.ai/mcp --header "Authorization: Bearer ovm_..."` (or leave out `--header` and log in with `/mcp`)
- **Codex CLI**: `~/.codex/config.toml`, with the key in the environment variable `OVM_API_KEY`
  ```toml
  [mcp_servers.opus-video]
  url = "https://maker.vcorp.ai/mcp"
  bearer_token_env_var = "OVM_API_KEY"
  ```
- **Gemini CLI**: `settings.json`
  ```json
  { "mcpServers": { "opus-video": { "httpUrl": "https://maker.vcorp.ai/mcp", "headers": { "Authorization": "Bearer ovm_..." } } } }
  ```
- **Anything else** (n8n, Dify, Coze, GPT Actions, scripts): the REST API, see below.

## How a job runs

Opus Video Maker turns a brief (plus optional images, audio, video, PDFs or links) into a finished MP4. An agent writes and renders the video frame by frame, so a job takes roughly 10 to 60 minutes.

Workflow:
1. create_video returns a job_id immediately. The job is queued; each user's jobs run one at a time.
2. Call wait_for_video(job_id) repeatedly. Each call returns within about a minute, sooner if the job finishes or needs you.
   If you cannot wait, give the job_id to the user and check back later with get_video: the job keeps running without you.
3. When status is "done", video_url is the MP4. Links last 7 days; call get_video again for fresh ones.

Instead of polling you may pass callback_url (https, public): the server POSTs there when the job is done, failed, cancelled, waiting for plan review, or left interrupted by a restart, and retries for about a day until it gets a 2xx. Requests follow Standard Webhooks (https://www.standardwebhooks.com/): verify the "webhook-signature" header with the webhook.secret returned by create_video; the body is {type, timestamp, data} where data is what get_video returns. Events may arrive more than once or out of order; use data.updated_at, and answer 410 to stop them.

By default the plan is not reviewed: the video is rendered straight after planning. With review=true the job stops at "awaiting_approval" with a contact sheet (one frame per shot); then call approve_plan or revise_video.
Pass a client_request_id with create_video and reuse it when retrying, so a retry after a timeout does not start (and bill) a second video.
Every job is billed to the key owner's balance and capped by budget_usd; get_account shows the balance and limits before you start.

Statuses: `queued` → `planning` → (`awaiting_approval` when review is on) → `rendering` → `done`. A job can also end `failed`, `cancelled`, or `interrupted` (cut off by a server restart; it normally resumes by itself). `continue_video` resumes the last three. Every view has `next` (what to do now, in words) and `poll_after_seconds`.

A typical agent turn:

1. `get_account`, if unsure the balance covers a new job.
2. `create_video` with a `client_request_id` → `job_id`.
3. Tell the user the job_id and roughly how long it takes.
4. `wait_for_video` in a loop while the status is queued, planning or rendering; or stop and check back later with `get_video`; or rely on the webhook.
5. On `done`: hand over `video_url` (valid 7 days; `get_video` gives fresh links). To change something, `revise_video` with the feedback.

## Tools

### create_video

Start making a video from a brief. Returns a job_id at once; the video itself takes about 10-60 minutes. Follow up with wait_for_video or get_video.

| Argument | Type | Notes |
|---|---|---|
| `prompt` * | string | The brief: what the video is about, audience, tone, must-have shots or text, and how to use the attached material. Any language. |
| `aspect` | `16:9` / `9:16` / `1:1` / `4:3` | Frame shape. 9:16 for vertical phone video. Default `"16:9"`. |
| `duration_seconds` | integer |  Default `30`. |
| `style` | `auto` / `explainer` / `news` / `product` / `art` / `motion` / `mv` / `edit` | Kind of video. auto: 由你按内容判断; explainer: 知识解说／科普动画; news: 竖屏资讯／评测; product: 产品宣传片; art: 艺术风格动画（按需求里描述的画风）; motion: 抽象动效／动态字体／showreel; mv: 跟音乐卡点的 MV; edit: 在用户的视频素材上剪辑、加字幕 Default `"auto"`. |
| `voice` | `none` / `中文` / `英文` / `中英双语` | Voice-over: none, Chinese (中文), English (英文), or one version in each (中英双语). Default `"中文"`. |
| `budget_usd` | number | Hard cap on what this job may spend, in USD. Default `20`. |
| `effort` | `low` / `medium` / `high` / `xhigh` / `max` | How hard the director thinks. Higher is slower and costs more. Default `"high"`. |
| `review` | boolean | Stop after planning for approve_plan / revise_video instead of rendering straight away. Default `false`. |
| `links` | array of string | http(s) links to articles, pages or PDFs to use as material. Fetched inside the sandbox. |
| `files` | array of object | Small attachments inline, at most 20 MB decoded in total: images, audio, video, PDF, txt, md. Put larger material behind links. |
| `client_request_id` | string | Your own id for this request. Retrying with the same id within 24 hours returns the existing job instead of starting another. |
| `callback_url` | string | Optional https URL on a public host to POST to when the job settles (Standard Webhooks; the signing secret comes back as webhook.secret). Lets you stop polling. |

### wait_for_video

Wait for a job to finish or need attention, up to timeout_seconds (default 45, max 55), then return its state like get_video. Call it again while status is queued, planning or rendering.

| Argument | Type | Notes |
|---|---|---|
| `job_id` * | string | The job_id returned by create_video. |
| `timeout_seconds` | integer |  Default `45`. |

### get_video

Current state of a job: status, queue position, cost, recent progress, and download links once there is output. When the plan awaits review, the contact sheet is attached as an image.

| Argument | Type | Notes |
|---|---|---|
| `job_id` * | string | The job_id returned by create_video. |

### list_videos

Recent jobs of this account, newest first.

| Argument | Type | Notes |
|---|---|---|
| `status` | string | Only jobs in this status (queued, planning, awaiting_approval, rendering, done, failed, cancelled, interrupted). |
| `limit` | integer |  Default `10`. |

### approve_plan

For a job in awaiting_approval: accept the plan and render the full video. Optional feedback is applied while rendering.

| Argument | Type | Notes |
|---|---|---|
| `job_id` * | string | The job_id returned by create_video. |
| `feedback` | string |  |

### revise_video

Ask for changes. In awaiting_approval this redoes the plan; on a done job it re-renders the video (the old one is kept as final_vN.mp4).

| Argument | Type | Notes |
|---|---|---|
| `job_id` * | string | The job_id returned by create_video. |
| `feedback` * | string | What to change. |

### continue_video

Resume a failed, interrupted or cancelled job from where it stopped.

| Argument | Type | Notes |
|---|---|---|
| `job_id` * | string | The job_id returned by create_video. |
| `note` | string | Optional extra instructions. |

### get_account

Balance, what a new job needs to start, jobs queued or running, and the limits that apply. Check it before create_video if you are unsure the account can pay.

### cancel_video

Stop a queued or running job. What it has spent so far stays billed.

| Argument | Type | Notes |
|---|---|---|
| `job_id` * | string | The job_id returned by create_video. |


## Limits

- Inline files: 20 MB decoded per call in total; put larger material behind `links` (fetched inside the sandbox, never by this server).
- A long poll waits at most 55 seconds.
- At most 5 jobs queued or running per account when an agent starts one; one runs at a time.
- 120 requests per minute per key or app.
- A new job needs a minimum balance (see `get_account`); each job is capped by `budget_usd`.

## REST

The same operations, with an API key. Bodies and answers are the MCP arguments and views as JSON. Errors are `{"error": {"status", "message"}}`.

| | |
|---|---|
| `GET /v1/account` | get_account |
| `POST /v1/videos` | create_video. `Idempotency-Key` header works like `client_request_id`. 202 when created, 200 for a repeat. |
| `GET /v1/videos?status=&limit=` | list_videos |
| `GET /v1/videos/{id}` | get_video |
| `GET /v1/videos/{id}/wait?timeout=45` | wait_for_video |
| `POST /v1/videos/{id}/approve` `{feedback?}` | approve_plan |
| `POST /v1/videos/{id}/revise` `{feedback}` | revise_video |
| `POST /v1/videos/{id}/continue` `{note?}` | continue_video |
| `POST /v1/videos/{id}/cancel` | cancel_video |

```sh
curl -s https://maker.vcorp.ai/v1/videos -H "Authorization: Bearer $OVM_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-teaser-1" \
  -d '{"prompt": "A 20-second vertical teaser for our note-taking app", "aspect": "9:16", "duration_seconds": 20}'
curl -s "https://maker.vcorp.ai/v1/videos/<job_id>/wait?timeout=45" -H "Authorization: Bearer $OVM_API_KEY"
```

## Webhooks

Pass `callback_url` (https, public host) to create_video. The server POSTs `{type, timestamp, data}` when the job is `done`, `failed`, `cancelled`, waiting for review, or left interrupted; `data` is what get_video returns. Signed per [Standard Webhooks](https://www.standardwebhooks.com/): headers `webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64 HMAC-SHA256 of "id.timestamp.body">`, keyed with the base64 part of `webhook.secret` (`whsec_...`) from create_video. Any 2xx counts as delivered; otherwise retried for about a day. Answer 410 to stop. Deliveries can repeat or arrive out of order: dedupe on `webhook-id`, order by `data.updated_at`.

## Shared videos

A video the owner shares has a public page `https://maker.vcorp.ai/s/<token>` with schema.org `VideoObject` JSON-LD, and the same content as JSON at `https://maker.vcorp.ai/s/<token>.json`.
