{
  "openapi": "3.1.0",
  "info": {
    "title": "GetVideoFrames Video Frame API",
    "version": "1.0.0",
    "summary": "Turn a public video link into still frames.",
    "description": "Extract JPG, PNG, or WebP frames from YouTube, TikTok, Instagram, Facebook, X, Vimeo, Google Drive, or direct .mp4/.mov/.webm URLs by interval, evenly spaced count, or exact timestamps. Jobs are asynchronous: create a job, then poll it or pass wait=true to block up to 50 seconds.\n\nCredits: 1 credit = 1 started minute of processed video in interval or count mode (up to 100 frames per minute; 1 extra credit per additional 100 frames), or 1 credit per 10 frames in timestamp mode. Failed jobs are not charged. The free plan includes 60 credits per month.\n\nRate limits: responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. A 429 RateLimited error also sends Retry-After.\n\nThe same operations are available to AI agents through the remote MCP server at https://mcp.getvideoframes.com/mcp.",
    "termsOfService": "https://getvideoframes.com/terms/",
    "contact": {
      "name": "GetVideoFrames support",
      "email": "support@getvideoframes.com",
      "url": "https://getvideoframes.com/contact/"
    }
  },
  "externalDocs": {
    "description": "Developer guide, pricing, and dashboard",
    "url": "https://getvideoframes.com/developers/"
  },
  "servers": [{ "url": "https://api.getvideoframes.com/v1" }],
  "security": [{ "bearerAuth": [] }, { "apiKeyHeader": [] }],
  "tags": [
    { "name": "Jobs", "description": "Frame extraction jobs" },
    { "name": "Account", "description": "Usage and pricing" },
    { "name": "Demo", "description": "Unauthenticated 6-frame preview" }
  ],
  "paths": {
    "/jobs": {
      "post": {
        "tags": ["Jobs"],
        "operationId": "createJob",
        "summary": "Create a frame extraction job",
        "description": "Queues a frame extraction for a public video URL. Credits are charged only when the job completes.",
        "parameters": [
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "description": "Block up to 50 seconds for the result.",
            "schema": { "type": "boolean" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateJobRequest" },
              "examples": {
                "interval": {
                  "summary": "One frame every 2 seconds",
                  "value": { "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "every": 2, "format": "jpg" }
                },
                "count": {
                  "summary": "12 evenly spaced frames",
                  "value": { "url": "https://www.tiktok.com/@user/video/1234567890", "count": 12 }
                },
                "timestamps": {
                  "summary": "Exact timestamps",
                  "value": { "url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "timestamps": [1, 5.5, 12], "format": "png" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created. With wait=true the job may already be completed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "get": {
        "tags": ["Jobs"],
        "operationId": "listJobs",
        "summary": "List jobs",
        "description": "Newest first. History rows do not include frame URLs; read a single job for those.",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } },
          { "name": "skip", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["queued", "processing", "completed", "failed"] } }
        ],
        "responses": {
          "200": {
            "description": "A page of jobs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": { "type": "integer" },
                    "limit": { "type": "integer" },
                    "skip": { "type": "integer" },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Job" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/jobs/{id}": {
      "get": {
        "tags": ["Jobs"],
        "operationId": "getJob",
        "summary": "Get a job",
        "description": "Returns status and presigned frame URLs while the job is completed and not expired.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "wait", "in": "query", "description": "Block up to 50 seconds for completion.", "schema": { "type": "boolean" } }
        ],
        "responses": {
          "200": { "description": "The job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/usage": {
      "get": {
        "tags": ["Account"],
        "operationId": "getUsage",
        "summary": "Get plan, credit balance, and limits",
        "responses": {
          "200": { "description": "Usage", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Usage" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/pricing": {
      "get": {
        "tags": ["Account"],
        "operationId": "getPricing",
        "summary": "Get public plans and credit pricing",
        "security": [],
        "responses": {
          "200": { "description": "Pricing catalog", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/demo": {
      "post": {
        "tags": ["Demo"],
        "operationId": "createDemo",
        "summary": "Create a free 6-frame preview",
        "description": "No authentication. Returns 6 evenly spaced JPG frames up to 640 px wide. Videos up to 15 minutes are processed whole; longer videos return 6 frames from the opening instead of an error. Limited per IP per day; never charges credits.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri" } } }
            }
          }
        },
        "responses": {
          "201": { "description": "Demo job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DemoJob" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/demo/{id}": {
      "get": {
        "tags": ["Demo"],
        "operationId": "getDemo",
        "summary": "Get a demo job",
        "security": [],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Demo job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DemoJob" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from https://getvideoframes.com/developers/dashboard/, sent as Authorization: Bearer gvf_live_…"
      },
      "apiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-API-Key" }
    },
    "schemas": {
      "CreateJobRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Public video URL or direct .mp4/.mov/.webm link" },
          "mode": { "type": "string", "enum": ["interval", "count", "timestamps"], "description": "Inferred from count or timestamps when omitted" },
          "every": { "type": "number", "minimum": 0.1, "maximum": 3600, "default": 1, "description": "Seconds between frames in interval mode" },
          "count": { "type": "integer", "minimum": 1, "description": "Evenly spaced frame count" },
          "timestamps": { "type": "array", "items": { "type": "number", "minimum": 0 }, "maxItems": 500, "description": "Exact timestamps in seconds" },
          "start": { "type": "number", "minimum": 0, "description": "Range start in seconds" },
          "end": { "type": "number", "exclusiveMinimum": 0, "description": "Range end in seconds" },
          "format": { "type": "string", "enum": ["jpg", "png", "webp"], "default": "jpg" },
          "resolution": {
            "type": "string",
            "enum": ["240", "360", "480", "720", "1080", "1440", "2160"],
            "default": "1080",
            "description": "Requested resolution. Free plan max is 720. Frames are never upscaled; if the video lacks this size, the next lower version is used and resolution_note explains it."
          },
          "max_width": { "type": "integer", "minimum": 64, "description": "Extra downscale, never above the requested resolution" },
          "output": { "type": "string", "enum": ["urls", "zip", "both"], "default": "urls", "description": "Jobs over 300 frames are always zipped" },
          "webhook_url": { "type": "string", "format": "uri", "description": "HTTPS URL that receives job.completed or job.failed" },
          "wait": { "type": "boolean", "description": "Block up to 50 seconds for the result" }
        }
      },
      "Frame": {
        "type": "object",
        "properties": {
          "index": { "type": "integer" },
          "timestamp": { "type": "number", "description": "Seconds from the start of the source" },
          "status": { "type": "string", "enum": ["ok", "error"] },
          "url": { "type": ["string", "null"], "format": "uri", "description": "Presigned URL, valid until expires_at" },
          "error": { "type": ["string", "null"] }
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "object": { "type": "string", "const": "job" },
          "status": { "type": "string", "enum": ["queued", "processing", "completed", "failed", "expired"] },
          "url": { "type": "string" },
          "platform": { "type": "string" },
          "options": { "type": "object", "additionalProperties": true },
          "duration_seconds": { "type": ["number", "null"] },
          "frames_count": { "type": ["integer", "null"] },
          "frames_failed": { "type": "integer" },
          "credits_charged": { "type": ["integer", "null"] },
          "width": { "type": ["integer", "null"] },
          "height": { "type": ["integer", "null"] },
          "frames": { "type": "array", "items": { "$ref": "#/components/schemas/Frame" } },
          "zip_url": { "type": ["string", "null"], "format": "uri" },
          "zip_bytes": { "type": ["integer", "null"] },
          "requested_resolution": { "type": ["integer", "null"] },
          "delivered_resolution": { "type": ["integer", "null"] },
          "resolution_note": { "type": ["string", "null"] },
          "notice": { "type": "string" },
          "error": { "oneOf": [{ "$ref": "#/components/schemas/JobError" }, { "type": "null" }] },
          "created_at": { "type": "string", "format": "date-time" },
          "started_at": { "type": ["string", "null"], "format": "date-time" },
          "completed_at": { "type": ["string", "null"], "format": "date-time" },
          "expires_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "DemoJob": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "object": { "type": "string", "const": "demo_job" },
          "status": { "type": "string", "enum": ["queued", "processing", "completed", "failed", "expired"] },
          "platform": { "type": "string" },
          "duration_seconds": { "type": ["number", "null"] },
          "frames_count": { "type": ["integer", "null"] },
          "frames_failed": { "type": "integer" },
          "width": { "type": ["integer", "null"] },
          "height": { "type": ["integer", "null"] },
          "frames": { "type": "array", "items": { "$ref": "#/components/schemas/Frame" } },
          "error": { "oneOf": [{ "$ref": "#/components/schemas/JobError" }, { "type": "null" }] },
          "created_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": ["string", "null"], "format": "date-time" },
          "expires_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "JobError": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "message": { "type": "string" },
          "userMessage": { "type": "string" }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "usage" },
          "plan": { "type": "string", "enum": ["free", "starter", "pro", "scale"] },
          "credits": {
            "type": "object",
            "properties": {
              "available": { "type": "integer" },
              "plan": { "type": "integer", "description": "Resets each period" },
              "pack": { "type": "integer", "description": "Never expires" },
              "monthly_allowance": { "type": "integer" }
            }
          },
          "period": {
            "type": "object",
            "properties": {
              "start": { "type": ["string", "null"], "format": "date-time" },
              "end": { "type": ["string", "null"], "format": "date-time" }
            }
          },
          "subscription": {
            "type": ["object", "null"],
            "properties": {
              "status": { "type": "string" },
              "cancel_at_period_end": { "type": "boolean" }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_video_minutes": { "type": "integer" },
              "max_frames_per_job": { "type": "integer" },
              "max_width": { "type": "integer" },
              "retention_days": { "type": "integer" },
              "max_active_jobs": { "type": "integer" },
              "requests_per_minute": { "type": "integer" }
            }
          },
          "this_period": { "type": "object", "additionalProperties": true },
          "recent_ledger": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Feathers error class, for example BadRequest or NotAuthenticated"
          },
          "message": { "type": "string" },
          "code": { "type": "integer" },
          "className": { "type": "string" },
          "data": {
            "type": "object",
            "description": "Structured details. data.name is the API error type: InvalidUrl, InvalidRequest, NoVideoFound, PrivateContent, DownloadFailed, VideoTooLong, TooManyFrames, ResolutionNotAllowed, InsufficientBalance, InvalidApiKey, AuthenticationRequired, RateLimited, DemoLimitReached, JobNotFound, ExtractionFailed, ServiceUnavailable.",
            "additionalProperties": true
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid URL, unsupported or private video, or a plan limit was exceeded",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Missing, invalid, or revoked API key",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PaymentRequired": {
        "description": "Not enough credits (InsufficientBalance)",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Job not found",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Too many requests. Retry after the number of seconds in Retry-After.",
        "headers": {
          "Retry-After": { "schema": { "type": "integer" } },
          "X-RateLimit-Limit": { "schema": { "type": "integer" } },
          "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
          "X-RateLimit-Reset": { "schema": { "type": "integer" } }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  }
}
