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

Base URLhttps://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.com

Quick start

  1. Create a key in the app under API & agents (scopes: read, clip, post, admin).
  2. Add a watched source. Clips from new uploads land automatically.
  3. List clips by score and download the winners. (Approving and auto-posting are internal beta; see Posts.)
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.",
    "clip_prefs": { "lengths": [30, 60], "max_clips": 10 }
  }'

Authentication

Send one of:

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

Plans & limits

PlanPriceSource hours / moClips / moDownloadAPI + MCPAuto-posting
Free$03201080x1920, no watermark—coming soon
Creator$19151501080x1920, no watermarkyescoming soon
Clipper$39405001080x1920, no watermarkyescoming soon
Agency$991502,0001080x1920, no watermarkyescoming soon

Usage counts source minutes once per job (at ingest) and clips once per rendered clip (usage.clips vs limits.clips). 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, plus features: {posting, download}.

Workspace & keys

GET/v1/me

The signed-in user and their workspaces: {user, workspaces: [{ws_id, name, plan, role}]}.

GET/v1/workspace

Workspace metadata, usage, limits, features: {posting, download}, mcp_url and (internal) the posting-rail profile up_profile.

PATCH/v1/workspace

Body: {name?, owner_phone?}

POST/v1/keys

Body: {name, scopes: ["read","clip","post","admin"]} → {key_id, key, prefix, scopes}. key is only ever returned here.

GET/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.

POST/v1/sources

Body: {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).

GET/v1/sources GET/v1/sources/{id}
PATCH/v1/sources/{id}

Any mutable field. {"watch": false} pauses the watcher.

DELETE/v1/sources/{id}

Removes the source. Its clips stay in the bank.

POST/v1/sources/{id}/run

Body: {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

POST/v1/uploads

Body: {filename, content_type} → {upload_key, put_url}. PUT the file to put_url (valid 1 hour), then start a job with upload_key.

POST/v1/jobs

Body: 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.

GET/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.

POST/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

GET/v1/clips

Filters: source_id, job_id, status, min_score, since, limit, cursor. Statuses: ready | needs_approval | approved | scheduled | posted | skipped | held | failed.

GET/v1/clips/{id}

The clip plus urls: {video, thumb} (presigned, 15 min).

PATCH/v1/clips/{id}

Body: {title?, caption?, hashtags?, hooks?}. Free; never costs usage.

POST/v1/clips/{id}/approve

Body: {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. Internal beta: public plans get 403 posting_not_available.

POST/v1/clips/{id}/skip POST/v1/clips/{id}/hold
POST/v1/clips/{id}/rerender

Body: {brandkit_id?, hook?, caption_style?, length?, format?, start?, end?} → a job with kind: "rerender". Free.

GET/v1/clips/{id}/download

→ {url} presigned 1080x1920 MP4 (15 min). This is the Phase 1 output for every plan; no watermark.

{ "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

POST/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.

POST/v1/brandkits/{id}/assets

Body: {kind: "logo"|"endplate", filename} → {put_url}. PUT the PNG there.

AccountsInternal beta — coming soon for all plans

Connected social accounts. Posting runs through our rail; each account keeps its own cadence and health.

Phase 1: these routes answer 403 posting_not_available for free, creator, clipper and agency workspaces. They are live for internal workspaces and will open to all plans soon. Check features.posting on GET /v1/workspace.

POST/v1/accounts/connect

Body: {platforms: ["tiktok","instagram","youtube","facebook","x","linkedin","threads","pinterest"]} → {url}. Open it in a browser, sign in to each platform, then sync.

POST/v1/accounts/sync

Pulls the connected accounts from the rail and upserts them → list.

GET/v1/accounts
PATCH/v1/accounts/{id}

Body: {per_day, windows, tz, jitter_min, angle: {kit_id?, hook_style?}, health: "paused"}.

DELETE/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": "…" }

SchedulesInternal beta — coming soon for all plans

POST/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.

PostsInternal beta — coming soon for all plans

POST/v1/posts

Body: {clip_id, account_ids, at?: iso | "next" | "now"} → POST items with status: "scheduled". Omit at for the next free slot per account.

GET/v1/posts GET/v1/posts/{id}

Filters: status (scheduled|posting|sent|failed|canceled), clip_id, account_id.

POST/v1/posts/{id}/cancel POST/v1/posts/{id}/retry

Every 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 & analyticsInternal beta — coming soon for all plans

GET/v1/metrics

Query: clip_id | account_id | source_id and window=1h|24h|72h|7d → views, likes, comments, shares.

GET/v1/analytics/summary

→ {posted_7d, views_7d, best_hooks[], best_slots[], usable_ratio, calibration}. Sparse early on; fills in as posts age.

GET/v1/usage

Period usage against plan limits.

GET/v1/events

Last 100 workspace events (jobs, posts, approvals, billing, agent actions).

Billing

POST/v1/billing/checkout

Body: {plan: "creator"|"clipper"|"agency"} → {url} Stripe Checkout. Requires admin.

POST/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, horror stories only, 30 to 60 seconds, and download anything over 80." The agent will call add_source, poll list_clips and hand you the download links.

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. Tools marked internal beta return isError: true with posting_not_available on public plans until auto-posting opens to all.

ToolInputsREST equivalent
add_sourcetype, url, rights, watch, backfill, ruleset_text, brandkit_id, gate, schedulePOST /v1/sources
run_sourcesource_id, limitPOST /v1/sources/{id}/run
list_sources—GET /v1/sources
clip_urlurl, rights, ruleset_text, max_clips, lengthsPOST /v1/jobs
get_jobjob_idGET /v1/jobs/{id}
list_jobsstatusGET /v1/jobs
list_clipssource_id, status, min_score, limitGET /v1/clips
get_clipclip_idGET /v1/clips/{id}
approve_clip internal betaclip_id, account_idsPOST /v1/clips/{id}/approve
skip_clipclip_idPOST /v1/clips/{id}/skip
rerender_clipclip_id, brandkit_id, hook, caption_stylePOST /v1/clips/{id}/rerender
list_accounts internal beta—GET /v1/accounts
connect_accounts internal betaplatformsPOST /v1/accounts/connect
sync_accounts internal beta—POST /v1/accounts/sync
set_account_cadence internal betaaccount_id, per_day, windows, tzPATCH /v1/accounts/{id}
create_brandkitkit fieldsPOST /v1/brandkits
list_brandkits—GET /v1/brandkits
post_clip internal betaclip_id, account_ids, atPOST /v1/posts
list_posts internal betastatusGET /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

ObjectFields
Workspacename, 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
Sourcetype, 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
Jobsrc_id, kind (clip|rerender|backfill), input{}, status, progress, stage_msg, clips_count, source_minutes, error, created, updated, finished, clips[]
Clipjob_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
BrandKitname, 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
Accountplatform, handle, display, rail, health (healthy|needs_reauth|paused|unknown), per_day, windows[], tz, jitter_min, angle{}, warmup_stage, last_post_at, created
Schedulename, account_ids[], source_ids[], per_day, windows[], tz, jitter_min, paused_until, created
Postclip_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

StatuscodeWhen
400bad_requestMissing or invalid field.
401unauthorizedMissing, revoked or expired credentials.
403forbidden / scopeKey lacks the scope (e.g. post), or rights/health rule refused the action.
403posting_not_availableAccounts, schedules, posts and approve on a public plan during Phase 1. Body: {"error":{"code":"posting_not_available","message":"Auto-posting is coming soon for your plan. Download your clips in the meantime."}}
402plan_limitSource hours or posted clips exhausted for the period.
404not_foundObject isn't in this workspace.
409conflictInvalid state transition (e.g. approving a posted clip).
429rate_limitedSlow down.
5xxinternalOur 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

Public plans (Phase 1): every rendered clip lands ready; download it from GET /v1/clips/{id}/download. Internal 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.