{
  "openapi": "3.1.0",
  "info": {
    "title": "Video Hooks API",
    "version": "1.0.0",
    "description": "Browse and search published video hooks. No account or API key is required. Browser GET requests are supported from any origin.\n\nBrowsing shares a limit of 60 requests per IP per minute across these endpoints, website collection data, and MCP browsing tools. Nonempty searches also have a separate limit of 30 requests per IP per minute. Limits are approximate and enforced per Cloudflare location. Throttled requests return 429 with a Retry-After header of 60 seconds.\n\nOpen `/hooks/{id}` for a hook's public preview page. Thumbnail URLs use `/thumbnails/{sha256}.jpg`."
  },
  "servers": [
    {
      "url": "/",
      "description": "Current deployment"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Hooks",
      "description": "Browse the published hook library."
    }
  ],
  "paths": {
    "/api/hooks": {
      "get": {
        "operationId": "listVideoHooks",
        "summary": "List hooks",
        "tags": [
          "Hooks"
        ],
        "description": "Returns the newest hooks first, ordered by publication time and then ID. New hooks can shift page boundaries between requests. An empty library returns `hooks: []`, `total: 0`, and `pages: 1`. A page past the end returns an empty array. Repeated parameters and pagination offsets outside JavaScript's safe integer range return 400.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number, starting at 1. Send this parameter once.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Hooks per page. Send this parameter once.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of published hooks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookPage"
                },
                "examples": {
                  "emptyLibrary": {
                    "summary": "Empty library",
                    "value": {
                      "hooks": [],
                      "page": 1,
                      "limit": 20,
                      "total": 0,
                      "pages": 1
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/hooks/{id}": {
      "get": {
        "operationId": "getVideoHook",
        "summary": "Get a hook",
        "tags": [
          "Hooks"
        ],
        "description": "Retrieve one hook using its ID from listing or search results, plus up to six related hooks. Related results can be empty.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Hook ID from listing or search results.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested hook and related results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "No hook exists for this ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "searchVideoHooks",
        "summary": "Search hooks",
        "tags": [
          "Hooks"
        ],
        "description": "Search by caption, topic, mood, subject, or scene. Returns up to 20 hooks in semantic relevance order, without pagination. Missing or blank queries return `{ query: \"\", matches: [] }`. Nonempty searches are limited to 30 requests per IP per minute before any OpenAI call. A rate-limited request returns 429; wait a minute before retrying.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "One URL-encoded search string. Whitespace is trimmed and only the first 200 characters are used. Repeated q parameters return 400.",
            "schema": {
              "type": "string",
              "default": ""
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The normalized query and matching hooks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResults"
                },
                "examples": {
                  "emptyQuery": {
                    "summary": "Empty query",
                    "value": {
                      "query": "",
                      "matches": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Hook": {
        "type": "object",
        "required": [
          "id",
          "sha256",
          "title",
          "description",
          "duration",
          "width",
          "height",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1,
            "description": "Hook ID. Use /hooks/{id} to open its public preview page."
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "description": "Extracted hook hash. Its thumbnail is available at /thumbnails/{sha256}.jpg."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hook title. Older records can have no title."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description of the scene."
          },
          "duration": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Clip length in seconds."
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "description": "Video width in pixels."
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "description": "Video height in pixels."
          },
          "created_at": {
            "type": "integer",
            "description": "Publication time as Unix seconds."
          }
        }
      },
      "HookPage": {
        "type": "object",
        "required": [
          "hooks",
          "page",
          "limit",
          "total",
          "pages"
        ],
        "properties": {
          "hooks": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Hook"
            }
          },
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Total number of hooks in the library."
          },
          "pages": {
            "type": "integer",
            "minimum": 1,
            "description": "Number of pages for the requested limit. An empty library has one empty page."
          }
        }
      },
      "HookDetail": {
        "type": "object",
        "required": [
          "hook",
          "related"
        ],
        "properties": {
          "hook": {
            "$ref": "#/components/schemas/Hook"
          },
          "related": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "$ref": "#/components/schemas/Hook"
            }
          }
        }
      },
      "SearchMatch": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Hook"
          },
          {
            "type": "object",
            "required": [
              "score"
            ],
            "properties": {
              "score": {
                "type": "number",
                "description": "Cosine similarity. Higher scores mean closer semantic matches, not a probability."
              }
            }
          }
        ]
      },
      "SearchResults": {
        "type": "object",
        "required": [
          "query",
          "matches"
        ],
        "properties": {
          "query": {
            "type": "string",
            "maxLength": 200,
            "description": "The trimmed, truncated search string."
          },
          "matches": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/SearchMatch"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "statusCode",
          "statusMessage"
        ],
        "properties": {
          "statusCode": {
            "type": "integer",
            "minimum": 400,
            "maximum": 599
          },
          "statusMessage": {
            "type": "string",
            "description": "Human-readable error detail."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid or repeated parameters, unsafe pagination offset, or an invalid hook ID.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "Hook storage, semantic search, or the browsing rate limiter is not configured.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "A storage or search service request failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "The shared browsing limit or separate search limit was reached. Wait 60 seconds before retrying.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "enum": [
                60
              ]
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
