@clipwright/mcp-server 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/SKILL.md +391 -981
- package/dist/format.js +4 -2
- package/dist/format.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +71 -21
- package/dist/server.js.map +1 -1
- package/dist/workflow-tools.js +4 -1
- package/dist/workflow-tools.js.map +1 -1
- package/package.json +4 -5
- package/CHANGELOG.md +0 -1393
package/SKILL.md
CHANGED
|
@@ -12,137 +12,72 @@ author: Clipwright
|
|
|
12
12
|
homepage: https://clipwright.io/?ref=clawhub
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
# SKILL.md —
|
|
15
|
+
# SKILL.md — install `clipwright-mcp`, then make a video
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
have nothing to do with the MCP server.
|
|
17
|
+
This file is for an agent. It holds what the tool schemas cannot tell you: the
|
|
18
|
+
places where a call answers without an error and the answer is still wrong for
|
|
19
|
+
the user. Each such trap carries the date it was last checked.
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
under the MIT license, so you do NOT need the repository to install it — `npx` is
|
|
24
|
-
enough. Building from source is kept below as the development path.
|
|
21
|
+
Everything else is reference, kept in one place so it does not go stale here:
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
- the tool schemas (`tools/list`): every field, its limits and defaults. A field
|
|
24
|
+
the service accepts but does not honor says `NOT HONORED YET` in its own
|
|
25
|
+
description;
|
|
26
|
+
- https://clipwright.io/docs: the REST API, refusal codes and prices;
|
|
27
|
+
- https://api.clipwright.io/openapi.json: the REST contract as OpenAPI 3.1;
|
|
28
|
+
- https://clipwright.io/changelog: read its **Breaking:** entries before you
|
|
29
|
+
move to a newer version.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
`@clipwright/mcp-server` and `@clipwright/cli`
|
|
32
|
-
(REST API, render pipeline, dashboard) is closed. That split is deliberate, not
|
|
33
|
-
an oversight: see `LICENSE` in the repository root.
|
|
31
|
+
The client packages `@clipwright/core`, `@clipwright/sdk`,
|
|
32
|
+
`@clipwright/mcp-server` and `@clipwright/cli` are MIT; the service is closed.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
# Part 1 — Install
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
## 1. Connect
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
signed in with OAuth or with a `cw_*` key — section 1.4. Nothing to install;
|
|
41
|
-
- **this package run locally** over stdio with `CLIPWRIGHT_API_KEY` — sections
|
|
42
|
-
1.1 to 1.3, and section 2.
|
|
38
|
+
The hosted server and this package run locally serve the same tools.
|
|
43
39
|
|
|
44
|
-
### 1.1
|
|
40
|
+
### 1.1 The hosted server — nothing to install
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Expected: the process does NOT exit, prints `clipwright mcp server running on
|
|
51
|
-
stdio` to stderr, and hangs waiting for JSON-RPC. That is what a healthy server
|
|
52
|
-
looks like, not a freeze. Stop it with `Ctrl+C`.
|
|
42
|
+
Add `https://mcp.clipwright.io/mcp` (Streamable HTTP) to the client. On first
|
|
43
|
+
use the client opens a browser; the account holder signs in and approves the app.
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
| Client | How to add | Checked |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Claude Code | `claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp` in a session to sign in | verified live 2026-10-18 |
|
|
48
|
+
| Claude.ai, Claude Desktop | Customize → Connectors → Add custom connector, paste the URL | from the client's docs |
|
|
49
|
+
| ChatGPT | the URL as the remote MCP server of a plugin | from the client's docs |
|
|
50
|
+
| Cursor | `~/.cursor/mcp.json`: `{"mcpServers": {"clipwright": {"url": "https://mcp.clipwright.io/mcp"}}}` | from the client's docs |
|
|
51
|
+
| VS Code | `.mcp.json`: `{"mcpServers": {"clipwright": {"type": "http", "url": "https://mcp.clipwright.io/mcp"}}}` | from the client's docs |
|
|
52
|
+
| Gemini CLI | `gemini mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp auth clipwright` | from the client's docs |
|
|
53
|
+
| Codex | `codex mcp add clipwright --url https://mcp.clipwright.io/mcp`, then `codex mcp login clipwright` | from the client's docs |
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
`package.json` — with corepack available: `corepack enable`).
|
|
55
|
+
For CI and scripts, a `cw_*` key replaces OAuth and holds both permissions:
|
|
64
56
|
|
|
65
57
|
```bash
|
|
66
|
-
|
|
67
|
-
cd clipwright
|
|
68
|
-
pnpm install
|
|
69
|
-
pnpm build # pnpm -r build — builds every package in topological order
|
|
58
|
+
claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp --header "Authorization: Bearer cw_..."
|
|
70
59
|
```
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
`@clipwright/sdk` must be built first — `...` pulls them in automatically):
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
pnpm --filter @clipwright/mcp-server... build
|
|
77
|
-
```
|
|
61
|
+
What differs on the hosted server is in section 1.4.
|
|
78
62
|
|
|
79
|
-
|
|
63
|
+
### 1.2 The local server from npm
|
|
80
64
|
|
|
81
65
|
```bash
|
|
82
|
-
|
|
66
|
+
npx -y -p @clipwright/mcp-server@latest clipwright-mcp
|
|
83
67
|
```
|
|
84
68
|
|
|
85
|
-
|
|
69
|
+
A healthy server does not exit: it prints `clipwright mcp server running on
|
|
70
|
+
stdio` to stderr and waits for JSON-RPC. Exiting at once with
|
|
71
|
+
`CLIPWRIGHT_API_KEY env var is required` means the key did not reach it, not a
|
|
72
|
+
broken build. Both answers verified 2026-10-31. `@latest` is deliberate: it
|
|
73
|
+
keeps the client in step with the API.
|
|
86
74
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
CLIPWRIGHT_API_KEY=cw_test node packages/mcp-server/dist/index.js
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Expected: the process does NOT exit on its own, prints `clipwright mcp server
|
|
95
|
-
running on stdio` to stderr, and waits on stdio for JSON-RPC. Stop it with
|
|
96
|
-
`Ctrl+C`. If it instead exits immediately with `CLIPWRIGHT_API_KEY env var is
|
|
97
|
-
required`, the variable was not passed through (expected behaviour on an empty
|
|
98
|
-
key, see `src/index.ts`) — not a bug. If it dies on a module import, the build is
|
|
99
|
-
incomplete: go back to step 1.2 and run `pnpm build` from the root rather than a
|
|
100
|
-
targeted `--filter`, so `@clipwright/core` and `@clipwright/sdk` are definitely
|
|
101
|
-
rebuilt.
|
|
75
|
+
From a source checkout: `pnpm install && pnpm build` at the root, then run
|
|
76
|
+
`node packages/mcp-server/dist/index.js` with the same environment.
|
|
102
77
|
|
|
103
78
|
### 1.3 Connecting it to an MCP host
|
|
104
79
|
|
|
105
|
-
|
|
106
|
-
path to `dist/index.js`:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
realpath packages/mcp-server/dist/index.js
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
If you installed from npm (section 1.1), you do not need a path — take variant C
|
|
113
|
-
at the end of this section; it differs only in `command`/`args`.
|
|
114
|
-
|
|
115
|
-
**Variant A — `.mcp.json` (project-level host config):**
|
|
116
|
-
|
|
117
|
-
```json
|
|
118
|
-
{
|
|
119
|
-
"mcpServers": {
|
|
120
|
-
"clipwright": {
|
|
121
|
-
"type": "stdio",
|
|
122
|
-
"command": "node",
|
|
123
|
-
"args": ["/absolute/path/to/clipwright/packages/mcp-server/dist/index.js"],
|
|
124
|
-
"env": {
|
|
125
|
-
"CLIPWRIGHT_API_KEY": "cw_...",
|
|
126
|
-
"CLIPWRIGHT_API_URL": "https://api.clipwright.io",
|
|
127
|
-
"CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Variant B — `claude mcp add`** (variant A as a single command):
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
claude mcp add clipwright \
|
|
138
|
-
-e CLIPWRIGHT_API_KEY=cw_... \
|
|
139
|
-
-e CLIPWRIGHT_API_URL=https://api.clipwright.io \
|
|
140
|
-
-e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
|
|
141
|
-
-- node /absolute/path/to/clipwright/packages/mcp-server/dist/index.js
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
**Variant C — from npm, without the repository.** Same thing, but the command is
|
|
145
|
-
`npx`:
|
|
80
|
+
Generate `CLIPWRIGHT_CLIENT_ID` BEFORE the first run (section 2):
|
|
146
81
|
|
|
147
82
|
```json
|
|
148
83
|
{
|
|
@@ -153,113 +88,63 @@ claude mcp add clipwright \
|
|
|
153
88
|
"args": ["-y", "-p", "@clipwright/mcp-server@latest", "clipwright-mcp"],
|
|
154
89
|
"env": {
|
|
155
90
|
"CLIPWRIGHT_API_KEY": "cw_...",
|
|
156
|
-
"
|
|
157
|
-
"CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
|
|
91
|
+
"CLIPWRIGHT_CLIENT_ID": "<a unique value, section 2>"
|
|
158
92
|
}
|
|
159
93
|
}
|
|
160
94
|
}
|
|
161
95
|
}
|
|
162
96
|
```
|
|
163
97
|
|
|
164
|
-
As
|
|
98
|
+
As one command:
|
|
165
99
|
|
|
166
100
|
```bash
|
|
167
101
|
claude mcp add clipwright \
|
|
168
102
|
-e CLIPWRIGHT_API_KEY=cw_... \
|
|
169
|
-
-e CLIPWRIGHT_API_URL=https://api.clipwright.io \
|
|
170
103
|
-e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
|
|
171
104
|
-- npx -y -p @clipwright/mcp-server@latest clipwright-mcp
|
|
172
105
|
```
|
|
173
106
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
and a client out of sync with the server costs more than an unexpected upgrade.
|
|
177
|
-
Once the contract settles, the version should be pinned.
|
|
178
|
-
|
|
179
|
-
Environment variables:
|
|
107
|
+
For a source build, the command is `node` with the absolute path to
|
|
108
|
+
`packages/mcp-server/dist/index.js`.
|
|
180
109
|
|
|
181
110
|
| Variable | Required | Meaning |
|
|
182
111
|
|---|---|---|
|
|
183
|
-
| `CLIPWRIGHT_API_KEY` | YES |
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
112
|
+
| `CLIPWRIGHT_API_KEY` | YES | A `cw_*` key issued on your account at https://app.clipwright.io/api-keys. It is yours: a second person gets an account and a key of their own. Several keys on one account share its runs, balance and idempotency. |
|
|
113
|
+
| `CLIPWRIGHT_CLIENT_ID` | YES | This installation's own identifier, section 2. |
|
|
114
|
+
| `CLIPWRIGHT_API_URL` | no | Defaults to `https://api.clipwright.io`; set it only for another environment. |
|
|
186
115
|
|
|
187
|
-
After
|
|
188
|
-
returns
|
|
116
|
+
After a restart, confirm that `tools/list`
|
|
117
|
+
returns 45 tools: `make_ugc`, `get_run`, `get_run_shots`, `quote_revision`, `revise_run`, `quote_candidate`, `create_candidate`,
|
|
189
118
|
`list_generation_models`, `quote_image`, `generate_image`, `quote_video`, `generate_video`, `quote_ugc`, `list_actors`,
|
|
190
119
|
`list_voices`, `list_template_sounds`, `upload_image`, `create_upload`, `get_upload`, `create_asset`, `list_assets`, `get_asset`, `update_asset`,
|
|
191
|
-
`delete_asset`, `get_account`, `create_actor`, `quote_actor`, `delete_actor`, `get_actor_defaults`,
|
|
120
|
+
`delete_asset`, `quote_analysis`, `analyze_reference`, `get_reference_analysis`, `get_account`, `list_prices`, `create_actor`, `quote_actor`, `delete_actor`, `get_actor_defaults`,
|
|
192
121
|
`set_actor_defaults`, `quote_faceless`, `make_faceless`, `list_workflows`, `get_workflow`, `prepare_workflow_plan`,
|
|
193
|
-
`get_workflow_plan`, `approve_workflow_plan`, `quote_workflow_plan`, `run_workflow_plan`.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
`
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
| Client | How to add | Verified |
|
|
218
|
-
|---|---|---|
|
|
219
|
-
| Claude Code | `claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp` in a session to sign in | verified live 2026-10-18 |
|
|
220
|
-
| Claude.ai, Claude Desktop | Customize → Connectors → Add custom connector, paste the URL | not yet verified live |
|
|
221
|
-
| ChatGPT | the URL as the remote MCP server of a plugin (OpenAI's guide "Connect and test your plugin") | not yet verified live |
|
|
222
|
-
| Cursor | `~/.cursor/mcp.json`: `{"mcpServers": {"clipwright": {"url": "https://mcp.clipwright.io/mcp"}}}` | not yet verified live |
|
|
223
|
-
| VS Code | `.mcp.json` at the workspace root: `{"mcpServers": {"clipwright": {"type": "http", "url": "https://mcp.clipwright.io/mcp"}}}` | not yet verified live |
|
|
224
|
-
| Gemini CLI | `gemini mcp add --transport http clipwright https://mcp.clipwright.io/mcp`, then `/mcp auth clipwright` | not yet verified live |
|
|
225
|
-
| Codex | `codex mcp add clipwright --url https://mcp.clipwright.io/mcp`, then `codex mcp login clipwright` | not yet verified live |
|
|
226
|
-
|
|
227
|
-
**Permissions.** The approval page lists what the app asks for:
|
|
228
|
-
|
|
229
|
-
- `mcp:read` — the tools with `readOnlyHint: true`: account, runs, shots,
|
|
230
|
-
voices, actors, workflows and every quote. Free.
|
|
231
|
-
- `mcp:write` — every other tool: `make_ugc`, `make_faceless`, `revise_run`,
|
|
232
|
-
`create_candidate`, `generate_image`, `generate_video`, `create_actor` and
|
|
233
|
-
`run_workflow_plan` **spend credits**;
|
|
234
|
-
`upload_image`, `create_upload`, `create_asset`, `update_asset`, `delete_asset`, `delete_actor`,
|
|
235
|
-
`set_actor_defaults`, `prepare_workflow_plan` and `approve_workflow_plan` change the account.
|
|
236
|
-
|
|
237
|
-
The account holder approves or declines the whole request; only an admitted
|
|
238
|
-
account can approve. A connection without `mcp:write` that calls one of those
|
|
239
|
-
tools gets HTTP `403` with `error: "insufficient_scope"` and
|
|
240
|
-
`WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:write", resource_metadata="…"`.
|
|
241
|
-
Nothing is charged and no run is created. Reconnect the app and approve
|
|
242
|
-
spending to fix it.
|
|
243
|
-
|
|
244
|
-
**Revoking.** The account holder revokes an app at
|
|
245
|
-
`https://app.clipwright.io/api-keys`, under **Connected apps**. Its next request
|
|
246
|
-
gets `401`. A connection lasts 90 days from approval and is not extended by
|
|
247
|
-
use: after that the app must sign in again. An access token lasts one hour;
|
|
248
|
-
clients renew it themselves.
|
|
249
|
-
|
|
250
|
-
**Files.** The hosted server cannot read files on the agent's machine, so
|
|
251
|
-
`upload_image` refuses a `path` there. Pass a public https URL as
|
|
252
|
-
`upload_image({url})` and the server downloads it, or call `create_upload`,
|
|
253
|
-
PUT the file to its `put_url` with curl and confirm with `get_upload`. Either
|
|
254
|
-
way the `upload_id` works in `first_frame`, `last_frame` and `references`.
|
|
255
|
-
|
|
256
|
-
**Idempotency.** Section 2 does not apply here. Each connected app (one
|
|
257
|
-
grant) is its own client identity, and so is each `cw_*` key, so two
|
|
258
|
-
connections of one account never collapse into one run. A repeated request
|
|
259
|
-
within one connection returns the same run, as on stdio.
|
|
122
|
+
`plan_reference_remake`, `get_workflow_plan`, `approve_workflow_plan`, `quote_workflow_plan`, `run_workflow_plan`, `send_feedback`.
|
|
123
|
+
|
|
124
|
+
When no tool does the job, or an answer is confusing or wrong, file it with
|
|
125
|
+
`send_feedback` (free) instead of rephrasing the request and giving up.
|
|
126
|
+
|
|
127
|
+
### 1.4 What differs on the hosted server
|
|
128
|
+
|
|
129
|
+
- **Permissions.** `mcp:read` covers the tools marked `readOnlyHint: true`,
|
|
130
|
+
every quote included; `mcp:write` covers every other tool. A connection
|
|
131
|
+
without `mcp:write` that calls one gets HTTP `403` with
|
|
132
|
+
`error: "insufficient_scope"`; nothing is charged. Reconnect and approve spending.
|
|
133
|
+
- **Files.** The hosted server cannot read the agent's disk, so `upload_image`
|
|
134
|
+
refuses a `path` there. Pass a public https `url`, or call `create_upload`,
|
|
135
|
+
PUT the file to its `put_url` and confirm with `get_upload`.
|
|
136
|
+
- **Idempotency.** Each connected app and each `cw_*` key is its own client
|
|
137
|
+
identity, so section 2 does not apply. A repeated request within one
|
|
138
|
+
connection returns the same run.
|
|
139
|
+
- **Sign-in.** A connection lasts 90 days from approval and use does not extend
|
|
140
|
+
it; access tokens last an hour and clients renew them. The account holder
|
|
141
|
+
revokes an app at https://app.clipwright.io/api-keys, under Connected apps.
|
|
142
|
+
- **Endpoint errors.** No token, or a revoked or unknown one: `401` with a
|
|
143
|
+
`WWW-Authenticate` header the client follows to sign in. `GET /mcp`: `405`. A
|
|
144
|
+
body over 1 MB: `413`.
|
|
260
145
|
|
|
261
146
|
**An agent with no browser** signs in with a device code (verified live
|
|
262
|
-
2026-10-18). Only the
|
|
147
|
+
2026-10-18). Only the client id `clipwright-agent` may use this flow:
|
|
263
148
|
|
|
264
149
|
```bash
|
|
265
150
|
curl -s https://mcp.clipwright.io/device/auth -d client_id=clipwright-agent \
|
|
@@ -267,707 +152,305 @@ curl -s https://mcp.clipwright.io/device/auth -d client_id=clipwright-agent \
|
|
|
267
152
|
--data-urlencode "resource=https://mcp.clipwright.io/mcp"
|
|
268
153
|
```
|
|
269
154
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
approval the answer is `authorization_pending`; a faster poll gets
|
|
278
|
-
`slow_down`.
|
|
279
|
-
3. Send `access_token` as `Authorization: Bearer` to `/mcp`. Renew it with
|
|
280
|
-
`grant_type=refresh_token`, the refresh token and the same `client_id`. Each
|
|
281
|
-
renewal returns a new refresh token: keep the newest one.
|
|
282
|
-
|
|
283
|
-
**With a `cw_*` key instead of OAuth** — for CI and scripts. A key holds both
|
|
284
|
-
permissions:
|
|
285
|
-
|
|
286
|
-
```bash
|
|
287
|
-
claude mcp add --transport http clipwright https://mcp.clipwright.io/mcp --header "Authorization: Bearer cw_..."
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Errors of the endpoint itself:** no token, or a revoked or unknown key —
|
|
291
|
-
`401` with
|
|
292
|
-
`WWW-Authenticate: Bearer resource_metadata="https://mcp.clipwright.io/.well-known/oauth-protected-resource/mcp", scope="mcp:read mcp:write"`,
|
|
293
|
-
which a client follows to sign in; an expired or revoked OAuth token — the same
|
|
294
|
-
`401` with `error="invalid_token"` added; `GET /mcp` with a valid token — `405`;
|
|
295
|
-
a body over 1 MB — `413`.
|
|
155
|
+
Show the person `user_code` and `verification_uri`; they approve on any device
|
|
156
|
+
if the code matches. Meanwhile poll `POST https://mcp.clipwright.io/token` with
|
|
157
|
+
`grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code` and
|
|
158
|
+
`client_id=clipwright-agent`, no more often than every 5 seconds
|
|
159
|
+
(`authorization_pending` until approval, `slow_down` when too fast). Send
|
|
160
|
+
`access_token` as `Authorization: Bearer`. Renew with
|
|
161
|
+
`grant_type=refresh_token`; each renewal returns a new refresh token, keep the newest.
|
|
296
162
|
|
|
297
163
|
## 2. `CLIPWRIGHT_CLIENT_ID` — required, and WHY
|
|
298
164
|
|
|
299
|
-
This section is about the local server
|
|
300
|
-
|
|
165
|
+
This section is about the local server; the hosted server takes the identity
|
|
166
|
+
from the connection (section 1.4).
|
|
301
167
|
|
|
302
|
-
**Rule: EVERY installation
|
|
303
|
-
|
|
304
|
-
not leave it empty hoping the default will "work out somehow".
|
|
168
|
+
**Rule: EVERY installation gets its OWN unique `CLIPWRIGHT_CLIENT_ID` BEFORE the
|
|
169
|
+
first paid call.** Do not copy it from another machine and do not leave it to chance.
|
|
305
170
|
|
|
306
171
|
### Why this is not optional
|
|
307
172
|
|
|
308
|
-
The idempotency key the SDK stamps on every paid
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
mean a separate account: several live tokens on one account are normal (laptop,
|
|
328
|
-
CI, smoke script), and they all share one idempotency axis. The account is not
|
|
329
|
-
visible from the token — only the service side knows it, so "is this my own
|
|
330
|
-
account?" is confirmed by whoever issued the token.
|
|
331
|
-
- **`clientId` separates within one account.** On its own it comes from
|
|
332
|
-
`~/.clipwright/client-id` and is born random on first run, so two genuinely
|
|
333
|
-
separate installations diverge even without the environment variable. It can
|
|
334
|
-
only match if it was MADE to match: a shared `HOME`, a value copied from
|
|
335
|
-
someone else's machine, an image cloned together with the file.
|
|
336
|
-
|
|
337
|
-
The dangerous case is exactly the crossing of those two conditions: **one account
|
|
338
|
-
AND one `clientId`**. Then a request whose input matches an earlier one — for
|
|
339
|
-
example, both people copied a sample from this SKILL.md verbatim — returns 200, a
|
|
340
|
-
`run_id` and a playable link. But it is the video rendered for the first person,
|
|
341
|
-
presented to the second as their own success. A diagnostic tool would read as
|
|
342
|
-
"works" while no paid render ever ran for the second person.
|
|
343
|
-
|
|
344
|
-
Hence the rule: **a second person gets their own ACCOUNT and a token issued on
|
|
345
|
-
it**, and the value of `CLIPWRIGHT_CLIENT_ID` is not copied from anyone's machine.
|
|
346
|
-
Setting it explicitly is mandatory wherever the file resolver cannot give a stable
|
|
347
|
-
value of its own: a read-only or shared `HOME`, a container from a cloned image,
|
|
348
|
-
CI.
|
|
173
|
+
The idempotency key the SDK stamps on every paid call is derived from **the hash
|
|
174
|
+
of the input** and **the client identity**. The server deduplicates on that key:
|
|
175
|
+
a repeated key returns 200 with the run **created by the first request**, and no
|
|
176
|
+
new paid render starts. That is right when the same user retries.
|
|
177
|
+
|
|
178
|
+
Collapsing requires BOTH to match: the account and the `clientId`.
|
|
179
|
+
|
|
180
|
+
- **The account separates by construction.** The server matches a key only
|
|
181
|
+
within your account (its `account_id`). A separate token does not by itself
|
|
182
|
+
mean a separate account: several live tokens on one account share one
|
|
183
|
+
idempotency axis, and only the service knows which account a token is on.
|
|
184
|
+
- **`clientId` separates within one account.** Without the variable it comes
|
|
185
|
+
from `~/.clipwright/client-id`, born random on first run. It matches only when
|
|
186
|
+
made to match: a shared `HOME`, a copied value, an image cloned with the file.
|
|
187
|
+
|
|
188
|
+
With one account AND one `clientId`, a request whose input matches an earlier
|
|
189
|
+
one (both people copied the same sample) returns 200, a `run_id` and a playable
|
|
190
|
+
link to the video rendered for the FIRST person. Nothing errors, and no render
|
|
191
|
+
ran for the second.
|
|
349
192
|
|
|
350
193
|
### How to set it
|
|
351
194
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
generator use the same shape (a UUID without dashes). Generate it with Node, not
|
|
356
|
-
`uuidgen`: Node is certainly present here, whereas `uuidgen` may be missing on a
|
|
357
|
-
bare Linux image — and then the shell silently substitutes an EMPTY string, which
|
|
358
|
-
switches off precisely the protection the variable exists for.
|
|
195
|
+
Set it explicitly in the host environment (section 1.3). Generate it with Node,
|
|
196
|
+
not `uuidgen`: on a bare image without `uuidgen` the shell silently substitutes
|
|
197
|
+
an empty string and switches the protection off.
|
|
359
198
|
|
|
360
199
|
```bash
|
|
361
200
|
node -e "console.log(crypto.randomUUID().replaceAll('-',''))"
|
|
362
201
|
```
|
|
363
202
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
notice. When installing by this guide, **always set `CLIPWRIGHT_CLIENT_ID`
|
|
372
|
-
explicitly in the host environment** rather than relying on auto-generation.
|
|
373
|
-
|
|
374
|
-
If neither the variable nor writing the file is possible (read-only `$HOME`),
|
|
375
|
-
`resolveClientId()` deliberately THROWS instead of inventing a throwaway id on
|
|
376
|
-
every start — a silent fallback would disable deduplication entirely
|
|
377
|
-
(`packages/sdk/src/client-id.ts`).
|
|
378
|
-
|
|
379
|
-
**The server does NOT die in that case, and this is verified by a test run.** The
|
|
380
|
-
identity resolves lazily — only on the first `make_ugc`. In an environment where
|
|
381
|
-
`~/.clipwright` is not writable the server starts, `tools/list` returns every
|
|
382
|
-
tool, and `quote_ugc`, `get_run` and `list_voices` work: they do not need the
|
|
383
|
-
identity. Only `make_ugc` refuses, and its refusal names the variable that fixes
|
|
384
|
-
it. So the action to take is to set `CLIPWRIGHT_CLIENT_ID` explicitly — not to
|
|
385
|
-
hunt for a bug and not to reinstall the package.
|
|
386
|
-
|
|
387
|
-
## 3. CALL ORDER — `quote_ugc` first, `make_ugc` second
|
|
388
|
-
|
|
389
|
-
This is not a politeness convention but a mechanism, without which the contract
|
|
390
|
-
stays silent about its own decisions.
|
|
203
|
+
Without the variable and with a read-only `HOME`, the server still starts:
|
|
204
|
+
`tools/list`, `quote_ugc`, `get_run` and `list_voices` work; paid calls, uploads
|
|
205
|
+
and new assets refuse, naming the variable. Set it; do not reinstall.
|
|
206
|
+
|
|
207
|
+
# Part 2 — Make a video
|
|
208
|
+
|
|
209
|
+
## 3. CALL ORDER — quote first, paid call second
|
|
391
210
|
|
|
392
211
|
```
|
|
393
212
|
quote_ugc → read source / resolved_aspect_ratio / warnings → make_ugc → get_run (polling)
|
|
394
213
|
```
|
|
395
214
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
|
401
|
-
|
|
402
|
-
|
|
|
403
|
-
| `
|
|
404
|
-
|
|
|
405
|
-
|
|
|
406
|
-
|
|
|
407
|
-
|
|
|
408
|
-
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
`
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
| `character` | `actor_id` from `list_actors`, an `image`, or nothing (default actor); `person` does not select a face |
|
|
427
|
-
| `webhook_url` | poll `get_run` — webhooks are not delivered at all |
|
|
428
|
-
| `segments`, `broll_url` | a single `script`; segmented clips are not available yet |
|
|
429
|
-
| `disclosure_overlay` | nothing: no video carries a visible label, and the AI disclosure is in `ai_disclosure` of the finished run |
|
|
430
|
-
| `image` and `actor_gender` with the killswitch off | omit both — the render runs on the default actor |
|
|
431
|
-
|
|
432
|
-
**The aspect ratio can be refused.** Requesting `aspect_ratio: "1:1"` without an
|
|
433
|
-
`image` or `actor_id` returns a refusal: the default actor is vertical (432×768), and a square
|
|
434
|
-
frame could only come from the vendor's centre crop, which we do not do silently.
|
|
435
|
-
To get `1:1`, select an actor with a verified square variant or pass an `image`
|
|
436
|
-
with a square public https source. An unavailable actor format is refused before payment.
|
|
437
|
-
|
|
438
|
-
## 4. The tools
|
|
439
|
-
|
|
440
|
-
Schemas come from the single source of truth `@clipwright/core`
|
|
441
|
-
(`packages/core/src/skills.ts`, barrel `packages/core/src/index.ts`); MCP does not
|
|
442
|
-
retype them by hand.
|
|
215
|
+
Every paid tool has a free quote that reserves nothing. Show the user the
|
|
216
|
+
credits and start the paid call only after they agree. Take prices from the
|
|
217
|
+
quote or `list_prices`, never from memory or this page: they change.
|
|
218
|
+
|
|
219
|
+
| What | Free quote | Paid call | Poll with | The result |
|
|
220
|
+
|---|---|---|---|---|
|
|
221
|
+
| UGC video with a presenter | `quote_ugc` | `make_ugc` | `get_run` | `video_url` |
|
|
222
|
+
| Faceless video | `quote_faceless` | `make_faceless` | `get_run` | `video_url` |
|
|
223
|
+
| One image / one silent clip | `quote_image` / `quote_video` | `generate_image` / `generate_video` | `get_run` | `generation.url` |
|
|
224
|
+
| A personal actor | `quote_actor` | `create_actor` | `get_run` | `created_actor.actor_id` |
|
|
225
|
+
| A reading of a reference reel | `quote_analysis` | `analyze_reference` | `get_reference_analysis` | `reference_profile` or `data` |
|
|
226
|
+
| A revision of a faceless video | `quote_revision` | `revise_run` | `get_run` | `video_url` |
|
|
227
|
+
| A new picture or clip for one shot | `quote_candidate` | `create_candidate` | `get_run` | `candidate.preview_url` |
|
|
228
|
+
| A plan from a workflow method | `quote_workflow_plan` | `run_workflow_plan` | `get_run` | `workflow` |
|
|
229
|
+
|
|
230
|
+
- The paid call returns a `run_id` at once and does not wait. Poll every 5–10
|
|
231
|
+
seconds until `state` is `succeeded` or `failed`. Do not tell the user the
|
|
232
|
+
video is ready before you hold its link.
|
|
233
|
+
- Read `warnings[]` on the first answer, on each poll and on the terminal one.
|
|
234
|
+
What the service could not honor is announced there, not refused (section 4).
|
|
235
|
+
- A key the contract does not have (`aspectRatio`) is refused with 400
|
|
236
|
+
`unknown_field`, and the message names the right spelling. The removed fields
|
|
237
|
+
`character`, `webhook_url`, `broll_url` and `disclosure_overlay` are refused by
|
|
238
|
+
name with the replacement: `actor_id`, polling `get_run`, nothing, and the
|
|
239
|
+
`ai_disclosure` of the finished run. Nothing is charged.
|
|
240
|
+
- Before choosing a model for an image or a clip, call `list_generation_models`;
|
|
241
|
+
any other combination is `unsupported_combination` before any charge.
|
|
242
|
+
- A workflow launch needs the quote's revision, content digest, semantic input
|
|
243
|
+
digest and quote id, plus an explicit idempotency key, and the quote expires
|
|
244
|
+
five minutes after it is issued. Checked 2026-10-31.
|
|
443
245
|
|
|
444
246
|
### `quote_ugc` — free
|
|
445
247
|
|
|
446
|
-
|
|
447
|
-
is, the contract MINUS the rejected fields: they simply are not there, and listing
|
|
448
|
-
them here would mean offering what the server punishes you for (section 3).
|
|
449
|
-
|
|
450
|
-
`script` (required while `segments` is rejected; length is limited by the resolved
|
|
451
|
-
TTS model, as reported by `tools/list`: `eleven_v3`, `eleven_v4` and `gemini-3.8-flash-tts`
|
|
452
|
-
allow 5000 characters, `eleven_flash_v2_5` and `eleven_turbo_v2_5` allow 10000. Spaces, audio tags and stress
|
|
453
|
-
marks count; emoji may count as two characters. There is no word-count limit or fixed
|
|
454
|
-
duration implied by it), `tts_model` (omit it and the voice decides: the Russian presets
|
|
455
|
-
`owner_ru_clone` and `daria_ru_female` and the catalog voices named `ru_*` speak
|
|
456
|
-
`eleven_v4`, a voice of kind `model_voice` speaks its own model `gemini-3.8-flash-tts`,
|
|
457
|
-
every other preset and catalog voice speaks `eleven_v3`; a raw `voice_id`
|
|
458
|
-
speaks `eleven_v4` when at least 60% of the script's Cyrillic and Latin letters are
|
|
459
|
-
Cyrillic, and `eleven_v3` otherwise; an explicit `tts_model` always wins;
|
|
460
|
-
`eleven_flash_v2_5` and `eleven_turbo_v2_5` cost less and suit
|
|
461
|
-
languages other than Russian), optionally `person` OR
|
|
462
|
-
`image` (a public `https` URL) with `actor_gender` (`female`/`male`, only next to
|
|
463
|
-
`image`), plus `name`, `captions` (defaults to `false` —
|
|
464
|
-
**captions are opt-in, ASK THE USER FIRST**), `caption_style`
|
|
465
|
-
(`hormozi`/`tiktok`/`minimal`), `look` (`natural`/`commercial`/`raw_iphone`),
|
|
466
|
-
`aspect_ratio` (`9:16`/`1:1`, with NO default — silence and an explicit choice
|
|
467
|
-
differ), `resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
|
|
468
|
-
|
|
469
|
-
**Russian stress marks.** Tell a user who writes in Russian that they can fix stress:
|
|
470
|
-
write the stressed vowel as a capital inside a lowercase word — `потОм`, `зАмок` — and
|
|
471
|
-
Clipwright sends `eleven_v4`, `eleven_v3` and `gemini-3.8-flash-tts` the stress mark U+0301
|
|
472
|
-
(`пото́м`). A capital that starts a
|
|
473
|
-
word stays a capital, a word with a second capital or an inner capital consonant
|
|
474
|
-
(`ВУЗы`, all caps) stays as written, and a U+0301 typed directly is kept. A single
|
|
475
|
-
inner capital vowel always reads as stress, so `ЯндексЕда` needs a space. Keep Russian
|
|
476
|
-
scripts on one of those three models: the 2.5 models misread the mark, so a script
|
|
477
|
-
carrying it draws a warning there, and so does any Cyrillic script. With a preset, a
|
|
478
|
-
catalog voice or `voice_id` the choice is `eleven_v4` or `eleven_v3`, because
|
|
479
|
-
`gemini-3.8-flash-tts` speaks only its own voices. Retrying never
|
|
480
|
-
fixes stress; only the text does.
|
|
481
|
-
|
|
482
|
-
**Break tags on `eleven_v4` and `gemini-3.8-flash-tts`.** These two models give no pause
|
|
483
|
-
for `<break time="1.5s" />`.
|
|
484
|
-
Clipwright cuts every tag of that form (`time` in `s` or `ms`) from the text it sends to
|
|
485
|
-
them, leaves it out of the character count, and names the cut in `warnings[]` of
|
|
486
|
-
the quote and the run. A script that holds nothing but break tags is refused before any
|
|
487
|
-
charge. The other models get the tag as written.
|
|
488
|
-
|
|
489
|
-
The exact list is always available from the tool schema itself: `tools/list` is
|
|
490
|
-
generated from the same shape, and fields that are not honoured carry
|
|
491
|
-
`NOT HONORED YET` right in their own `description`.
|
|
492
|
-
|
|
493
|
-
Spends no credits. The response is the raw `quote` in full:
|
|
248
|
+
Its price fields for a 12-second script:
|
|
494
249
|
|
|
495
250
|
```json
|
|
496
251
|
{
|
|
497
252
|
"skill": "make_ugc",
|
|
498
|
-
"credits_estimate":
|
|
253
|
+
"credits_estimate": 120,
|
|
499
254
|
"duration_estimate_sec": 12,
|
|
255
|
+
"tariff": "parts-v2",
|
|
256
|
+
"price_lines": [
|
|
257
|
+
{ "label": "presenter, HeyGen Avatar IV", "unit": "face_second", "quantity": 12, "unit_credits": 9, "credits": 108 },
|
|
258
|
+
{ "label": "production (voice and render)", "unit": "second", "quantity": 12, "unit_credits": 1, "credits": 12 }
|
|
259
|
+
],
|
|
260
|
+
"minimum_credits": 50,
|
|
500
261
|
"warnings": []
|
|
501
262
|
}
|
|
502
263
|
```
|
|
503
264
|
|
|
504
|
-
`
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
###
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
### `get_run_shots` — free, read-only
|
|
654
|
-
|
|
655
|
-
Takes a `run_id` and returns the shots of that video as the API states them
|
|
656
|
-
(`GET /v1/runs/{id}/shots`). It changes nothing and starts nothing.
|
|
657
|
-
|
|
658
|
-
- `planned: true` — the answer holds `frame`, `timebase.fps`, `duration_frames`,
|
|
659
|
-
the narration `words` with their timing, and `tracks[]`. Each shot has an `id`,
|
|
660
|
-
`start_frame` and `end_frame`, a `kind`, the words it covers, a `description`
|
|
661
|
-
and a `preview_url`. Spans are whole frames at `timebase.fps`.
|
|
662
|
-
- `planned: false` — a normal answer, not an error. The run has no shots to show,
|
|
663
|
-
and `editable.state` with `editable.reason` says why: the run did not succeed,
|
|
664
|
-
its `line` is one the API reads no shots for, or it kept no record of its
|
|
665
|
-
media. Tell the user that reason; do not retry.
|
|
666
|
-
|
|
667
|
-
`preview_url` is a signed link to the shot's source picture or clip, which is a
|
|
668
|
-
large file. It expires: call the tool again for a fresh link and never store it.
|
|
669
|
-
A field named in `withheld` is null on every shot, so a null `description` or
|
|
670
|
-
`preview_url` there means "not shown", not "missing".
|
|
671
|
-
|
|
672
|
-
`editable.media_available_until` is how long the media of the run are stored.
|
|
673
|
-
`paid_edits.enabled` says whether a video can be made from an edit; while it is
|
|
674
|
-
`false`, none can, and `revise_run` answers `editing_disabled`. Read `kind`, `line`,
|
|
675
|
-
`editable.state` and each shot's `operations` as open strings, because a later
|
|
676
|
-
API may name values this text does not list.
|
|
677
|
-
|
|
678
|
-
### `quote_revision` — free; `revise_run` — paid
|
|
679
|
-
|
|
680
|
-
Both take a `run_id` and exactly one of `changes` (shots to generate anew over
|
|
681
|
-
the run as it is: each a `shot_id` and its `operation`, such as
|
|
682
|
-
`{ "mode": "retake" }`) or `document` (the whole edited document; retimes,
|
|
683
|
-
deletions and uploads go this way). `quote_revision` answers what the revision
|
|
684
|
-
would change (`summary`) and cost (`billing.total_credits`) and spends nothing.
|
|
685
|
-
Show the user that price before `revise_run`.
|
|
686
|
-
|
|
687
|
-
`revise_run` starts a NEW run and returns it at once; poll it with `get_run`.
|
|
688
|
-
Once it succeeded it is editable under its own `run_id`. A revision that
|
|
689
|
-
delivers is charged its quoted total; one that fails is charged nothing, even
|
|
690
|
-
when its paid work was already done. An identical request returns the same run
|
|
691
|
-
it started before, so a call repeated after a lost answer starts nothing new;
|
|
692
|
-
pass `attempt: 2` to start a second revision with the same request on purpose.
|
|
693
|
-
|
|
694
|
-
The refusals are terminal; do not retry them unchanged:
|
|
695
|
-
|
|
696
|
-
- `editing_disabled`, `editing_not_priced`, `editing_window_not_set` (503) —
|
|
697
|
-
paid edits are not open. Nothing was charged and no run was created.
|
|
698
|
-
- `edit_ceiling_reached` (409) — `ceiling: "in_flight"` means a revision of the
|
|
699
|
-
original is still running: follow `run_ids` with `get_run` and start the next
|
|
700
|
-
one after it finishes; if it is held for review, contact support.
|
|
701
|
-
`"failed_or_unfinished"` means support has to raise the limit.
|
|
702
|
-
- `edit_invalid` (422) — the document breaks a rule of the run; `issues` lists
|
|
703
|
-
each, and the rules are in the run's shots.
|
|
704
|
-
- `run_not_editable` (409) with `state: "edit_window_closed"` — the paid window
|
|
705
|
-
of the original has closed.
|
|
706
|
-
|
|
707
|
-
### `quote_candidate` — free; `create_candidate` — paid
|
|
708
|
-
|
|
709
|
-
A shot candidate is one picture or clip made for one shot of a `make_faceless`
|
|
710
|
-
run, to keep or discard in a later revision. Both tools take a `run_id`, a
|
|
711
|
-
`shot_id` and an `operation` the shot lists in `get_run_shots` (such as
|
|
712
|
-
`{ "mode": "retake" }`), and an optional `take`, 1 by default.
|
|
713
|
-
`quote_candidate` answers the `unit`, its `credits` and `usable_until`, the last
|
|
714
|
-
moment the candidate can enter a revision, and spends nothing. Show the user
|
|
715
|
-
that price before `create_candidate`.
|
|
716
|
-
|
|
717
|
-
`create_candidate` starts a NEW run of skill `edit_shot` and returns it at once;
|
|
718
|
-
poll it with `get_run`. Once it succeeded, `candidate.preview_url` shows what was
|
|
719
|
-
made. A delivered candidate is charged its one unit whether it is kept or
|
|
720
|
-
discarded; one that fails is charged nothing, even when its paid work was
|
|
721
|
-
already done. An identical request returns the same run it started before, so a
|
|
722
|
-
call repeated after a lost answer starts nothing new; pass `take: 2` to make
|
|
723
|
-
another candidate with the same request on purpose, charged again.
|
|
724
|
-
|
|
725
|
-
The refusals are those of `revise_run` above. The candidate limits of an
|
|
726
|
-
original, running at once and failed or unfinished, are counted apart from its
|
|
727
|
-
revisions, and support raises only the revision limit. An operation the shot
|
|
728
|
-
does not offer is `edit_invalid` with the code `action_not_allowed`.
|
|
729
|
-
|
|
730
|
-
### `quote_image`, `quote_video` — free; `generate_image`, `generate_video` — paid
|
|
731
|
-
|
|
732
|
-
One image or one silent clip from a prompt, each a run of its own.
|
|
733
|
-
`list_generation_models` lists the models, modes, aspect ratios, resolutions,
|
|
734
|
-
durations and `unit_credits`: credits per image, or per second of a clip. Any
|
|
735
|
-
other combination is `unsupported_combination` before any charge. The quotes
|
|
736
|
-
answer the credits, the width and height, and warnings; show the price first.
|
|
737
|
-
|
|
738
|
-
A reference is `{ "upload_id": ..., "rights_confirmed": true }` for a picture
|
|
739
|
-
from `upload_image` or `create_upload`, `{ "run_id": ... }` of a succeeded `generate_image` run
|
|
740
|
-
of this account until its `reusable_until`, or `{ "asset_id": ... }` of a picture asset from
|
|
741
|
-
`create_asset`, whose rights were confirmed when it was made. In `first_frame` mode the frame must
|
|
742
|
-
match the requested aspect ratio within 2% (`source_aspect_mismatch`).
|
|
743
|
-
|
|
744
|
-
The run tools return the run at once; poll it with `get_run`, and once it
|
|
745
|
-
succeeded `generation.url` is the file. A delivered generation is charged its
|
|
746
|
-
quoted credits; one that fails is charged nothing, even when the vendor was
|
|
747
|
-
already paid. An identical request returns the same run; pass `attempt: 2` for
|
|
748
|
-
another. While generations are closed the tools answer `generations_disabled`
|
|
749
|
-
or `generations_marking_pending` and charge nothing.
|
|
750
|
-
|
|
751
|
-
### `list_actors` — free, no input
|
|
752
|
-
|
|
753
|
-
Returns the permanent actor catalog from `/v1/actors`, including stable
|
|
754
|
-
`actor_id`, version, name, gender, approximate age and verified image formats.
|
|
755
|
-
Each variant has dimensions, `preview_url` and `preview_expires_at`. Preview
|
|
756
|
-
links expire; retain `actor_id` for reuse and call the catalog again for fresh previews.
|
|
757
|
-
|
|
758
|
-
Pass `actor_id` to both `quote_ugc` and `make_ugc`. It cannot be combined with
|
|
759
|
-
`image` or `person`. An unavailable actor or format returns an error before paid
|
|
760
|
-
work; it never substitutes the default face. Existing runs retain the selected
|
|
761
|
-
version and image hash even if the catalog changes later.
|
|
762
|
-
|
|
763
|
-
### `list_voices` — free, optional filters
|
|
764
|
-
|
|
765
|
-
Returns the voices for the `voice` field of `make_ugc`, or of `make_faceless`
|
|
766
|
-
when called with `skill: "make_faceless"`. Each voice names the one skill that
|
|
767
|
-
speaks it in `skill`, and the other skill refuses it before any charge. The
|
|
768
|
-
`make_ugc` voices are the presets, the thirty voices of `gemini-3.8-flash-tts`
|
|
769
|
-
and the catalog voices, by language and then by rank. The seventeen
|
|
770
|
-
`make_faceless` voices are a list of their own. Costs nothing and creates no
|
|
771
|
-
run.
|
|
772
|
-
|
|
773
|
-
All filters are optional:
|
|
774
|
-
|
|
775
|
-
- `skill` — `make_ugc` or `make_faceless`: the skill whose voices to list.
|
|
776
|
-
Without it the list holds the `make_ugc` voices only, so call it with
|
|
777
|
-
`skill: "make_faceless"` before a faceless video.
|
|
778
|
-
- `language` — the voice's native language. It is a filter, not a limit: any
|
|
779
|
-
voice speaks any supported language. A `model_voice` voice has no language of
|
|
780
|
-
its own and matches the languages measured on its model, `en` and `ru`; the
|
|
781
|
-
filter also picks the language of its sample.
|
|
782
|
-
- `gender` — `female` or `male`.
|
|
783
|
-
- `age` and `use_case` — labels as the entries print them, such as `young` or
|
|
784
|
-
`narrative_story`.
|
|
785
|
-
- `model` — keeps the voices that speech model speaks: the presets and catalog
|
|
786
|
-
voices whose language it supports, or the model's own voices.
|
|
787
|
-
`model: "gemini-3.8-flash-tts"` returns its thirty voices.
|
|
788
|
-
|
|
789
|
-
A voice without a label matches no value of that filter; `language` on a
|
|
790
|
-
`model_voice` voice is the one exception above. An unknown value is
|
|
791
|
-
refused with the allowed values, and an unknown filter name with
|
|
792
|
-
`unknown_field`; neither returns an empty list.
|
|
793
|
-
|
|
794
|
-
Each entry carries `name`, `kind` (`preset`, `model_voice`, `faceless_voice` or
|
|
795
|
-
`catalog`), `skill`, `language`, `description`, `model` (its default speech
|
|
796
|
-
model) and `supported_models`. A `model_voice` entry has no `language`, names one model in
|
|
797
|
-
both `model` and `supported_models` and carries `gender` only where it is known
|
|
798
|
-
(`kore` is female, `charon` is male). A `faceless_voice` entry carries its
|
|
799
|
-
`language` and `gender` and the model label `narrator-v1`, which is not a
|
|
800
|
-
`tts_model`: the `model` filter never returns it. A
|
|
801
|
-
catalog voice also carries `verified_models` and whichever of `locale`,
|
|
802
|
-
`accent`, `gender`, `age` and `use_case` the catalog has. The tool returns
|
|
803
|
-
`{ "voices": [...] }` only. REST `GET /v1/voices` also returns `models`, and the
|
|
804
|
-
script limit of each model is in `tools/list`.
|
|
805
|
-
|
|
806
|
-
There is still a network call: the tool hits OUR `/v1/voices` and needs a working
|
|
807
|
-
`CLIPWRIGHT_API_KEY`, like the other three. Nobody calls the vendor — the list
|
|
808
|
-
lives in `@clipwright/core` and the samples in our own bucket, so this surface
|
|
809
|
-
needs no vendor key.
|
|
810
|
-
|
|
811
|
-
Call it before `make_ugc` when the user asks for "a different voice": a voice
|
|
812
|
-
name is preferable to a raw `voice_id`, because a name survives a change of
|
|
813
|
-
vendor and an identifier does not.
|
|
814
|
-
|
|
815
|
-
**Let the user HEAR the voice before paying for it.** Every voice with a recorded
|
|
816
|
-
sample carries `preview_url`: a short clip spoken by that voice with its own
|
|
817
|
-
model, so it matches what the render will sound like. A catalog voice speaks its
|
|
818
|
-
sample in its native language. A `model_voice` voice has two samples: the English
|
|
819
|
-
one comes by default and the Russian one with `language: "ru"`. Descriptions convey neither
|
|
820
|
-
diction nor accent nor pace; the owner caught a voice-face mismatch by ear, on a
|
|
821
|
-
finished clip. Offer the sample whenever the user asks for a different voice.
|
|
822
|
-
|
|
823
|
-
The link is signed and expires at `preview_expires_at`, an hour out. For a fresh
|
|
824
|
-
one call `list_voices` again; never store it. A voice with no sample carries no
|
|
825
|
-
`preview_url` at all — the sample does not exist, rather than having been dropped
|
|
826
|
-
from the answer.
|
|
827
|
-
|
|
828
|
-
### `get_account` — free, no input
|
|
829
|
-
|
|
830
|
-
Returns the body of `GET /v1/account`: `balance_credits`, `debt_credits`,
|
|
831
|
-
`holds_credits` and the unexpired `grants`. It costs nothing and creates no run.
|
|
832
|
-
|
|
833
|
-
- `debt_credits` above zero blocks `make_ugc` with `debt_outstanding`, whatever
|
|
834
|
-
the balance, until the account buys credits.
|
|
835
|
-
- With no debt, `balance_credits` is what a new run can spend.
|
|
836
|
-
- `holds_credits` are reserved for runs in progress; they settle when those
|
|
837
|
-
runs finish.
|
|
838
|
-
|
|
839
|
-
`quote_ugc` already warns in `warnings[]` when `make_ugc` with the same input
|
|
840
|
-
would be refused for money or access, so a render rarely needs this call first.
|
|
841
|
-
Call it when the user asks how many credits are left, or to show the numbers
|
|
842
|
-
behind such a warning.
|
|
843
|
-
|
|
844
|
-
### `quote_faceless` — free, and `make_faceless` — paid, creates a run
|
|
845
|
-
|
|
846
|
-
A faceless video is narration over an opening animated clip and image scenes,
|
|
847
|
-
without an on-camera presenter. You select 30 to 90 seconds; the video ends with
|
|
848
|
-
its narration, runs at least 25 seconds and at most a quarter past the selection,
|
|
849
|
-
never beyond 90. Captions are on by default; pass
|
|
850
|
-
`captions: false` to turn them off.
|
|
851
|
-
|
|
852
|
-
Give the narration one of two ways, and name which with `input_mode`:
|
|
853
|
-
|
|
854
|
-
- `input_mode: "script"` with `script` — your exact text is read as written.
|
|
855
|
-
The video ends with the narration, runs at least 25 seconds and may exceed
|
|
856
|
-
`duration_seconds` by up to a quarter, never beyond 90 seconds; a video more
|
|
857
|
-
than 5 seconds longer says so in `warnings[]`. A script is accepted when its
|
|
858
|
-
estimated length is 21 seconds to `duration_seconds` plus 5, at most 90. The charge follows the delivered duration and never
|
|
859
|
-
exceeds the quote; the minimum is 200 base credits plus 150 for the opener.
|
|
860
|
-
- `input_mode: "brief"` with `brief` — a short description; the narration is
|
|
861
|
-
written from it.
|
|
862
|
-
|
|
863
|
-
Send exactly one of `script` and `brief`, the one that matches `input_mode`. The
|
|
864
|
-
tool schema lists both as optional because MCP cannot express "one or the
|
|
865
|
-
other"; the server and this client both refuse a mismatch, and the client
|
|
866
|
-
refuses it before any request is sent.
|
|
867
|
-
|
|
868
|
-
`duration_seconds` (30 to 90, on a 1/25 s frame boundary) is required. Optional:
|
|
869
|
-
`style_reference` (a public https image the scenes follow in style),
|
|
870
|
-
`character_reference` (a public https image of a person or figure to keep
|
|
871
|
-
consistent) and `scene_images` (your own images, each anchored to a word range
|
|
872
|
-
or a quote of the narration). An uploaded image must belong to this account.
|
|
873
|
-
|
|
874
|
-
`voice` picks the narration voice: a `name` from `list_voices` with
|
|
875
|
-
`skill: "make_faceless"`. There are seventeen, in English, Russian, Spanish,
|
|
876
|
-
Portuguese, French and Italian; each entry has a `language` and a `gender`.
|
|
877
|
-
Without `voice` the narration is read by `narrator_en_wise_lady`, an English
|
|
878
|
-
voice, whatever the language of the text, so pick a voice in the language of
|
|
879
|
-
the narration. A voice reads any language with its own accent; when the
|
|
880
|
-
language of a voice you named is written in another alphabet than the script,
|
|
881
|
-
the quote and the run say so in `warnings[]`: the voice may mispronounce the
|
|
882
|
-
text, and the video may come out longer than its length limit and fail. A
|
|
883
|
-
brief draws that line, worded as a condition, only when it is written in the
|
|
884
|
-
other alphabet and holds no letter of the voice's: the narration is written in the language of the brief
|
|
885
|
-
unless the brief asks for another, so name the narration's language in the
|
|
886
|
-
brief when it differs from the brief's own. An unknown name and a `make_ugc`
|
|
887
|
-
voice such as `george` are refused before any charge. `voice_id` is not
|
|
888
|
-
accepted: `make_faceless` speaks only the voices listed for it. Both faceless
|
|
889
|
-
tools refuse `voice_id` and any other key they do not declare before any
|
|
890
|
-
request is sent. These voices have no `preview_url` yet.
|
|
891
|
-
|
|
892
|
-
Call `quote_faceless` first and show the user the price: it spends nothing and
|
|
893
|
-
warns when `make_faceless` would be refused for money or access. `make_faceless`
|
|
894
|
-
returns a `run_id` at once; poll `get_run` until `succeeded` (with `video_url`)
|
|
895
|
-
or `failed`. As with `make_ugc`, a repeated call with the same input returns the
|
|
896
|
-
same run, and `attempt` 2, 3, … deliberately starts a new paid one. `attempt`
|
|
897
|
-
belongs to `make_faceless`: a quote takes none, and `quote_faceless` refuses
|
|
898
|
-
it. When faceless generation is switched off on the server, both tools answer
|
|
899
|
-
`paid_render_disabled` and nothing is charged.
|
|
900
|
-
|
|
901
|
-
A `succeeded` faceless run can carry a remark from our own delivery check in
|
|
902
|
-
`warnings[]`: that the check could not confirm the captions or a scene change at
|
|
903
|
-
a given second, or that it did not run. The video is delivered and charged as
|
|
904
|
-
any other. Read the warning to the user and let them watch the video before
|
|
905
|
-
they publish it.
|
|
265
|
+
Also read `source`, `actor`, `resolved_aspect_ratio` (what will ACTUALLY be
|
|
266
|
+
rendered) and `tts_model`. The quote and the run share one verdict: a 400 on the
|
|
267
|
+
quote is the same 400 on the run, and a money or access refusal the run would
|
|
268
|
+
meet is named in the quote's `warnings[]`. The free curl form is in section 7.
|
|
269
|
+
|
|
270
|
+
## 4. Traps: a wrong answer without an error
|
|
271
|
+
|
|
272
|
+
### Silent: the answer looks right and is not
|
|
273
|
+
|
|
274
|
+
**An omitted `aspect_ratio` follows the source, not 9:16.** With `image`, the
|
|
275
|
+
video takes the supported format nearest the photo: a 1600×900 photo gives a
|
|
276
|
+
16:9 video, and the format draws no warning. Only the default actor is always
|
|
277
|
+
9:16. Pass `aspect_ratio` whenever you pass `image`. Checked live 2026-10-31.
|
|
278
|
+
|
|
279
|
+
**A format up to 15% away from the source is snapped, not refused.** The frame
|
|
280
|
+
is padded with white bars or center-cropped at the sides, and `warnings[]` says
|
|
281
|
+
which; within 2% there is no warning. Over 15% on an explicit `aspect_ratio` is
|
|
282
|
+
400 `aspect_conflict` before any charge, so `1:1` on the default actor is refused.
|
|
283
|
+
A photo that could not be measured renders as requested, or 9:16, with a warning
|
|
284
|
+
that the format was not verified. Checked 2026-10-31.
|
|
285
|
+
|
|
286
|
+
**`person`, `name` and `look` change nothing.** They are accepted and quoted; the
|
|
287
|
+
default actor renders, and only `warnings[]` says so. The `character` refusal
|
|
288
|
+
suggests `person`, which still chooses no face. Choose one with `actor_id` from
|
|
289
|
+
`list_actors`, an `image`, or a new actor from `create_actor`. Checked live 2026-10-31.
|
|
290
|
+
|
|
291
|
+
**Captions are off in `make_ugc` and on in `make_faceless`.** `make_ugc` draws
|
|
292
|
+
word-timed captions only with `captions: true`; `make_faceless` draws them unless
|
|
293
|
+
`captions: false`. `make_faceless` takes only `captions`; the style, emphasis and
|
|
294
|
+
mode fields below are `make_ugc` fields, and `make_faceless` refuses them. Whether a
|
|
295
|
+
video has captions is the user's call:
|
|
296
|
+
|
|
297
|
+
1. **Ask whether they want captions** before the quote. Never turn them on unprompted.
|
|
298
|
+
2. **If yes, pick a style with them.** From a reference video take its look:
|
|
299
|
+
bold uppercase cues with one coloured keyword is `hormozi`, one or two words
|
|
300
|
+
popping in colour is `tiktok`, a sentence on a dark box is `youtube` or
|
|
301
|
+
`lecture`. Otherwise offer three or four styles that fit the video, one line
|
|
302
|
+
each, and link https://clipwright.io/captions. Without a choice the server draws `hormozi`.
|
|
303
|
+
3. **Mark key words with `caption_emphasis`**: quotes from `script` drawn larger
|
|
304
|
+
or in the accent colour; `caption_mode: "highlights"` captions only around them.
|
|
305
|
+
|
|
306
|
+
The styles: `hormozi` (the default), `accent`, `tiktok`, `youtube`, `instagram`,
|
|
307
|
+
`minimalist`, `neon`, `genz`, `campfire`, `midnight`, `plottwist`, `headline`,
|
|
308
|
+
`promo`, `lecture`, `highlighter`, `studio`, `broadcast`, `arcade`, `clutch`,
|
|
309
|
+
`bubblegum`, `crayon`, `lyrics`, `bassdrop`, `punch`; `minimal` is an older name
|
|
310
|
+
for `minimalist`. Quote each style's description from `caption_style` in
|
|
311
|
+
`tools/list`, not from memory. Captions are refused with `gemini-3.8-flash-tts`,
|
|
312
|
+
which returns no word timings. Checked 2026-10-31.
|
|
313
|
+
|
|
314
|
+
**`make_faceless` without `voice` narrates in English.** The default
|
|
315
|
+
`narrator_en_wise_lady` reads a Russian or Spanish script with an English accent,
|
|
316
|
+
and `warnings[]` stays empty: the language warning fires only for a voice you
|
|
317
|
+
named. Pick a voice in the narration's language from `list_voices` with
|
|
318
|
+
`skill: "make_faceless"`. Without `skill`, `list_voices` lists only `make_ugc`
|
|
319
|
+
voices, and `make_faceless` refuses those. A brief is narrated in its own
|
|
320
|
+
language unless it asks for another. Checked live 2026-10-31.
|
|
321
|
+
|
|
322
|
+
**A photo's gender is not detected.** With `image` and neither `actor_gender`
|
|
323
|
+
nor `voice`, the male voice `george` speaks and `warnings[]` says so. Pass
|
|
324
|
+
`actor_gender` or a `voice`. With `actor_id` the voice follows the actor's gender.
|
|
325
|
+
Checked 2026-10-31.
|
|
326
|
+
|
|
327
|
+
**`voice_pace` does nothing on `eleven_v4`, the default model of presets and
|
|
328
|
+
catalog voices.** The voice speaks at its own pace, and the run's `warnings[]`
|
|
329
|
+
says so; the quote does not. To change pace, pick a voice by `words_per_second`
|
|
330
|
+
in `list_voices`. Checked 2026-10-31.
|
|
331
|
+
|
|
332
|
+
**`eleven_v3` is spoken as `eleven_v4`, and break tags give no pause** on
|
|
333
|
+
`eleven_v4` and `gemini-3.8-flash-tts`: `<break time="1.5s" />` is cut from the
|
|
334
|
+
text. `warnings[]` names both. A script of nothing but break tags is refused.
|
|
335
|
+
Checked 2026-10-31.
|
|
336
|
+
|
|
337
|
+
**Russian stress is written, not retried.** On `eleven_v4` and
|
|
338
|
+
`gemini-3.8-flash-tts` a capital vowel inside a lowercase word marks stress:
|
|
339
|
+
`потОм`. A capital that starts a word, an all-caps word, or a word with a second
|
|
340
|
+
capital stays as written. The 2.5 models misread the mark and warn on Cyrillic.
|
|
341
|
+
A retry never fixes stress; only the text does. Checked 2026-10-31.
|
|
342
|
+
|
|
343
|
+
**`make_faceless` `duration_seconds` is a target, not a length.** The video ends
|
|
344
|
+
with its narration and runs at least 25 seconds. It usually ends within a
|
|
345
|
+
quarter past the selection and 90 seconds. A longer narration runs to its end,
|
|
346
|
+
and the charge never exceeds the quote. A video more than 5 seconds past the
|
|
347
|
+
selection names both lengths in `warnings[]`. A
|
|
348
|
+
script whose estimated length is under 21 seconds or over the selection plus 5
|
|
349
|
+
is refused before any charge; the message names the window, the code is the
|
|
350
|
+
broader `unusable_source`. Checked live 2026-10-31.
|
|
351
|
+
|
|
352
|
+
**A `make_ugc` quote is an estimate; a `make_faceless` quote is a ceiling.**
|
|
353
|
+
`make_ugc` charges the measured duration rounded up to whole seconds, which can
|
|
354
|
+
exceed `credits_estimate`. `make_faceless` charges the delivered length and never
|
|
355
|
+
more than its quote. Read `credits_charged` on the finished run: it is `null`
|
|
356
|
+
until the run settles, while `credits_reserved` holds the reserve. Checked 2026-10-31.
|
|
357
|
+
|
|
358
|
+
**The price field has a different name in each quote.** `credits_estimate` in
|
|
359
|
+
`quote_ugc`, `quote_actor` and `quote_analysis`; `credits` in `quote_image`,
|
|
360
|
+
`quote_video` and `quote_candidate`; `totalCredits` in `quote_faceless`;
|
|
361
|
+
`billing.total_credits` in `quote_revision`. A missing field is a wrong name, not
|
|
362
|
+
a free call. Checked live 2026-10-31.
|
|
363
|
+
|
|
364
|
+
**The same input after a failure returns the same failed run.** A repeated paid
|
|
365
|
+
call with the same input and the same `attempt` answers 200 with the earlier run,
|
|
366
|
+
terminal failure included, and starts nothing. It is not a permanent refusal: to
|
|
367
|
+
try again, raise `attempt` (section 5). `create_candidate` takes `take` instead.
|
|
368
|
+
Checked 2026-10-31.
|
|
369
|
+
|
|
370
|
+
**`failed` is not always final.** Rarely, a run that ended `failed` is reopened
|
|
371
|
+
when its video arrives late: `get_run` shows `queued` with a warning, then
|
|
372
|
+
`succeeded` with a link and a charge for the delivered video, and `warnings[]`
|
|
373
|
+
names the recovery. A retry started meanwhile is a second video and a second
|
|
374
|
+
charge. Before reporting a failure as final, read the run once more. Checked 2026-10-31.
|
|
375
|
+
|
|
376
|
+
**A new Idempotency-Key on the same input is a second paid run.** Over REST the
|
|
377
|
+
key is the only thing that collapses a repeat; the server does not compare inputs
|
|
378
|
+
across keys. After an uncertain answer (a timeout, `429`, `5xx`), resend with THE
|
|
379
|
+
SAME key. The same key with a different body is 409 `idempotency_key_reused`.
|
|
380
|
+
Checked 2026-10-31.
|
|
381
|
+
|
|
382
|
+
**A `succeeded` faceless run can carry a remark from the delivery check** in
|
|
383
|
+
`warnings[]`: captions or a scene change it could not confirm at a given second,
|
|
384
|
+
or that the check did not run. The video is delivered and charged. Read the
|
|
385
|
+
remark to the user and let them watch before they publish. Checked 2026-10-31.
|
|
386
|
+
|
|
387
|
+
### Errors whose shape hides the cause
|
|
388
|
+
|
|
389
|
+
**`curl -I` on `video_url` answers 403.** The link is signed for GET only. Check
|
|
390
|
+
it with a ranged GET (`curl -r 0-0`). Each `get_run` signs a fresh link valid for
|
|
391
|
+
seven days; never store it. Preview links of voices, actors and shots expire too:
|
|
392
|
+
call the tool again. Checked 2026-10-31.
|
|
393
|
+
|
|
394
|
+
**A raw `voice_id` the account does not have passes the quote and fails the run.**
|
|
395
|
+
It is checked at the start of the run, before any paid call, so the failure costs
|
|
396
|
+
nothing; a `voice_id` the account has is spoken. A `voice`
|
|
397
|
+
name from `list_voices` is checked at the quote. Checked 2026-10-31.
|
|
398
|
+
|
|
399
|
+
**`planned: false` from `get_run_shots` is an answer.** `editable.state` and
|
|
400
|
+
`editable.reason` say why the run has no shots to show; tell the user, do not
|
|
401
|
+
retry. A field named in `withheld` is null on every shot: "not shown", not
|
|
402
|
+
"missing". Checked 2026-10-31.
|
|
403
|
+
|
|
404
|
+
**A reference reel is polled with `get_reference_analysis`, not `get_run`.** Pass
|
|
405
|
+
an Instagram or TikTok link as `source_url` (no `create_asset` first); a YouTube
|
|
406
|
+
link is refused, so ask for the MP4. The same reel by its full link, in the same
|
|
407
|
+
mode, answers its earlier analysis for free; a short `vm.tiktok.com` link always
|
|
408
|
+
starts a new, paid one. `mode: "data"` reads many reels cheaply, without a
|
|
409
|
+
`reference_profile`. Checked 2026-10-31.
|
|
410
|
+
|
|
411
|
+
**An unknown terminal state reads as in progress.** The MCP formatter reports a
|
|
412
|
+
state it does not know as `IN_PROGRESS`, so polling would not end. Update the
|
|
413
|
+
client: MCP and the API ship as one version. Checked 2026-10-31.
|
|
906
414
|
|
|
907
415
|
## 5. Retrying a failed run is NOT a permanent refusal
|
|
908
416
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
a sign that the video is forever out of reach.
|
|
914
|
-
|
|
915
|
-
**To actually retry, pass `attempt` one higher than before** (the first call is
|
|
916
|
-
`attempt=1` by default; the second attempt is `attempt: 2`, the third `attempt: 3`,
|
|
917
|
-
and so on). This is the behaviour documented verbatim in the `make_ugc` tool
|
|
918
|
-
description:
|
|
417
|
+
A repeated call with the SAME input and WITHOUT a new `attempt` returns THAT SAME
|
|
418
|
+
failed run (section 4). **To actually retry, pass `attempt` one higher than
|
|
419
|
+
before** (the first call is `attempt: 1`). The `make_ugc` tool description says
|
|
420
|
+
the same:
|
|
919
421
|
|
|
920
422
|
> "Pass attempt=2,3,… to deliberately start a NEW run for the same input
|
|
921
423
|
> (retry after a failure)."
|
|
922
424
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
gives a different key, so the server sees a NEW request, which means a new
|
|
926
|
-
`run_id` and a new paid vendor call.
|
|
927
|
-
|
|
928
|
-
Do not confuse this with `CLIPWRIGHT_CLIENT_ID`: `attempt` is the agent's
|
|
929
|
-
deliberate "this is a new attempt", while `clientId` is the fixed identity of the
|
|
930
|
-
installation and must NOT change between attempts.
|
|
425
|
+
`attempt` is the agent's "this is a new try"; `CLIPWRIGHT_CLIENT_ID` is the
|
|
426
|
+
installation's fixed identity and must NOT change between attempts.
|
|
931
427
|
|
|
932
428
|
### ⚠️ EVERY `attempt` BUMP COSTS MONEY
|
|
933
429
|
|
|
934
|
-
A new `attempt` is a
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
a dropped connection);
|
|
952
|
-
- **no daily cap stands between you and your credits (0.7.0).** Not on your
|
|
953
|
-
account, and not on the service either: `global_cap_exceeded` is no longer
|
|
954
|
-
produced. What still stops a run before any paid call is your own balance, an
|
|
955
|
-
unpaid debt, and our killswitch.
|
|
956
|
-
|
|
957
|
-
## 5.1. Server refusals are read from fields, not guessed (0.2.0)
|
|
958
|
-
|
|
959
|
-
Since 0.2.0 an API refusal carries structured fields instead of one free-form
|
|
960
|
-
sentence. It still arrives the way every tool result does — as JSON **inside the
|
|
961
|
-
text block**, with `isError: true`:
|
|
430
|
+
A new `attempt` is a **new paid run**, not a free resend. The loop "failed → bump
|
|
431
|
+
→ failed → bump" burns credits on every iteration.
|
|
432
|
+
|
|
433
|
+
- **Read the `error` of the failed run first.** `rejected_field`,
|
|
434
|
+
`aspect_conflict` and `script_encoding_lost` come from the input, and the next
|
|
435
|
+
attempt fails the same way for money. For `script_encoding_lost`, re-send the
|
|
436
|
+
script decoded as UTF-8.
|
|
437
|
+
- **`speech_too_short_for_script` is almost never fixed by a retry**: change the
|
|
438
|
+
text or the voice.
|
|
439
|
+
- **Bump `attempt` only for a failure that looks temporary** (a timeout, `5xx`, a
|
|
440
|
+
dropped connection).
|
|
441
|
+
- **No daily cap stands between you and the credits.** What stops a run before
|
|
442
|
+
any paid call is the balance, an unpaid debt, or a pause on our side.
|
|
443
|
+
|
|
444
|
+
## 6. Server refusals are read from fields, not guessed
|
|
445
|
+
|
|
446
|
+
A refusal arrives as JSON **inside the text block**, with `isError: true`:
|
|
962
447
|
|
|
963
448
|
```ts
|
|
964
449
|
{ content: [{ type: "text", text: "{\"status\":\"FAILED\", …}" }], isError: true }
|
|
965
450
|
```
|
|
966
451
|
|
|
967
|
-
So `JSON.parse(content[0].text)` first
|
|
968
|
-
`
|
|
969
|
-
text, not beside it. Do not relay the raw text to the user and do not guess from
|
|
970
|
-
it.
|
|
452
|
+
So `JSON.parse(content[0].text)` first; `content[0].balance_credits` is
|
|
453
|
+
`undefined`. Do not relay the raw text and do not guess from it.
|
|
971
454
|
|
|
972
455
|
```json
|
|
973
456
|
{
|
|
@@ -983,165 +466,92 @@ it.
|
|
|
983
466
|
}
|
|
984
467
|
```
|
|
985
468
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
| `limit`, `window_seconds` | only when the 429 body followed the Clipwright contract; a 429 whose body did not carries neither |
|
|
992
|
-
| `balance_credits`, `required_credits` | `insufficient_credits` only |
|
|
993
|
-
| `debt_credits` | `debt_outstanding` only |
|
|
994
|
-
|
|
995
|
-
Treat every one of them as optional and read what is there. In particular, do not
|
|
996
|
-
expect balance numbers on `debt_outstanding`: that refusal reports the debt, and
|
|
997
|
-
the balance is not what blocks the run.
|
|
469
|
+
The numbers appear only where the refusal has them: `retry_after_seconds` on
|
|
470
|
+
`rate_limited` and on a `5xx` that named a pause; `limit` and `window_seconds`
|
|
471
|
+
only when the 429 body followed the Clipwright contract; `balance_credits` and
|
|
472
|
+
`required_credits` on `insufficient_credits` only; `debt_credits` on
|
|
473
|
+
`debt_outstanding` only. Treat each as optional.
|
|
998
474
|
|
|
999
475
|
**One rule: look at `retryable`.**
|
|
1000
476
|
|
|
1001
|
-
- `retryable: true` (`rate_limited`, 5xx) — wait `retry_after_seconds` and
|
|
1002
|
-
**THE SAME call with the same arguments**. Do not bump `attempt`: this
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
account has no beta access. Buying credits will not lift it — access is granted
|
|
1013
|
-
by the operator. Say that, and do not offer to top up.
|
|
1014
|
-
|
|
1015
|
-
Do NOT promise a position in line or an upcoming batch. The refusal says one
|
|
1016
|
-
thing only: this account has no access right now. An account whose access was
|
|
1017
|
-
REVOKED gets the same 403, and promising it a turn would be a lie at the worst
|
|
1018
|
-
possible moment.
|
|
1019
|
-
|
|
1020
|
-
The two money codes are cured the same way — buy credits — but they must not be
|
|
1021
|
-
conflated, because they describe different situations. `insufficient_credits`
|
|
1022
|
-
means the balance is too small for this run. `debt_outstanding` means the account
|
|
1023
|
-
owes credits, and it can owe them while still holding a balance, so telling the
|
|
1024
|
-
human "you do not have enough credits" would be false. Credits bought while a
|
|
1025
|
-
debt stands clear the debt first and only then reach the balance. Clearing the
|
|
1026
|
-
debt lifts this particular refusal and nothing else: the run still needs enough
|
|
477
|
+
- `retryable: true` (`rate_limited`, `5xx`) — wait `retry_after_seconds` and
|
|
478
|
+
repeat **THE SAME call with the same arguments**. Do not bump `attempt`: this
|
|
479
|
+
was a refused request, not a failed run.
|
|
480
|
+
- `retryable: false` — **stop and tell the human**. For money refusals quote the
|
|
481
|
+
NUMBERS: "the account has 3 credits, this needs 8".
|
|
482
|
+
|
|
483
|
+
The two money codes are cured the same way — buy credits — but they describe
|
|
484
|
+
different situations. `insufficient_credits` means the balance is too small for
|
|
485
|
+
this run. `debt_outstanding` means the account owes credits, possibly while still
|
|
486
|
+
holding a balance, so "you do not have enough credits" would be false. Credits
|
|
487
|
+
bought while a debt stands clear the debt first; the run still needs enough
|
|
1027
488
|
credits of its own, so a purchase equal to the debt clears it and adds nothing to
|
|
1028
|
-
the balance. Quote the debt figure
|
|
1029
|
-
|
|
1030
|
-
human has to buy more than the debt depends on what the balance already holds,
|
|
1031
|
-
and this refusal does not say — the balance is not what blocks the run.
|
|
1032
|
-
|
|
1033
|
-
Do not read a source into the refusal. A `429` or a `5xx` can come from the
|
|
1034
|
-
Clipwright API itself or from anything between you and it, and the fields above
|
|
1035
|
-
cannot tell you which — so the report does not claim one, and neither should you
|
|
1036
|
-
when relaying it. What is certain is the code and what to do about it.
|
|
489
|
+
the balance. Quote the debt figure and say that a purchase adds to the balance
|
|
490
|
+
only the part that exceeds the debt; this refusal does not report the balance.
|
|
1037
491
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
**Nobody waits on your behalf here — you do the polling.** `get_run` makes one
|
|
1043
|
-
call and hands you whatever came back, refusals included. So a `429` on a status
|
|
1044
|
-
check is yours to sit out: wait `retry_after_seconds`, then call `get_run` again
|
|
1045
|
-
with the same `run_id`. Do not treat it as a failed run and do not start a new
|
|
1046
|
-
one — the run is already running and already paid for.
|
|
1047
|
-
|
|
1048
|
-
(The SDK's own `makeUgc` does wait these out internally, which is why the CLI and
|
|
1049
|
-
direct SDK users never see them. The MCP tools are deliberately thin: `make_ugc`
|
|
1050
|
-
returns as soon as the run starts, so a long render never blocks your turn.)
|
|
492
|
+
`account_not_admitted` (403) is not a money refusal and carries no numbers: the
|
|
493
|
+
account has no access. Buying credits does not lift it; contact support. Promise
|
|
494
|
+
no date and no position: an account whose access was REVOKED gets the same 403.
|
|
1051
495
|
|
|
1052
|
-
|
|
496
|
+
A `429` or `5xx` can come from the Clipwright API or from anything between you
|
|
497
|
+
and it; do not claim a source. A `429` does **not** mean no run was created: a
|
|
498
|
+
repeat with an already-used key can get one too, so retry with THE SAME key. On a
|
|
499
|
+
status check, a `429` is yours to sit out: wait, then call `get_run` again with
|
|
500
|
+
the same `run_id`. The run is already going and already paid for.
|
|
1053
501
|
|
|
1054
|
-
|
|
1055
|
-
directly with curl, bypassing MCP, SDK and CLI entirely. If curl gets through, the
|
|
1056
|
-
packaging layer is at fault; if it does not, production or the network is.
|
|
502
|
+
## 7. On failure — plain curl splits the diagnosis
|
|
1057
503
|
|
|
1058
|
-
|
|
1059
|
-
|
|
504
|
+
If the MCP path does not reach a playable video, call the same REST API directly.
|
|
505
|
+
If curl gets through, the MCP setup is at fault (section 1); if not, the key,
|
|
506
|
+
the network or the service is.
|
|
1060
507
|
|
|
1061
508
|
```bash
|
|
1062
|
-
TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY
|
|
509
|
+
TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY
|
|
1063
510
|
API=https://api.clipwright.io
|
|
1064
511
|
|
|
1065
|
-
#
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
#
|
|
1071
|
-
#
|
|
512
|
+
# 0. Free: the quote. Verified 2026-10-31 (200, 50 credits, resolved 9:16).
|
|
513
|
+
curl -sS -X POST "$API/v1/skills/make_ugc/quote" \
|
|
514
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
515
|
+
-d '{"script":"Hello! This is a test line."}'
|
|
516
|
+
|
|
517
|
+
# 1. Paid: start a run. The idempotency key is required and must be UNIQUE per
|
|
518
|
+
# call: a repeated key returns the earlier run. Generate it with Node, not
|
|
519
|
+
# uuidgen, which may be missing and leave the key empty and constant.
|
|
1072
520
|
curl -sS -X POST "$API/v1/skills/make_ugc/run" \
|
|
1073
521
|
-H "Authorization: Bearer $TOKEN" \
|
|
1074
522
|
-H "Content-Type: application/json" \
|
|
1075
523
|
-H "Idempotency-Key: cw-$(node -e 'process.stdout.write(crypto.randomUUID())')" \
|
|
1076
524
|
-d '{"script":"Hello! This is a test line."}'
|
|
1077
525
|
|
|
1078
|
-
# 2.
|
|
526
|
+
# 2. Free: poll until terminal, every ~5–10 s.
|
|
1079
527
|
curl -sS "$API/v1/runs/<run_id>" -H "Authorization: Bearer $TOKEN"
|
|
1080
528
|
```
|
|
1081
529
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
environment — section 1), not in the REST API and not in access or the token.
|
|
1087
|
-
- **curl fails too:** the problem is not MCP. Check `$TOKEN` from the commands
|
|
1088
|
-
above (shaped `cw_*`, not truncated when copied), network access to `$API`, and —
|
|
1089
|
-
for the cloned-run story — that `CLIPWRIGHT_CLIENT_ID` really is unique on this
|
|
1090
|
-
machine (section 2). If you get a 200 with a `run_id` that is not yours in
|
|
1091
|
-
substance, that is the symptom of the collapse described in section 2 on the MCP
|
|
1092
|
-
path; curl (which uses its own random `Idempotency-Key` per call) will not
|
|
1093
|
-
reproduce it directly, but it is worth checking separately via `get_run` on that
|
|
1094
|
-
`run_id`.
|
|
1095
|
-
|
|
1096
|
-
The second request above — `GET /v1/runs/{id}` — is free: you can poll an existing
|
|
1097
|
-
run as often as you like without making a new paid call.
|
|
1098
|
-
|
|
1099
|
-
## The whole sequence (what must reproduce)
|
|
1100
|
-
|
|
1101
|
-
1. Install the server: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
|
|
1102
|
-
starts and prints `running on stdio` (section 1.1). From source:
|
|
1103
|
-
`pnpm install && pnpm build` in the repository root, then `dist/index.js`
|
|
1104
|
-
exists.
|
|
1105
|
-
2. Generate a unique `CLIPWRIGHT_CLIENT_ID` (section 2) and put it, together with
|
|
1106
|
-
`CLIPWRIGHT_API_KEY`, into the MCP host config (section 1.3).
|
|
1107
|
-
3. Restart the host and confirm `tools/list` returns 4 tools (`make_ugc`,
|
|
1108
|
-
`quote_ugc`, `get_run`, `list_voices`).
|
|
1109
|
-
4. `quote_ugc` with a script → show the price AND read `source`,
|
|
1110
|
-
`resolved_aspect_ratio`, `warnings[]` (section 3).
|
|
1111
|
-
5. `make_ugc` → get a `run_id` immediately.
|
|
1112
|
-
6. `get_run` every ~5–10 s, reading `state` along the way and `status` + `state` on
|
|
1113
|
-
the terminal response, until `SUCCEEDED` (`video_url`) or `FAILED` (`error`).
|
|
1114
|
-
7. On failure — the same two curl commands from section 6 to split the diagnosis.
|
|
530
|
+
If curl reaches `succeeded`
|
|
531
|
+
with its own `run_id` and MCP does not, look at the MCP build, host config and
|
|
532
|
+
environment. If curl fails too, check that `$TOKEN` is a whole `cw_*` key, that
|
|
533
|
+
`$API` is reachable, and that `CLIPWRIGHT_CLIENT_ID` is unique (section 2).
|
|
1115
534
|
|
|
1116
535
|
### If you are a second user evaluating the product
|
|
1117
536
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
Before you start, make sure of two different things.
|
|
537
|
+
Your task: **reach a playable video using this file, without spoken help from
|
|
538
|
+
whoever gave you access.** What is tested is the guide, not you: do not ask,
|
|
539
|
+
write down where you got stuck.
|
|
1123
540
|
|
|
1124
541
|
1. **The token was issued on YOUR account, not merely as a separate token.** The
|
|
1125
|
-
idempotency axis is `account_id`, and one account can hold several live
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
- **`rejected_field`, `unknown_field`, `aspect_conflict`** — a contract doing its job, not a
|
|
1143
|
-
defect. Read the error text: it names both the reason and the replacement;
|
|
1144
|
-
- **everything else** (timeout, 5xx, empty response, tool not found) — a candidate
|
|
1145
|
-
for a real defect. Here, run the same two curl commands from section 6: if curl
|
|
1146
|
-
reaches a playable link and MCP does not, the cause is in the MCP packaging
|
|
1147
|
-
layer, and that should be written down separately from the rest.
|
|
542
|
+
idempotency axis is `account_id`, and one account can hold several live
|
|
543
|
+
tokens. You cannot check this from your side, so the confirmation that the
|
|
544
|
+
token is on a SEPARATE account comes WITH the token. If it did not, write that
|
|
545
|
+
down and move on.
|
|
546
|
+
2. **`CLIPWRIGHT_CLIENT_ID` was not copied from anyone's machine** (section 2).
|
|
547
|
+
If you and the person who issued your token are on one account AND your
|
|
548
|
+
`clientId` matches theirs, the server returns THAT OWNER'S RUN with a 200 and
|
|
549
|
+
a playable link: a false success that looks more convincing than a real one.
|
|
550
|
+
|
|
551
|
+
Tell two classes of failure apart:
|
|
552
|
+
|
|
553
|
+
- **`rejected_field`, `unknown_field`, `aspect_conflict`** — the contract doing
|
|
554
|
+
its job; the message names the reason and the replacement.
|
|
555
|
+
- **everything else** (a timeout, `5xx`, an empty response, a missing tool) — a
|
|
556
|
+
candidate for a real defect. Run the curl commands of section 7 and write down
|
|
557
|
+
separately whether curl reached a playable link where MCP did not.
|