{
  "openapi": "3.1.0",
  "info": {
    "title": "Singing Pet API",
    "version": "1.0.0",
    "summary": "Turn a pet photo plus audio into a lip-synced singing video.",
    "description": "The Singing Pet API backs singing.pet. Templates and pricing are open to anyone;\neverything that touches a person's videos, credits or billing needs a Firebase ID\ntoken.\n\nVideo generation is asynchronous. A create call returns once the render job is\nqueued, and the video's `status` moves from `pending` to `completed` or `failed`.\nPoll `GET /user/videos` or listen on the websocket at `wss://api.singing.pet` for\nthe transition.\n\nErrors that are not produced by a specific endpoint come back as the `Error`\nschema: a machine-readable `error.code`, a human `error.message`, and an\n`error.hint` describing what to do about it.",
    "contact": {
      "name": "Singing Pet support",
      "email": "jack@jrl.software",
      "url": "https://singing.pet/contact"
    },
    "termsOfService": "https://singing.pet/terms",
    "license": {
      "name": "Proprietary",
      "url": "https://singing.pet/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.singing.pet",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://singing.pet/developers"
  },
  "tags": [
    {
      "name": "Meta",
      "description": "Service discovery, versioning and health."
    },
    {
      "name": "Public API",
      "description": "The versioned API for third parties and agents, authenticated with an API key or an OAuth 2.1 token and paid for with prepaid credits."
    },
    {
      "name": "MCP",
      "description": "Model Context Protocol server, so an AI client can call Singing Pet as tools rather than as HTTP."
    },
    {
      "name": "Catalog",
      "description": "Song templates and voices, no account needed."
    },
    {
      "name": "Videos",
      "description": "Create, list and delete a signed-in user's videos."
    },
    {
      "name": "Audio",
      "description": "Text-to-speech and audio extraction."
    },
    {
      "name": "Billing",
      "description": "Prices, checkout and subscription state."
    },
    {
      "name": "Account",
      "description": "The signed-in user's profile and credits."
    },
    {
      "name": "Email",
      "description": "Lifecycle email preferences."
    }
  ],
  "security": [
    {
      "firebaseIdToken": []
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "getServiceIndex",
        "summary": "Service index",
        "description": "Machine-readable entry point listing the spec URL, documentation and the endpoints that need no credentials.",
        "tags": [
          "Meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service index.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceIndex"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "summary": "This specification",
        "description": "Returns this OpenAPI document, the same one published at https://singing.pet/openapi.json.",
        "tags": [
          "Meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenApiDocument"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Health check",
        "description": "Returns the literal body `OK` with a 200 while the service is up. Used by the deploy platform's health probe.",
        "tags": [
          "Meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "examples": [
                    "OK"
                  ]
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/templates": {
      "get": {
        "operationId": "listTemplates",
        "summary": "List song templates",
        "description": "Every song template a pet can be animated to sing, with a preview video and still. Public and cached for an hour.",
        "tags": [
          "Catalog"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "All templates, oldest id first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Template"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Templates could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscription/pricing": {
      "get": {
        "operationId": "getPricing",
        "summary": "Get prices for the caller's country",
        "description": "Resolves the caller's country from the Cloudflare `CF-IPCountry` header and returns plan prices in that country's currency, in minor units. Unknown countries fall back to USD.",
        "tags": [
          "Billing"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Prices in the caller's currency.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pricing"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscription/status": {
      "get": {
        "operationId": "getSubscriptionStatus",
        "summary": "Get the caller's subscription",
        "description": "Current subscription state, refreshed from Stripe when reachable and served from the database otherwise.",
        "tags": [
          "Billing"
        ],
        "responses": {
          "200": {
            "description": "Subscription state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscription/create-checkout-session": {
      "post": {
        "operationId": "createCheckoutSession",
        "summary": "Start a subscription checkout",
        "description": "Creates a Stripe Checkout Session for the weekly or annual plan, priced in the caller's currency, and returns the URL to send them to.",
        "tags": [
          "Billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          },
          "400": {
            "description": "`plan_type` is missing or not a known plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscription/create-one-time-checkout": {
      "post": {
        "operationId": "createOneTimeCheckout",
        "summary": "Buy watermark removal for one video",
        "description": "Charges once to re-render a single watermarked video without the watermark. If the video was already paid for, the re-render restarts and `url` comes back null with `rerendering: true`.",
        "tags": [
          "Billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OneTimeCheckoutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created, or an already-paid re-render restarted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          },
          "400": {
            "description": "`video_id` is missing, or the video has no watermark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such video belongs to the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscription/create-portal-session": {
      "post": {
        "operationId": "createPortalSession",
        "summary": "Open the Stripe billing portal",
        "description": "Returns a Stripe customer portal URL where the caller can change or cancel their plan.",
        "tags": [
          "Billing"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PortalRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Portal session created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The caller has no Stripe customer record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user/me": {
      "get": {
        "operationId": "getCurrentUser",
        "summary": "Get the caller's account",
        "description": "Subscription flag and remaining free credits. Free credits reset to 2 at the start of each calendar month.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Account summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No account row for this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/user/videos": {
      "get": {
        "operationId": "listUserVideos",
        "summary": "List the caller's videos",
        "description": "Newest first, paginated. Includes render status and watermark state.",
        "tags": [
          "Videos"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Videos per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of videos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoPage"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/create-with-template": {
      "post": {
        "operationId": "createVideoFromTemplate",
        "summary": "Create a video from a song template",
        "description": "Queues a lip-sync render of the uploaded pet photo against a template's driving video. Consumes one free credit unless the caller is subscribed. Returns as soon as the job is queued.",
        "tags": [
          "Videos"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/CreateFromTemplateRequest"
              },
              "encoding": {
                "image": {
                  "contentType": "image/png, image/jpeg, image/webp"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Render job queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RenderQueued"
                }
              }
            }
          },
          "400": {
            "description": "Missing image or template id, unknown template, or no credits left.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/create-with-audio": {
      "post": {
        "operationId": "createVideoFromAudio",
        "summary": "Create a video from your own audio",
        "description": "Queues a lip-sync render of the uploaded pet photo against uploaded audio. Audio must be between 2 and 30 seconds; the output is as long as the audio. Non-native formats are transcoded to MP3.",
        "tags": [
          "Videos"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/CreateFromAudioRequest"
              },
              "encoding": {
                "image": {
                  "contentType": "image/png, image/jpeg, image/webp"
                },
                "audio": {
                  "contentType": "audio/mpeg, audio/wav, audio/mp4, audio/webm"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Render job queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RenderQueued"
                }
              }
            }
          },
          "400": {
            "description": "Missing image or audio, audio outside 2-30 seconds, unsupported format, or no credits left.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delete-video": {
      "delete": {
        "operationId": "deleteVideo",
        "summary": "Delete one of the caller's videos",
        "description": "Removes the video row and its stored image, audio and output files.",
        "tags": [
          "Videos"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteVideoRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Video deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteVideoResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The video could not be deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tts": {
      "post": {
        "operationId": "generateSpeech",
        "summary": "Generate speech from text",
        "description": "Renders up to 500 characters (about 30 seconds) with one of the catalog voices and returns a hosted audio URL you can pass to createVideoFromAudio.",
        "tags": [
          "Audio"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TtsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audio generated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TtsResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing text, unknown voice id, or text over 500 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Upstream speech quota exhausted. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Speech generation is temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tts/voices": {
      "get": {
        "operationId": "listVoices",
        "summary": "List text-to-speech voices",
        "description": "Every voice generateSpeech accepts, each with a hosted sample clip you can play before choosing.",
        "tags": [
          "Audio"
        ],
        "responses": {
          "200": {
            "description": "Available voices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tiktok/extract-audio": {
      "post": {
        "operationId": "extractTikTokAudio",
        "summary": "Extract the audio from a TikTok video",
        "description": "Pulls the soundtrack off a public TikTok URL and stores it, returning a hosted audio URL for createVideoFromAudio.",
        "tags": [
          "Audio"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TikTokRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audio extracted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TikTokAudio"
                }
              }
            }
          },
          "400": {
            "description": "URL is missing, not a TikTok URL, or the video is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Firebase ID token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/email/unsubscribe": {
      "get": {
        "operationId": "unsubscribeFromEmails",
        "summary": "Unsubscribe from lifecycle email",
        "description": "Opts the holder of the token out of lifecycle email. Called from the unsubscribe link in an email.",
        "tags": [
          "Email"
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "The per-user token embedded in the unsubscribe link.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Opted out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnsubscribeResult"
                }
              }
            }
          },
          "400": {
            "description": "The token is missing or does not match a user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "unsubscribeFromEmailsOneClick",
        "summary": "One-click unsubscribe (RFC 8058)",
        "description": "The POST form mail clients use for one-click unsubscribe. Accepts the token in the query string or the body.",
        "tags": [
          "Email"
        ],
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The per-user unsubscribe token."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Opted out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnsubscribeResult"
                }
              }
            }
          },
          "400": {
            "description": "The token is missing or does not match a user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/versions": {
      "get": {
        "operationId": "listApiVersions",
        "summary": "Versions and deprecation policy",
        "description": "Every API version, its status and its deprecation and sunset dates, plus the policy describing what counts as a breaking change and how much notice one gets.",
        "tags": [
          "Meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Version list and policy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionsDocument"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates": {
      "get": {
        "operationId": "listApiTemplates",
        "summary": "List song templates",
        "description": "The songs a pet can be animated singing. Call this to get a template_id before creating a video. Requires an API key or OAuth token with the videos:read scope.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Every available template.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiTemplateList"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Read Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "getApiAccount",
        "summary": "Account and credit balance",
        "description": "The account behind the credential, its remaining API credits and the scopes the credential carries. Cheapest way to check a key works.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Account summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiAccount"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits": {
      "get": {
        "operationId": "getApiCredits",
        "summary": "Credit balance and recent ledger",
        "description": "The current prepaid credit balance plus the most recent ledger entries, so a caller can see what was bought, spent and refunded.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Balance and recent activity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiCredits"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/videos": {
      "get": {
        "operationId": "listApiVideos",
        "summary": "List videos",
        "description": "This account's videos, newest first, with their render status and download URLs. Covers videos made through the API and through the website alike.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many to return, 1 to 100. Defaults to 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account's videos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiVideoList"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createApiVideo",
        "summary": "Create a singing video",
        "description": "Queues a render of a pet photo singing the chosen song. Costs one credit. Returns 202 immediately; the render takes about a minute, so poll GET /v1/videos/{id} until status is completed. API renders are never watermarked.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiVideoRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Render queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedApiVideo"
                }
              }
            }
          },
          "400": {
            "description": "template_id or image_url is missing, malformed, or points somewhere we will not fetch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Not enough credits. Buy more before retrying.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks the videos:write scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Read Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/videos/{id}": {
      "get": {
        "operationId": "getApiVideo",
        "summary": "Get one video",
        "description": "The render status of a single video and, once it is completed, the URL to download it. This is the endpoint to poll after creating a video.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Video id returned by createApiVideo.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The video.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiVideo"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No video with that id belongs to this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "description": "Any client error, in the shared Error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteApiVideo",
        "summary": "Delete a video",
        "description": "Permanently deletes one of this account's videos. The credit it cost is not refunded, because the render already happened.",
        "tags": [
          "Public API"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Video id to delete.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "401": {
            "description": "The credential is missing, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks the videos:write scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No video with that id belongs to this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "callMcpServer",
        "summary": "MCP endpoint",
        "description": "Model Context Protocol server over Streamable HTTP. Speaks JSON-RPC 2.0, not REST, so it is described here for discovery rather than for generated clients. Authenticates with the same API key or OAuth token as the rest of the API. Tools and manifest at https://singing.pet/mcp.json.",
        "tags": [
          "MCP"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "oauth2": [
              "videos:read",
              "videos:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result, as JSON or as an SSE stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "401": {
            "description": "No usable credential. Carries WWW-Authenticate pointing at the protected-resource metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "firebaseIdToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "A Firebase Authentication ID token for project `singing-pet-d080d`, sent as `Authorization: Bearer <token>`. Sign in with Google or an email link at https://singing.pet to mint one."
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key, sent as `Authorization: Bearer sp_live_...`. Created at https://singing.pet/dashboard/api-keys and shown once. Keys cannot be self-provisioned by an agent."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 authorization code with PKCE (S256 only). Dynamic client registration is open at https://api.singing.pet/oauth/register.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://singing.pet/oauth/authorize",
            "tokenUrl": "https://api.singing.pet/oauth/token",
            "refreshUrl": "https://api.singing.pet/oauth/token",
            "scopes": {
              "videos:read": "Read templates, videos and the credit balance.",
              "videos:write": "Create and delete videos, spending credits."
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorDetail": {
        "type": "object",
        "description": "The structured form, returned by the shared error handler.",
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable code.",
            "examples": [
              "not_found"
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong, in English."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code."
          },
          "hint": {
            "type": "string",
            "description": "What to do about it."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "code",
          "message",
          "status",
          "hint",
          "documentation_url"
        ]
      },
      "Error": {
        "type": "object",
        "description": "Every failure is JSON, in one of two shapes.\n\nUnknown routes, malformed bodies and anything thrown past a controller go\nthrough the shared error handler, which returns `error` as an ErrorDetail\nobject and repeats the text in a top-level `message`.\n\nEndpoints that reject their own input, and the bearer-token check, predate\nthat handler and return a plain string in either `error` or `message`.\nRead `message` first, then `error`, and only treat `error` as an object when\nit is one.",
        "properties": {
          "error": {
            "description": "A plain message, or the structured detail.",
            "oneOf": [
              {
                "type": "string",
                "examples": [
                  "Unauthorized - Missing or invalid token format"
                ]
              },
              {
                "$ref": "#/components/schemas/ErrorDetail"
              }
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong. Always present on the shared envelope, where it copies error.message."
          }
        }
      },
      "ApiTemplate": {
        "type": "object",
        "description": "A song a pet can be animated singing.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Template id. A numeric string; send it back exactly as given rather than parsing it.",
            "examples": [
              "393"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "Dance Monkey(Cat Version)"
            ]
          },
          "preview_video_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Example clip of this template."
          },
          "preview_image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Animated still of this template."
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ApiTemplateList": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "list"
            ]
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiTemplate"
            }
          }
        },
        "required": [
          "object",
          "data"
        ]
      },
      "ApiVideo": {
        "type": "object",
        "description": "A render, finished or in progress.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "description": "Render state. Extensible: treat an unknown value as not-yet-terminal.",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "template_id": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "393"
            ]
          },
          "video_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Direct MP4 link. Null until status is completed."
          },
          "thumbnail_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "has_watermark": {
            "type": "boolean",
            "description": "Always false for videos created through the API."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "has_watermark",
          "created_at"
        ]
      },
      "ApiVideoList": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "list"
            ]
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiVideo"
            }
          }
        },
        "required": [
          "object",
          "data"
        ]
      },
      "CreateApiVideoRequest": {
        "type": "object",
        "properties": {
          "template_id": {
            "type": "string",
            "description": "Song to sing, from listApiTemplates.",
            "examples": [
              "393"
            ]
          },
          "image_url": {
            "type": "string",
            "format": "uri",
            "description": "Public HTTPS URL to a photo of the pet. JPEG, PNG, WebP or HEIC, up to 15MB. One pet facing the camera works best. Private and link-local hosts are refused.",
            "examples": [
              "https://example.com/dog.jpg"
            ]
          }
        },
        "required": [
          "template_id",
          "image_url"
        ]
      },
      "CreatedApiVideo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing"
            ]
          },
          "template_id": {
            "type": "string"
          },
          "credits_remaining": {
            "type": "integer",
            "description": "Balance after this render was charged."
          },
          "poll_url": {
            "type": "string",
            "format": "uri",
            "description": "Poll this until status is completed, roughly every 10 seconds."
          }
        },
        "required": [
          "id",
          "status",
          "credits_remaining",
          "poll_url"
        ]
      },
      "ApiAccount": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "credit_balance": {
            "type": "integer"
          },
          "credits_per_video": {
            "type": "integer",
            "examples": [
              1
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "user_id",
          "credit_balance",
          "credits_per_video",
          "scopes"
        ]
      },
      "CreditLedgerEntry": {
        "type": "object",
        "properties": {
          "delta": {
            "type": "integer",
            "description": "Positive for a purchase or refund, negative for a spend."
          },
          "reason": {
            "type": "string",
            "enum": [
              "purchase",
              "video_create",
              "refund",
              "grant"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "delta",
          "reason",
          "created_at"
        ]
      },
      "ApiCredits": {
        "type": "object",
        "properties": {
          "balance": {
            "type": "integer"
          },
          "credits_per_video": {
            "type": "integer",
            "examples": [
              1
            ]
          },
          "recent": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreditLedgerEntry"
            }
          }
        },
        "required": [
          "balance",
          "credits_per_video",
          "recent"
        ]
      },
      "ApiVersion": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "examples": [
              "v1"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "current",
              "deprecated",
              "sunset"
            ]
          },
          "base_url": {
            "type": "string",
            "format": "uri"
          },
          "released": {
            "type": "string",
            "format": "date"
          },
          "deprecated_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Null while the version is current."
          },
          "sunset_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "The day this version stops answering."
          }
        },
        "required": [
          "version",
          "status",
          "base_url",
          "released"
        ]
      },
      "VersionsDocument": {
        "type": "object",
        "properties": {
          "current": {
            "type": "string",
            "examples": [
              "v1"
            ]
          },
          "versions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiVersion"
            }
          },
          "policy": {
            "type": "object",
            "description": "What counts as breaking, and how a sunset is signaled.",
            "properties": {
              "scheme": {
                "type": "string",
                "enum": [
                  "url_path"
                ]
              },
              "description": {
                "type": "string"
              },
              "breaking_changes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "non_breaking_changes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "notice_period_days": {
                "type": "integer",
                "examples": [
                  180
                ]
              },
              "signals": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "documentation_url": {
                "type": "string",
                "format": "uri"
              }
            },
            "required": [
              "scheme",
              "notice_period_days",
              "signals"
            ]
          }
        },
        "required": [
          "current",
          "versions",
          "policy"
        ]
      },
      "JsonRpcRequest": {
        "type": "object",
        "description": "A JSON-RPC 2.0 call, as defined by the MCP specification.",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "method": {
            "type": "string",
            "examples": [
              "initialize",
              "tools/list",
              "tools/call"
            ]
          },
          "params": {
            "type": "object",
            "description": "Method-specific parameters."
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ]
      },
      "JsonRpcResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "result": {
            "type": "object",
            "description": "Present on success."
          },
          "error": {
            "type": "object",
            "description": "Present on failure.",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc"
        ]
      },
      "VoiceList": {
        "type": "object",
        "properties": {
          "voices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Voice"
            }
          }
        },
        "required": [
          "voices"
        ]
      },
      "DeleteVideoResult": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "examples": [
              "Video deleted"
            ]
          }
        },
        "required": [
          "message"
        ]
      },
      "UnsubscribeResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True once the address is opted out, whether or not it already was.",
            "examples": [
              true
            ]
          }
        },
        "required": [
          "ok"
        ]
      },
      "OpenApiDocument": {
        "type": "object",
        "description": "An OpenAPI 3.1 document. Shape is defined by the OpenAPI specification.",
        "properties": {
          "openapi": {
            "type": "string",
            "examples": [
              "3.1.0"
            ]
          },
          "info": {
            "type": "object"
          },
          "paths": {
            "type": "object"
          },
          "components": {
            "type": "object"
          }
        },
        "required": [
          "openapi",
          "info",
          "paths"
        ]
      },
      "ServiceIndex": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "Singing Pet API"
            ]
          },
          "description": {
            "type": "string"
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "llms_txt": {
            "type": "string",
            "format": "uri"
          },
          "websocket": {
            "type": "string",
            "description": "Realtime render-status socket."
          },
          "public_endpoints": {
            "type": "array",
            "description": "Endpoints callable without credentials.",
            "items": {
              "type": "string"
            }
          },
          "authentication": {
            "type": "string"
          },
          "contact": {
            "type": "string",
            "format": "email"
          }
        },
        "required": [
          "name",
          "documentation",
          "openapi",
          "public_endpoints"
        ]
      },
      "Template": {
        "type": "object",
        "description": "A song a pet can be animated to sing.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Template id, used by createVideoFromTemplate."
          },
          "name": {
            "type": "string",
            "examples": [
              "APT.(Kids Version)"
            ]
          },
          "example_video": {
            "type": "string",
            "description": "Preview clip, relative to https://assets.singing.pet."
          },
          "example_webp": {
            "type": "string",
            "description": "Animated still, relative to https://assets.singing.pet."
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "Pricing": {
        "type": "object",
        "description": "Plan prices in the caller's currency, in minor units.",
        "properties": {
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2.",
            "examples": [
              "GB"
            ]
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217, lowercase.",
            "examples": [
              "gbp"
            ]
          },
          "weekly": {
            "type": "integer",
            "description": "Weekly plan, minor units.",
            "examples": [
              949
            ]
          },
          "annual": {
            "type": "integer",
            "description": "Annual plan, minor units.",
            "examples": [
              2899
            ]
          },
          "unlock": {
            "type": "integer",
            "description": "One-off watermark removal, minor units.",
            "examples": [
              499
            ]
          }
        },
        "required": [
          "country",
          "currency",
          "weekly",
          "annual",
          "unlock"
        ]
      },
      "SubscriptionStatus": {
        "type": "object",
        "properties": {
          "isSubscribed": {
            "type": "boolean"
          },
          "subscriptionId": {
            "type": "string"
          },
          "currentPeriodEnd": {
            "type": "string",
            "format": "date-time"
          },
          "cancelAtPeriodEnd": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "description": "Stripe subscription status.",
            "examples": [
              "active"
            ]
          }
        },
        "required": [
          "isSubscribed"
        ]
      },
      "CheckoutRequest": {
        "type": "object",
        "properties": {
          "plan_type": {
            "type": "string",
            "description": "Which plan to buy.",
            "enum": [
              "weekly",
              "annual"
            ]
          },
          "checkout_source": {
            "type": "string",
            "description": "Where the checkout was started, for analytics."
          },
          "paywall_type": {
            "type": "string",
            "description": "Which paywall surface triggered it."
          },
          "page_path": {
            "type": "string"
          },
          "referral": {
            "type": "string",
            "description": "Affiliate referral id."
          },
          "utm_source": {
            "type": "string"
          },
          "utm_medium": {
            "type": "string"
          },
          "utm_campaign": {
            "type": "string"
          },
          "utm_content": {
            "type": "string"
          },
          "utm_term": {
            "type": "string"
          }
        },
        "required": [
          "plan_type"
        ]
      },
      "OneTimeCheckoutRequest": {
        "type": "object",
        "properties": {
          "video_id": {
            "type": "string",
            "format": "uuid",
            "description": "The watermarked video."
          },
          "checkout_source": {
            "type": "string"
          },
          "paywall_type": {
            "type": "string"
          }
        },
        "required": [
          "video_id"
        ]
      },
      "PortalRequest": {
        "type": "object",
        "properties": {
          "intent": {
            "type": "string",
            "description": "What the portal should open on, e.g. `cancel`."
          }
        }
      },
      "CheckoutSession": {
        "type": "object",
        "properties": {
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Where to send the buyer. Null when no payment was needed."
          },
          "rerendering": {
            "type": "boolean",
            "description": "True when an already-paid video was re-rendered instead of charged."
          }
        },
        "required": [
          "url"
        ]
      },
      "User": {
        "type": "object",
        "properties": {
          "isSubscribed": {
            "type": "boolean"
          },
          "creditsRemaining": {
            "type": "integer",
            "description": "Free videos left this calendar month."
          }
        },
        "required": [
          "isSubscribed",
          "creditsRemaining"
        ]
      },
      "Video": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "template_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "template_name": {
            "type": "string"
          },
          "video_url": {
            "type": "string",
            "description": "Playable MP4 once status is completed, empty string before that."
          },
          "thumbnail_url": {
            "type": "string"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "audio_file_path": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "completed",
              "failed"
            ]
          },
          "has_watermark": {
            "type": "boolean"
          },
          "is_locked": {
            "type": "boolean",
            "deprecated": true,
            "description": "Always false."
          },
          "can_remove_watermark": {
            "type": "boolean",
            "description": "Whether createOneTimeCheckout can re-render this one clean."
          }
        },
        "required": [
          "id",
          "status",
          "has_watermark"
        ]
      },
      "VideoPage": {
        "type": "object",
        "properties": {
          "videos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Video"
            }
          },
          "pagination": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "page": {
                "type": "integer"
              },
              "pageSize": {
                "type": "integer"
              },
              "totalPages": {
                "type": "integer"
              }
            },
            "required": [
              "total",
              "page",
              "pageSize",
              "totalPages"
            ]
          }
        },
        "required": [
          "videos",
          "pagination"
        ]
      },
      "CreateFromTemplateRequest": {
        "type": "object",
        "properties": {
          "image": {
            "type": "string",
            "format": "binary",
            "description": "The pet photo. One clear, front-facing face works best."
          },
          "templateId": {
            "type": "string",
            "description": "Template id from listTemplates."
          },
          "sourceType": {
            "type": "string",
            "description": "Analytics label for where the request came from."
          }
        },
        "required": [
          "image",
          "templateId"
        ]
      },
      "CreateFromAudioRequest": {
        "type": "object",
        "properties": {
          "image": {
            "type": "string",
            "format": "binary",
            "description": "The pet photo."
          },
          "audio": {
            "type": "string",
            "format": "binary",
            "description": "2 to 30 seconds of audio. MP3 and WAV pass through untouched."
          },
          "sourceType": {
            "type": "string",
            "description": "Analytics label, e.g. `upload`, `tts`, `tiktok`, `record`."
          }
        },
        "required": [
          "image",
          "audio"
        ]
      },
      "RenderQueued": {
        "type": "object",
        "description": "The render is queued, not finished. Poll listUserVideos for the result.",
        "properties": {
          "message": {
            "type": "string",
            "examples": [
              "Video processing started"
            ]
          },
          "runPodResponse": {
            "type": "object",
            "description": "Raw queue acknowledgement from the render backend.",
            "additionalProperties": true
          }
        },
        "required": [
          "message"
        ]
      },
      "DeleteVideoRequest": {
        "type": "object",
        "properties": {
          "videoId": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": [
          "videoId"
        ]
      },
      "TtsRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 500,
            "description": "What the pet should say."
          },
          "voiceId": {
            "type": "string",
            "description": "A voice id from listVoices."
          }
        },
        "required": [
          "text",
          "voiceId"
        ]
      },
      "TtsResult": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          },
          "audioUrl": {
            "type": "string",
            "format": "uri",
            "description": "Hosted audio clip."
          },
          "audioPath": {
            "type": "string"
          },
          "audioId": {
            "type": "string"
          },
          "voiceId": {
            "type": "string"
          },
          "voiceName": {
            "type": "string"
          }
        },
        "required": [
          "audioUrl"
        ]
      },
      "Voice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this as voiceId."
          },
          "name": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "ssmlGender": {
            "type": "string",
            "examples": [
              "FEMALE"
            ]
          },
          "sampleUrl": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "id",
          "displayName"
        ]
      },
      "TikTokRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A public TikTok video URL."
          }
        },
        "required": [
          "url"
        ]
      },
      "TikTokAudio": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "audioUrl": {
            "type": "string",
            "format": "uri"
          },
          "metadata": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "duration": {
                "type": "number",
                "description": "Seconds."
              },
              "uploader": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "success",
          "audioUrl"
        ]
      }
    }
  }
}
