{
  "openapi": "3.1.0",
  "info": {
    "title": "ScriptHaul API",
    "version": "1.0.0",
    "description": "Find YouTube videos, deliver captions, search indexed source-language passages within one channel or video, and monitor new uploads. A delivered YouTube search page or successful cold transcript costs one credit. Cache hits, library search, monitoring, channel resolution, latest uploads, and failures cost zero. Automatic monitor jobs use normal delivery credits. No video or audio download and no public transcript directory. OAuth tokens are accepted only by the separate MCP endpoint, not these REST operations; all grants for one account share one tier allowance pool while ordinary API-key limits remain per key.",
    "termsOfService": "https://api.scripthaul.com/terms",
    "contact": {
      "email": "support@scripthaul.com"
    }
  },
  "servers": [
    {
      "url": "https://api.scripthaul.com"
    }
  ],
  "tags": [
    {
      "name": "Authentication"
    },
    {
      "name": "Keys"
    },
    {
      "name": "Transcripts"
    },
    {
      "name": "Videos"
    },
    {
      "name": "Discovery"
    },
    {
      "name": "Account"
    },
    {
      "name": "Billing"
    },
    {
      "name": "Bulk jobs"
    },
    {
      "name": "Webhooks"
    },
    {
      "name": "Status"
    },
    {
      "name": "Library"
    },
    {
      "name": "Monitors"
    }
  ],
  "paths": {
    "/v1/auth/code": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "requestAuthCode",
        "summary": "Send a six-digit verification code",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthCodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code accepted for delivery",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "logoutSession",
        "summary": "End the dashboard session",
        "description": "Deletes the session behind the sh_dashboard cookie and clears the cookie. Like every cookie-authenticated mutation it requires the X-ScriptHaul-Dashboard: 1 header. API keys have no session to end.",
        "responses": {
          "204": {
            "description": "Signed out",
            "headers": {
              "Set-Cookie": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/auth/verify": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "verifyAuthCode",
        "summary": "Verify a code and return a session and first key",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verified",
            "headers": {
              "Set-Cookie": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthVerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "tags": [
          "Keys"
        ],
        "operationId": "listKeys",
        "summary": "List account keys without secret values",
        "responses": {
          "200": {
            "description": "Key list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "createKey",
        "summary": "Create and show a key once",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/keys/{keyId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/KeyId"
        }
      ],
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "updateKey",
        "summary": "Rename a key or set its daily credit cap",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Keys"
        ],
        "operationId": "revokeKey",
        "summary": "Revoke immediately or with a 24-hour grace period",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grace_period_hours": {
                    "type": "integer",
                    "enum": [
                      0,
                      24
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked or scheduled"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/keys/{keyId}/rotate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/KeyId"
        }
      ],
      "post": {
        "tags": [
          "Keys"
        ],
        "operationId": "rotateKey",
        "summary": "Create a replacement and give the old key 24 hours of grace",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Replacement key shown once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/transcript": {
      "get": {
        "tags": [
          "Transcripts"
        ],
        "operationId": "getTranscript",
        "summary": "Get one cache-first transcript",
        "description": "Use exactly one query mode: video_id starts or reads a transcript; request_id polls an earlier 202 response. The two parameters are mutually exclusive. A cache hit costs 0 credits and a cold delivery 1. With translate=true, when YouTube declines to translate the captions, the transcript is delivered in the requested language for 300 credits per started 10,000 characters of transcript (at most 1,500), on top of the delivery credit, labelled translation: served; nothing extra is charged when it cannot be delivered.",
        "x-scripthaul-query-modes": {
          "oneOf": [
            {
              "required": [
                "video_id"
              ],
              "forbidden": [
                "request_id"
              ]
            },
            {
              "required": [
                "request_id"
              ],
              "forbidden": [
                "video_id"
              ]
            }
          ]
        },
        "parameters": [
          {
            "name": "video_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 11,
              "maxLength": 11
            },
            "description": "An 11-character YouTube video ID. Required only when request_id is absent."
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "clean",
                "timestamped",
                "both",
                "srt",
                "vtt",
                "md"
              ],
              "default": "both"
            }
          },
          {
            "name": "language",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "en"
            }
          },
          {
            "name": "fallback",
            "in