Clip or Die API
Everything the web app does is an API call. The same engine is exposed three ways: REST under /v1, an MCP server at /mcp, and the web app itself (which is just a client of the API).
https://api.clipordie.com (during beta the same API answers at )HealthGET /v1/health → {"ok":true,"version":"…"}FormatJSON in, JSON out. ISO-8601 UTC timestamps. Money in cents. Durations in seconds.Supportsupport@clipordie.comQuick start
- Create a key in the app under API & agents (scopes:
read,clip,post,admin). - Add a watched source. Clips from new uploads land automatically.
- List clips, approve the ones you like, or let the gate auto-post above a score.
curl -X POST https://api.clipordie.com/v1/sources \
-H "Authorization: Bearer cod_live_…" \
-H "Content-Type: application/json" \
-d '{
"type": "youtube_channel",
"url": "https://youtube.com/@MrBallen",
"rights": "licensed",
"license_ref": "campaign-2026-10",
"watch": true,
"backfill": 3,
"ruleset_text": "Horror stories only. Skip sponsor reads. 30-60s. End on the reveal.",
"gate": { "auto_post_min_score": 80, "approval_channel": "app" },
"schedule": { "account_ids": ["acc_…"], "per_day": 3,
"windows": [["09:00","10:00"],["13:00","14:00"],["18:00","19:00"]],
"tz": "America/Chicago" }
}'Authentication
Send one of:
Authorization: Bearer cod_live_…— a workspace API key. Keys carry scopes;postis required for anything that publishes,adminfor keys and billing.x-api-key: cod_live_…— same thing, for MCP clients that only set custom headers.Authorization: Bearer <Cognito access token>— what the web app sends. Sessions have all scopes. Add?ws=ws_…to pick a workspace other than your default.
Keys look like cod_live_ + 32 characters. We store only a SHA-256 hash, so the full key is shown exactly once at creation.
Conventions
- Lists return
{"items": [...], "next": "cursor" | null}. Passcursor=to page. - Errors return
{"error": {"code": "...", "message": "..."}}with a 4xx/5xx status. - Idempotency: send
Idempotency-Key: <uuid>on any POST; the same key within 24 hours replays the original response. - Identifiers:
ws_workspace ·src_source ·job_job ·clp_clip ·kit_brand kit ·acc_account ·sch_schedule ·pst_post ·key_API key. - Media: every video/thumbnail URL is a presigned link valid for 15 minutes. Fetch the object again for a fresh one.
Plans & limits
| Plan | Price | Source hours / mo | Posted clips / mo | Accounts per platform | API + MCP |
|---|---|---|---|---|---|
| Free | $0 | 3 | 20 | 1 | — |
| Creator | $19 | 15 | 150 | unlimited | yes |
| Clipper | $39 | 40 | 500 | unlimited | yes |
| Agency | $99 | 150 | 2,000 | unlimited | yes |
Usage counts source minutes once per job (at ingest) and posted clips once per sent post. Retries, re-renders and caption edits are free. A limit hit returns 402 plan_limit. GET /v1/usage shows the period's counters against your limits.
Workspace & keys
/v1/meThe signed-in user and their workspaces: {user, workspaces: [{ws_id, name, plan, role}]}.
/v1/workspaceWorkspace metadata, usage, limits, mcp_url and the posting-rail profile up_profile.
/v1/workspaceBody: {name?, owner_phone?}
/v1/keysBody: {name, scopes: ["read","clip","post","admin"]} → {key_id, key, prefix, scopes}. key is only ever returned here.
/v1/keys DELETE/v1/keys/{id}{ "key_id": "key_7f2a91c0", "key": "cod_live_Qm3…k9Z", "prefix": "cod_live_Qm3a…k9Z",
"scopes": ["read","clip","post"] }Sources
A source is a watched subscription: a channel, playlist, feed, VOD, folder, upload or single URL. It carries rules, a brand kit, a gate and (optionally) a posting schedule.
/v1/sourcesBody: {type, url, rights, license_ref?, watch?, backfill?, ruleset_text?, brandkit_id?, gate?, clip_prefs?, schedule?} → source (+ job_ids when backfill > 0 or it's a single item).
type:youtube_channel | youtube_video | youtube_playlist | rss | twitch_vod | kick_vod | drive | upload | urlrights:owned | licensed | campaign | unknown. Nothing ever posts from anunknownsource; its clips are banked only until you change it.gate:{auto_post_min_score: 80, approval_channel: "app"|"imessage"|"email"|"none", originality_min: 0.5, hold_all: false}clip_prefs:{lengths: [30,60], max_clips: 10, formats: ["9:16"]}schedule:{account_ids, per_day, windows: [["09:00","10:00"],…], tz}creates and links a schedule in one call.
/v1/sources GET/v1/sources/{id}/v1/sources/{id}Any mutable field. {"watch": false} pauses the watcher.
/v1/sources/{id}Removes the source. Its clips stay in the bank.
/v1/sources/{id}/runBody: {limit?: 1} → clip the newest N items now → {job_ids}.
{ "id": "src_4c1e9a0b77d2", "type": "youtube_channel", "url": "https://youtube.com/@MrBallen",
"title": "MrBallen", "rights": "licensed", "watch": true, "backfill": 3,
"ruleset_text": "Horror stories only…", "brandkit_id": "kit_…", "schedule_ids": ["sch_…"],
"gate": {"auto_post_min_score": 80, "approval_channel": "app", "originality_min": 0.5, "hold_all": false},
"clip_prefs": {"lengths": [30,60], "max_clips": 10, "formats": ["9:16"]},
"status": "active", "last_checked": "2026-10-06T14:03:00Z", "created": "2026-10-06T13:00:00Z",
"job_ids": ["job_…","job_…","job_…"] }Jobs & uploads
/v1/uploadsBody: {filename, content_type} → {upload_key, put_url}. PUT the file to put_url (valid 1 hour), then start a job with upload_key.
/v1/jobsBody: one of {url} · {upload_key} · {source_id}, plus {ruleset_text?, brandkit_id?, max_clips?, lengths?, formats?, rights?} → job (status: "queued"). URL jobs auto-create a hidden source so every clip has one.
/v1/jobs GET/v1/jobs/{id}A job carries status, progress (0–100), stage_msg, clips_count, source_minutes, error, and clips ids when done. Poll every few seconds while it runs.
/v1/jobs/{id}/cancel{ "id": "job_8f31a0c2e4d9", "kind": "clip", "status": "rendering", "progress": 72,
"stage_msg": "Rendering clip 7 of 10", "src_id": "src_…", "input": {"url": "https://…"},
"clips_count": 6, "source_minutes": 48.2, "created": "…", "updated": "…" }Clips
/v1/clipsFilters: source_id, job_id, status, min_score, since, limit, cursor. Statuses: ready | needs_approval | approved | scheduled | posted | skipped | held | failed.
/v1/clips/{id}The clip plus urls: {video, thumb} (presigned, 15 min).
/v1/clips/{id}Body: {title?, caption?, hashtags?, hooks?}. Free; never costs usage.
/v1/clips/{id}/approveBody: {account_ids?}. Schedules the clip into the next free slot of each account (or the source's own schedule when omitted) → {post_ids}. Refused for rights: unknown sources and unhealthy accounts.
/v1/clips/{id}/skip POST/v1/clips/{id}/hold/v1/clips/{id}/rerenderBody: {brandkit_id?, hook?, caption_style?, length?, format?, start?, end?} → a job with kind: "rerender". Free.
/v1/clips/{id}/download→ {url} presigned MP4.
{ "id": "clp_2d7b4e9a10f3", "job_id": "job_…", "src_id": "src_…",
"source_title": "Man finds a door in his basement", "source_url": "https://…",
"title": "The door in the basement wasn't there yesterday",
"hooks": [{"text": "He opened the door and it wasn't empty", "score": 96}, …],
"start": 1412.4, "end": 1453.9, "duration": 41.5,
"score": {"total": 94, "hook": 96, "flow": 91, "payoff": 95, "trend": 88,
"why": "Cold open on the discovery, complete arc, ends on the reveal."},
"transcript": "…", "status": "needs_approval",
"renders": {"default": {"key": "ws/…/default.mp4", "w": 1080, "h": 1920, "bytes": 8123456, "kit_id": "kit_…", "hook": "…"}},
"caption": "He opened the door…", "hashtags": ["#horror","#truestory"], "credit_line": "Full story: @MrBallen",
"originality": 0.71, "posts": [], "urls": {"video": "https://…", "thumb": "https://…"},
"created": "…", "updated": "…" }Brand kits
/v1/brandkits GET/v1/brandkits PATCH/v1/brandkits/{id} DELETE/v1/brandkits/{id}Fields: name, font, colors {primary, accent, text}, caption_style (wordpop|karaoke|static), highlight_color, caption_pos (0–1), emojis, logo_pos, endplate_seconds, cta, hook_style, banned_words[], required_lines[], credit_template, is_default.
/v1/brandkits/{id}/assetsBody: {kind: "logo"|"endplate", filename} → {put_url}. PUT the PNG there.
Accounts
Connected social accounts. Posting runs through our rail; each account keeps its own cadence and health.
/v1/accounts/connectBody: {platforms: ["tiktok","instagram","youtube","facebook","x","linkedin","threads","pinterest"]} → {url}. Open it in a browser, sign in to each platform, then sync.
/v1/accounts/syncPulls the connected accounts from the rail and upserts them → list.
/v1/accounts/v1/accounts/{id}Body: {per_day, windows, tz, jitter_min, angle: {kit_id?, hook_style?}, health: "paused"}.
/v1/accounts/{id}{ "id": "acc_91b3c0d7e2a5", "platform": "tiktok", "handle": "@horrorbeathq", "display": "HorrorBeat",
"rail": "upload_post", "health": "healthy", "per_day": 3,
"windows": [["09:00","10:00"],["13:00","14:00"],["18:00","19:00"]], "tz": "America/Chicago",
"jitter_min": 20, "angle": {"kit_id": "kit_…", "hook_style": "question"},
"warmup_stage": 3, "last_post_at": "…", "created": "…" }Schedules
/v1/schedules GET/v1/schedules PATCH/v1/schedules/{id} DELETE/v1/schedules/{id}Fields: name, account_ids[], source_ids[] (empty = all), per_day, windows, tz, jitter_min, paused_until?.
Slot picking walks forward from now through each account's windows (in the account's timezone), one post per window up to per_day, skipping taken slots, with ± jitter_min.
Posts
/v1/postsBody: {clip_id, account_ids, at?: iso | "next" | "now"} → POST items with status: "scheduled". Omit at for the next free slot per account.
/v1/posts GET/v1/posts/{id}Filters: status (scheduled|posting|sent|failed|canceled), clip_id, account_id.
/v1/posts/{id}/cancel POST/v1/posts/{id}/retryEvery sent post carries a receipt: platform_url, platform_id, sent_at, rail_request_id. Failures retry up to 3 times (2m / 10m / 30m) and then stay failed with the error, never silently.
Metrics & analytics
/v1/metricsQuery: clip_id | account_id | source_id and window=1h|24h|72h|7d → views, likes, comments, shares.
/v1/analytics/summary→ {posted_7d, views_7d, best_hooks[], best_slots[], usable_ratio, calibration}. Sparse early on; fills in as posts age.
/v1/usagePeriod usage against plan limits.
/v1/eventsLast 100 workspace events (jobs, posts, approvals, billing, agent actions).
Billing
/v1/billing/checkoutBody: {plan: "creator"|"clipper"|"agency"} → {url} Stripe Checkout. Requires admin.
/v1/billing/portal→ {url} Stripe customer portal (change plan, cancel, invoices).
MCP server
Clip or Die speaks MCP Streamable HTTP at POST https://api.clipordie.com/mcp (JSON-RPC 2.0; one JSON object per response, no SSE needed). Authenticate with your API key as a Bearer token or x-api-key. Protocol version 2025-06-18.
Claude Code
claude mcp add --transport http clipordie https://api.clipordie.com/mcp \
--header "Authorization: Bearer cod_live_…"Generic config (Claude Desktop, Cursor, Windsurf, …)
{
"mcpServers": {
"clipordie": {
"type": "http",
"url": "https://api.clipordie.com/mcp",
"headers": { "Authorization": "Bearer cod_live_…" }
}
}
}Then say: "Start clipping youtube.com/@MrBallen, three a day to my TikTok and Instagram, horror stories only, I approve anything under 80." The agent will call add_source with a schedule and a gate and read the plan back.
MCP tools
Each tool is a thin wrapper over the same functions the REST routes use. Results come back as a JSON string in content[0].text.
| Tool | Inputs | REST equivalent |
|---|---|---|
add_source | type, url, rights, watch, backfill, ruleset_text, brandkit_id, gate, schedule | POST /v1/sources |
run_source | source_id, limit | POST /v1/sources/{id}/run |
list_sources | — | GET /v1/sources |
clip_url | url, rights, ruleset_text, max_clips, lengths | POST /v1/jobs |
get_job | job_id | GET /v1/jobs/{id} |
list_jobs | status | GET /v1/jobs |
list_clips | source_id, status, min_score, limit | GET /v1/clips |
get_clip | clip_id | GET /v1/clips/{id} |
approve_clip | clip_id, account_ids | POST /v1/clips/{id}/approve |
skip_clip | clip_id | POST /v1/clips/{id}/skip |
rerender_clip | clip_id, brandkit_id, hook, caption_style | POST /v1/clips/{id}/rerender |
list_accounts | — | GET /v1/accounts |
connect_accounts | platforms | POST /v1/accounts/connect |
sync_accounts | — | POST /v1/accounts/sync |
set_account_cadence | account_id, per_day, windows, tz | PATCH /v1/accounts/{id} |
create_brandkit | kit fields | POST /v1/brandkits |
list_brandkits | — | GET /v1/brandkits |
post_clip | clip_id, account_ids, at | POST /v1/posts |
list_posts | status | GET /v1/posts |
analytics_summary | — | GET /v1/analytics/summary |
usage | — | GET /v1/usage |
Raw JSON-RPC example
curl -X POST https://api.clipordie.com/mcp \
-H "Authorization: Bearer cod_live_…" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_clips","arguments":{"status":"needs_approval","min_score":70}}}'Object shapes
| Object | Fields |
|---|---|
| Workspace | name, plan (free|creator|clipper|agency|internal), plan_status, limits {source_hours, posted_clips, accounts_per_platform}, usage {period, source_min, posted}, up_profile, owner_phone, mcp_url, created |
| Source | type, url, title, rights, license_ref, watch, backfill, ruleset_text, brandkit_id, schedule_ids[], gate{}, clip_prefs{}, status (active|paused|error), last_checked, last_error, created |
| Job | src_id, kind (clip|rerender|backfill), input{}, status, progress, stage_msg, clips_count, source_minutes, error, created, updated, finished, clips[] |
| Clip | job_id, src_id, source_title, source_url, title, hooks[{text,score}], start, end, duration, score{total,hook,flow,payoff,trend,why}, transcript, status, renders{default{key,w,h,bytes,kit_id,hook}}, caption, hashtags[], credit_line, originality, posts[], urls{video,thumb}, created, updated |
| BrandKit | name, font, colors{}, caption_style, highlight_color, caption_pos, emojis, logo_key, logo_pos, endplate_key, endplate_seconds, cta, hook_style, banned_words[], required_lines[], credit_template, is_default |
| Account | platform, handle, display, rail, health (healthy|needs_reauth|paused|unknown), per_day, windows[], tz, jitter_min, angle{}, warmup_stage, last_post_at, created |
| Schedule | name, account_ids[], source_ids[], per_day, windows[], tz, jitter_min, paused_until, created |
| Post | clip_id, acc_id, platform, status (scheduled|posting|sent|failed|canceled), scheduled_at, sent_at, caption, hashtags, platform_url, platform_id, rail_request_id, attempts, error, created |
Errors
| Status | code | When |
|---|---|---|
| 400 | bad_request | Missing or invalid field. |
| 401 | unauthorized | Missing, revoked or expired credentials. |
| 403 | forbidden / scope | Key lacks the scope (e.g. post), or rights/health rule refused the action. |
| 402 | plan_limit | Source hours or posted clips exhausted for the period. |
| 404 | not_found | Object isn't in this workspace. |
| 409 | conflict | Invalid state transition (e.g. approving a posted clip). |
| 429 | rate_limited | Slow down. |
| 5xx | internal | Our fault. Retry with the same Idempotency-Key. |
State machines
JOB: queued → claimed → ingesting → transcribing → selecting → rendering → done | failed | canceled
POST: scheduled → posting → sent | failed (retry ≤3: 2m / 10m / 30m) | canceled
CLIP: ready → (gate) → needs_approval | scheduled → posted
approve → scheduled · skip → skipped · hold → held
Gate after render: hold_all → needs_approval. Else if the source has a schedule and score.total ≥ auto_post_min_score and rights ≠ unknown and originality ≥ originality_min → scheduled. Else → needs_approval.