{
  "openapi": "3.1.0",
  "info": {
    "title": "Everpop API",
    "version": "1.0.0",
    "description": "Turn prompts and your own videos into published YouTube Shorts with measured receipts. Built to be driven by AI agents: upload a file, clip it, publish it, and pull real 48h/7d performance — all by API. Full agent guide: https://everpop.app/skill.md · MCP connector: https://everpop.app/api/mcp/mcp?key=epk_...",
    "termsOfService": "https://everpop.app/terms",
    "contact": { "email": "privacy@everpop.app", "url": "https://everpop.app/contact" }
  },
  "servers": [{ "url": "https://everpop.app" }],
  "security": [{ "apiKey": [] }],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "epk_...",
        "description": "Per-user API key from everpop.app/dashboard/settings → API keys. Write scope required for mutating endpoints."
      }
    },
    "schemas": {
      "Envelope": {
        "type": "object",
        "description": "Every response: { ok: true, data } on success, { ok: false, error, ...detail } on failure (the HTTP status carries the verdict).",
        "properties": { "ok": { "type": "boolean" }, "data": {}, "error": { "type": "string" } },
        "required": ["ok"]
      },
      "JobRef": {
        "type": "object",
        "properties": {
          "jobId": { "type": "string" },
          "status": { "type": "string", "enum": ["PENDING", "PROCESSING", "COMPLETED", "FAILED", "CANCELED"] },
          "poll": { "type": "string", "description": "Relative URL to poll, e.g. /api/v1/jobs/{id}" }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "properties": {
          "ok": { "const": false },
          "error": { "type": "string" },
          "reason": { "type": "string" }
        },
        "required": ["ok", "error"]
      },
      "UploadSlotEnvelope": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "ok": { "const": true },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "putUrl": { "type": "string", "format": "uri" },
              "key": { "type": "string" },
              "requiredHeaders": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "Content-Type": {
                    "type": "string",
                    "enum": ["video/mp4", "video/quicktime", "video/webm", "video/x-matroska", "video/x-m4v"]
                  },
                  "If-None-Match": { "const": "*" }
                },
                "required": ["Content-Type", "If-None-Match"]
              },
              "expectedBytes": { "type": "integer", "minimum": 1, "maximum": 4294967296 },
              "expiresInSeconds": { "const": 900 },
              "next": { "type": "string" }
            },
            "required": ["putUrl", "key", "requiredHeaders", "expectedBytes", "expiresInSeconds", "next"]
          }
        },
        "required": ["ok", "data"]
      },
      "CancelUploadEnvelope": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "ok": { "const": true },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": { "canceled": { "const": true } },
            "required": ["canceled"]
          }
        },
        "required": ["ok", "data"]
      },
      "FinalizeUploadEnvelope": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "ok": { "const": true },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "videoId": { "type": "string" },
              "title": { "type": "string" },
              "existing": { "type": "boolean" },
              "linked": { "type": "boolean" },
              "autoClipping": { "type": "boolean" },
              "parked": { "type": "string", "enum": ["quota"] }
            },
            "required": ["videoId", "title", "existing", "linked", "autoClipping"]
          }
        },
        "required": ["ok", "data"]
      }
    },
    "responses": {
      "UploadRequired": {
        "description": "422 upload_required — this video's source is a YouTube URL and Everpop never downloads from YouTube. Upload the file via POST /api/v1/videos/upload (+ /complete with linkSourceVideoId to attach it to this video), then clip."
      }
    }
  },
  "paths": {
    "/api/v1/generate": {
      "post": {
        "operationId": "generateShort",
        "summary": "Generate a vertical Short from a text prompt (AI script, voiceover, visuals, captions)",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["prompt"], "properties": {
            "prompt": { "type": "string", "minLength": 5, "maxLength": 300 },
            "voice": { "type": "string", "enum": ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] },
            "tone": { "type": "string", "enum": ["educational", "storytime", "hype", "calm"] },
            "targetSeconds": { "type": "integer", "minimum": 20, "maximum": 60 },
            "captionStyle": { "type": "string", "enum": ["pop", "boxed", "minimal", "clean", "hormozi"] }
          } } } }
        },
        "responses": { "200": { "description": "Render started — poll the returned job (1–3 min).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobRef" } } } } }
      }
    },
    "/api/v1/videos": {
      "get": {
        "operationId": "listVideos",
        "summary": "List the user's source videos (detected uploads + API/dashboard uploads), newest first",
        "responses": { "200": { "description": "Array of { id, title, status, detectedAt }." } }
      }
    },
    "/api/v1/videos/upload": {
      "post": {
        "operationId": "requestUpload",
        "summary": "Step 1 of the compliant upload: get a presigned PUT for the user's video FILE",
        "description": "Everpop clips from the creator's file, never from a YouTube download. PUT exactly expectedBytes to the returned putUrl (15-min validity, ≤4 GB, .mp4/.mov/.webm/.mkv/.m4v), send every requiredHeaders entry, and let the HTTP client supply Content-Length. Then call /api/v1/videos/complete.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["filename", "sizeBytes"], "properties": {
          "filename": { "type": "string", "minLength": 1, "maxLength": 300 },
          "sizeBytes": { "type": "integer", "minimum": 1, "maximum": 4294967296, "description": "Exact byte length bound into the signed upload request; files over 4 GB need the dashboard's resumable uploader" }
        } } } } },
        "responses": {
          "200": { "description": "Exact signed upload instructions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadSlotEnvelope" } } } },
          "401": { "description": "Missing, expired, or revoked API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "402": { "description": "Subscription or plan does not allow API upload.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "403": { "description": "Account verification, suspension, or write scope blocks upload.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "Account deletion is in progress; no upload slot was opened.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "413": { "description": "File exceeds the API upload cap.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "422": { "description": "Missing/invalid size or unsupported file type.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "429": { "description": "Rate limit, active upload slots, or stuck cleanup backlog; obey Retry-After.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "503": { "description": "Storage upload is temporarily unavailable.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      },
      "delete": {
        "operationId": "cancelUpload",
        "summary": "Cancel a failed or abandoned direct-upload slot",
        "description": "Idempotent for a versioned key after terminal cleanup. A 503 cleanup_pending response means cleanup is durably recorded and will retry automatically. Never call this after successful or ambiguously acknowledged finalization.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object",
          "additionalProperties": false,
          "required": ["key"],
          "properties": { "key": { "type": "string", "minLength": 1, "maxLength": 400 } }
        } } } },
        "responses": {
          "200": { "description": "Cancellation accepted or the versioned key was already cleaned.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelUploadEnvelope" } } } },
          "401": { "description": "Missing, expired, or revoked API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "402": { "description": "Subscription or plan does not allow API writes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "403": { "description": "The key is not a versioned upload owned by this account.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "The upload was finalized or a provider operation is still settling.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "422": { "description": "The cancellation payload or key is invalid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "429": { "description": "Rate limit exceeded; obey Retry-After.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "503": { "description": "reason=cleanup_pending: provider cleanup is durably queued and will retry automatically.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/api/v1/videos/complete": {
      "post": {
        "operationId": "finalizeUpload",
        "summary": "Step 2: finalize the uploaded file into a clippable video (auto-clips for active plans)",
        "description": "rightsConfirmed must be the USER's attestation that they hold the rights — agents must ask, never assume. Pass linkSourceVideoId (a parked detected video from /videos) to attach the file to it: un-parks it and keeps its YouTube attribution. Idempotent per uploaded object.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["key", "rightsConfirmed"], "properties": {
          "key": { "type": "string", "minLength": 1, "maxLength": 400 },
          "title": { "type": "string", "maxLength": 200 },
          "rightsConfirmed": { "const": true, "description": "The user's explicit attestation; an agent must ask and may never infer this." },
          "linkSourceVideoId": { "type": "string", "minLength": 1, "maxLength": 64 }
        } } } } },
        "responses": {
          "200": { "description": "Idempotent adoption result. autoClipping=true means a render is already running; parked=quota means the file is safe but monthly videos are used up.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FinalizeUploadEnvelope" } } } },
          "400": { "description": "The uploaded object is missing or empty; start a new upload.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "401": { "description": "Missing, expired, or revoked API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "402": { "description": "Subscription or plan does not allow API writes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "403": { "description": "Write scope, ownership, suspension, or the user's rights attestation blocks finalization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "Account/video deletion fence, secure deletion, or upload_not_ready settlement fence. Retry only upload_not_ready with the same key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "413": { "description": "The stored object exceeds the 4 GB upload cap and is scheduled for secure cleanup.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "422": { "description": "The request body, key, title, or linkSourceVideoId is invalid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "429": { "description": "Rate limit exceeded; replay the same key after Retry-After.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "500": { "description": "Ambiguous service failure; replay the same key and never upload/cancel the bytes again.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "503": { "description": "Ambiguous service failure; replay the same key and never upload/cancel the bytes again.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/api/v1/videos/import": {
      "post": {
        "operationId": "importVideo",
        "summary": "Register a YouTube video's METADATA from the user's own connected channel (idempotent)",
        "description": "Metadata only — the video cannot be clipped until its file is uploaded (pair with /videos/upload + linkSourceVideoId).",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string" }, "rightsConfirmed": { "type": "boolean" } } } } } },
        "responses": { "200": { "description": "{ videoId, title, existing }" } }
      }
    },
    "/api/v1/clip": {
      "post": {
        "operationId": "clipVideo",
        "summary": "Clip the best moments from one of the user's videos into vertical Shorts",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["sourceVideoId"], "properties": {
          "sourceVideoId": { "type": "string" },
          "prompt": { "type": "string", "maxLength": 300, "description": "Optional steer, e.g. 'the most surprising moment' — omit and the AI picks" },
          "clipCount": { "type": "integer", "minimum": 1, "maximum": 5 },
          "captionStyle": { "type": "string", "enum": ["pop", "boxed", "minimal", "clean", "hormozi"] },
          "targetSeconds": { "type": "integer", "minimum": 10, "maximum": 180 }
        } } } } },
        "responses": { "200": { "description": "Render started — poll the returned job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobRef" } } } }, "422": { "$ref": "#/components/responses/UploadRequired" } }
      }
    },
    "/api/v1/jobs/{id}": {
      "get": {
        "operationId": "getJob",
        "summary": "Poll a generate/clip job — returns status and, when done, the produced clips",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "{ jobId, status, clips: [{ clipId, title, previewUrl, ... }] }" } }
      }
    },
    "/api/v1/publish": {
      "get": {
        "operationId": "listPublishDestinations",
        "summary": "List the exact connected destinations available to this API key",
        "description": "Read this first and show the user the destination id, platform, and display name before requesting publish confirmation.",
        "responses": { "200": { "description": "{ data: { destinations: [{ id, platform, displayName }] } }" } }
      },
      "post": {
        "operationId": "publishClip",
        "summary": "Publish a completed clip to explicitly named destinations (explicit user confirmation required)",
        "description": "Publishing is a real-world action. Before this call, show the user every exact destination, the final title/description, and visibility, then obtain an explicit go-ahead. Omitted destinations never mean all accounts.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["clipId", "destinationIds", "metadata"], "properties": {
          "clipId": { "type": "string", "minLength": 1 },
          "destinationIds": { "type": "array", "minItems": 1, "maxItems": 10, "items": { "type": "string", "minLength": 1 }, "description": "Exact ids returned by GET /api/v1/publish" },
          "metadata": { "type": "object", "required": ["title", "description", "privacyStatus"], "properties": {
            "title": { "type": "string", "minLength": 1, "maxLength": 100 },
            "description": { "type": "string", "maxLength": 5000 },
            "privacyStatus": { "type": "string", "enum": ["public", "unlisted", "private"] }
          } }
        } } } } },
        "responses": { "200": { "description": "{ clipId, posts: [...] }" } }
      }
    },
    "/api/v1/receipts": {
      "get": {
        "operationId": "listReceipts",
        "summary": "Measured performance per published clip — real YouTube Analytics at 48h and 7d",
        "responses": { "200": { "description": "Aggregate YouTube Analytics metrics: views, watch time, retention, and subscribers for eligible YouTube Shorts. A receipt may include a separately signed pre-publish prediction; measured metrics are not separately signed." } }
      }
    }
  }
}
