{
  "openapi": "3.1.0",
  "info": {
    "title": "Cliphi Agent API",
    "description": "Turn your long video into ready-to-post vertical clips with captions and branding. Submit a job, poll it, preview every strong moment free, then render only the winners. Rendering is the one billed choice an agent makes and every render response states its cost.",
    "version": "1.1.0"
  },
  "servers": [
    {
      "url": "https://www.cliphi.com/api/v1"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/jobs": {
      "post": {
        "operationId": "createJob",
        "x-openai-isConsequential": false,
        "summary": "Start a clip job from your video's URL",
        "description": "Use when the user shares a video link and wants clips. Fast ack (about 2s); poll getJob. Processing bills per video minute. Duplicate protection covers IN-FLIGHT jobs only (already_running: nothing new charged); resubmitting a video whose job already finished starts and bills a NEW job, so confirm that with the user first. Decision on record: not flagged consequential because the per-minute reserve is small next to renders and in-flight dedup catches accidental repeats.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "video_url"
                ],
                "properties": {
                  "video_url": {
                    "type": "string",
                    "description": "Public video URL (YouTube, Twitch VOD, direct file)."
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "720p",
                      "1080p"
                    ],
                    "description": "Max source quality. Defaults to 720p."
                  },
                  "start_time": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Optional window start, seconds."
                  },
                  "end_time": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Optional window end, seconds."
                  },
                  "language": {
                    "type": "string",
                    "description": "Optional ISO 639-1 transcription language override; auto-detected otherwise."
                  },
                  "instructions": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Plain-language guidance for moment detection, e.g. 'only the product demo moments, skip the banter'. Pass the user's own words. Cannot change billing."
                  },
                  "force_new": {
                    "type": "boolean",
                    "default": false,
                    "description": "Start a fresh job even when this video already has one in flight (charges again). Never set without explicit user approval."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job accepted (or already_running: an in-flight job for this video was found and nothing new was charged). Tell the user processing has started, takes a few minutes, and they will get an email when clips are ready.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "402": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "422": {
            "description": "error_code video_too_long (the video exceeds the plan's length limit; no errors list) OR validation_error (malformed request body; detail.errors rides along).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "500": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "503": {
            "$ref": "#/components/responses/EnvelopeError"
          }
        }
      }
    },
    "/jobs/{job_id}": {
      "get": {
        "operationId": "getJob",
        "x-openai-isConsequential": false,
        "summary": "Poll a job: status, phase, rendered clips, and free moment previews",
        "description": "Free. While processing, relay `message` (the sentence to tell the user) and wait `poll_after_seconds` before polling again. When completed, `moments` lists every strong moment with a free preview_page_url and a render_status; `clips` holds rendered files only, with billed_credits from the receipt.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 40
            },
            "description": "Server-side long-poll up to 40s: returns early when a processing job hits a terminal state or an in-flight render lands. Values under 3 behave as 0."
          }
        ],
        "responses": {
          "200": {
            "description": "Job status. Note: status can be \"failed\" inside an HTTP 200 - branch on the status field, never on the HTTP code alone. A moment's render_status can likewise be \"failed\" with render_error explaining why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "403": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "404": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "422": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "500": {
            "$ref": "#/components/responses/EnvelopeError"
          }
        }
      }
    },
    "/jobs/{job_id}/moments/{moment_id}/render": {
      "post": {
        "operationId": "renderMoment",
        "x-openai-isConsequential": true,
        "summary": "Render one chosen moment into a finished clip. BILLED in credits.",
        "description": "The purchase moment - call it only after the user explicitly picked this moment. The response states the cost (estimated_credits) before the clip exists; relay it once. An existing rendered clip for the aspect ratio returns free (already_rendered); re-rendering needs force=true and explicit user approval.",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "moment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "aspect_ratio": {
                    "type": "string",
                    "enum": [
                      "9:16",
                      "4:5",
                      "1:1",
                      "16:9"
                    ],
                    "default": "9:16"
                  },
                  "force": {
                    "type": "boolean",
                    "default": false,
                    "description": "Render again even when a clip exists (billed again). Never set without explicit user approval."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "status \"rendering\" (accepted; billed up to estimated_credits on completion; poll getJob, pass wait=40 to long-poll) or \"already_rendered\" (existing clip returned, nothing billed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RenderResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AuthError"
          },
          "402": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "403": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "404": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "409": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "422": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "500": {
            "$ref": "#/components/responses/EnvelopeError"
          }
        }
      }
    },
    "/demo": {
      "get": {
        "operationId": "getDemo",
        "x-openai-isConsequential": false,
        "summary": "Keyless live demo: a real finished job with free preview pages",
        "security": [],
        "description": "No API key needed and free to call. Returns a real completed job (demo: true) in the exact getJob shape, with live preview page links. Use it to show what Cliphi produces before the user creates a key.",
        "responses": {
          "200": {
            "description": "The demo job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/EnvelopeError"
          },
          "429": {
            "description": "Demo rate limit reached; the Retry-After header states the wait in seconds.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds until the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/EnvelopeError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "chp_live_... key, created in Cliphi Studio under Settings, then API Keys."
      }
    },
    "responses": {
      "AuthError": {
        "description": "Missing or invalid API key (error_code: missing_api_key | invalid_api_key).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "EnvelopeError": {
        "description": "Failure in the Cliphi error contract. Relay `message` verbatim - it teaches the next step; `billed` states whether money moved (false on every HTTP error).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "description": "Every /api/v1 failure: {detail: {error_code, message, billed}}. 422s add detail.errors. The error_code enum is the UNION across all operations, not the per-operation set. The object is not closed: future keys may appear, and the 402's studio-side extras (action, required_credits, credits_held, eta) are NOT promised here - rely on message.",
        "properties": {
          "detail": {
            "type": "object",
            "properties": {
              "error_code": {
                "type": "string",
                "description": "Stable machine code.",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key",
                  "invalid_url",
                  "insufficient_credits",
                  "video_too_long",
                  "throttled",
                  "maintenance",
                  "upstream_error",
                  "job_not_found",
                  "access_denied",
                  "moment_not_found",
                  "moment_not_ready",
                  "source_expired",
                  "nothing_to_render",
                  "clip_too_long",
                  "render_in_progress",
                  "demo_unavailable",
                  "not_found",
                  "method_not_allowed",
                  "http_error",
                  "validation_error",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Relay this to the user verbatim; it states the fix and, where relevant, billing impact."
              },
              "billed": {
                "type": "boolean",
                "description": "Whether this failure moved money. Always false on HTTP errors."
              },
              "errors": {
                "type": "array",
                "description": "422 only: the framework's raw validation error list.",
                "items": {
                  "type": "object"
                }
              }
            },
            "required": [
              "error_code",
              "message",
              "billed"
            ]
          }
        }
      },
      "JobCreated": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "already_running"
            ]
          },
          "already_running": {
            "type": "boolean",
            "description": "True when this video already has a job in flight; job_id points at it and nothing new was charged."
          },
          "message": {
            "type": "string",
            "description": "The sentence to relay to the user."
          },
          "status_url": {
            "type": "string",
            "description": "Poll target: /api/v1/jobs/{job_id}."
          },
          "studio_url": {
            "type": "string",
            "description": "Link to this job in the Cliphi studio: live progress while processing, full editing and publishing after. Offer it to the user. null on the demo."
          }
        }
      },
      "JobStatus": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ]
          },
          "raw_status": {
            "type": "string",
            "description": "The pipeline's own state, never remapped."
          },
          "progress": {
            "type": "integer"
          },
          "phase": {
            "type": "string",
            "enum": [
              "accepted",
              "downloading",
              "transcribing",
              "finding_moments",
              "generating_clips"
            ],
            "description": "Narratable stage while processing."
          },
          "phase_label": {
            "type": "string"
          },
          "elapsed_seconds": {
            "type": "integer"
          },
          "eta_seconds": {
            "type": "integer",
            "description": "Honest estimate or absent - never fabricated."
          },
          "poll_after_seconds": {
            "type": "integer",
            "description": "Suggested delay before the next poll."
          },
          "message": {
            "type": "string",
            "description": "The sentence to relay to the user."
          },
          "video": {
            "type": "object",
            "description": "title, channel, duration_seconds, thumbnail, source_url."
          },
          "clips": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Clip"
            },
            "description": "Rendered files only."
          },
          "moments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MomentPreview"
            }
          },
          "demo": {
            "type": "boolean",
            "description": "True only on getDemo."
          },
          "studio_url": {
            "type": "string",
            "description": "Link to this job in the Cliphi studio: live progress while processing, full editing and publishing after. Offer it to the user. null on the demo."
          }
        }
      },
      "Clip": {
        "type": "object",
        "properties": {
          "clip_id": {
            "type": "string"
          },
          "moment_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The finished clip file."
          },
          "thumbnail_url": {
            "type": "string"
          },
          "aspect_ratio": {
            "type": "string"
          },
          "resolution": {
            "type": "string"
          },
          "viral_score": {
            "type": "number"
          },
          "created_at": {
            "type": "string"
          },
          "billed_credits": {
            "type": "number",
            "description": "What this render actually cost, from the billing receipt (populated on getJob reads; may be absent on an already_rendered short-circuit)."
          }
        }
      },
      "MomentPreview": {
        "type": "object",
        "properties": {
          "moment_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "hook": {
            "type": "string"
          },
          "viral_score": {
            "type": "number"
          },
          "start_time": {
            "type": "number"
          },
          "end_time": {
            "type": "number"
          },
          "transcript_excerpt": {
            "type": "string"
          },
          "preview_url": {
            "type": "string",
            "description": "Raw source segment (machine use)."
          },
          "preview_page_url": {
            "type": "string",
            "description": "Signed human-facing page showing the finished look. Free - always offer this to the user."
          },
          "rendered": {
            "type": "boolean",
            "description": "A rendered (already paid) clip exists for this moment - point the user to it instead of re-rendering."
          },
          "rendered_aspect_ratios": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "render_status": {
            "type": "string",
            "enum": [
              "not_rendered",
              "rendering",
              "rendered",
              "failed"
            ]
          },
          "render_error": {
            "type": "string",
            "description": "On failed: relay verbatim."
          },
          "render_error_code": {
            "type": "string",
            "enum": [
              "insufficient_credits",
              "source_expired",
              "render_interrupted",
              "render_failed"
            ]
          }
        }
      },
      "RenderResponse": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "moment_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "rendering",
              "already_rendered"
            ]
          },
          "aspect_ratio": {
            "type": "string"
          },
          "estimated_credits": {
            "type": "number",
            "description": "State this to the user once; 0 on already_rendered."
          },
          "clip": {
            "$ref": "#/components/schemas/Clip"
          },
          "message": {
            "type": "string",
            "description": "The sentence to relay to the user."
          },
          "status_url": {
            "type": "string"
          },
          "studio_url": {
            "type": "string",
            "description": "Link to this job in the Cliphi studio: live progress while processing, full editing and publishing after. Offer it to the user. null on the demo."
          }
        }
      }
    }
  }
}
