{
  "openapi": "3.1.0",
  "info": {
    "title": "Clickframe API",
    "version": "1.0.0",
    "description": "Clickframe makes YouTube thumbnails (and square or portrait social posts) with AI. This API is the studio's own `/v1` API opened to API keys: thumbnail concepts, AI images in four formats, recreating a reference thumbnail's style, AI edits (at a spot, inside a painted area, expressions, text), AI scores, click-test estimates, titles and descriptions.\n\nIt uses the account's normal credits, with the same limits and safety checks as the app: 1 credit per image or AI edit (and per click-test version); concepts, scores, titles and descriptions are free. Credits never expire.\n\nImages are asynchronous: a request charges and queues them and answers 202. Poll the `Location` it returns (`GET /v1/generations/{id}`) every 2-3 seconds until `status` is `succeeded` or `failed` (a failed image's credit is refunded automatically). There are no webhooks. Identical image requests within 2 minutes return the same images without charging again.\n\nRequest bodies are JSON (at most 64 KB); unknown fields are refused. Errors are `{\"error\": code, \"message\": text}` with an HTTP status. AI assistants should use the MCP server (`POST /mcp`, OAuth) instead of a key.\n\nDocumentation: https://clickframe.norvane-studio.com/api\n\nRate limits: every answer to an API key carries `RateLimit-Policy` and `RateLimit` headers (IETF draft-ietf-httpapi-ratelimit-headers) with what is left of the key's hourly quota of 1,200 requests. A 429 names the limit that was used up in the same headers and says in `Retry-After` how many seconds to wait.",
    "termsOfService": "https://clickframe.norvane-studio.com/terms",
    "contact": {
      "name": "Clickframe support",
      "url": "https://clickframe.norvane-studio.com/support",
      "email": "norvane.studio@gmail.com"
    }
  },
  "externalDocs": {
    "description": "Clickframe API documentation",
    "url": "https://clickframe.norvane-studio.com/api"
  },
  "servers": [
    {
      "url": "https://clickframe-api.norvane-studio.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Projects",
      "description": "One project per video, with its concepts."
    },
    {
      "name": "Images",
      "description": "Rendering images (1 credit each) and reading them back."
    },
    {
      "name": "Edits",
      "description": "AI edits of an image (1 credit each)."
    },
    {
      "name": "Scores and text",
      "description": "AI scores, click tests, titles and descriptions."
    },
    {
      "name": "YouTube",
      "description": "Public YouTube video lookups."
    },
    {
      "name": "Personas and faces",
      "description": "Your face in images, and reaction face packs."
    },
    {
      "name": "Designs",
      "description": "Designs saved in Clickframe's editor."
    },
    {
      "name": "Files",
      "description": "Uploads straight to storage through presigned URLs."
    },
    {
      "name": "Account",
      "description": "The account behind the key."
    },
    {
      "name": "Catalog",
      "description": "Public information: options, prices and health."
    },
    {
      "name": "MCP",
      "description": "The Model Context Protocol server for AI apps."
    }
  ],
  "paths": {
    "/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Get the account",
        "description": "The account behind the key: credits, plan, and whether the email is confirmed. Free.",
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "user"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key, unauthorized, invalid_token, or account_gone: the account no longer exists."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      }
    },
    "/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List projects",
        "description": "Your projects, newest activity first (at most 100, without their concepts, titles or description), each with its cover, image counts and best score; the 6 latest saved designs (without their layers); and totals. Free.",
        "responses": {
          "200": {
            "description": "Projects, recent designs and totals.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      },
                      "description": "Projects (with `coverUrl` and the counts; `concepts` null)."
                    },
                    "recentDesigns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Design"
                      },
                      "description": "The 6 latest saved designs, `layers` null."
                    },
                    "totals": {
                      "type": "object",
                      "properties": {
                        "projects": {
                          "type": "integer",
                          "description": "Projects."
                        },
                        "images": {
                          "type": "integer",
                          "description": "Succeeded images (reaction faces excluded)."
                        },
                        "designs": {
                          "type": "integer",
                          "description": "Saved designs."
                        }
                      },
                      "required": [
                        "projects",
                        "images",
                        "designs"
                      ]
                    }
                  },
                  "required": [
                    "projects",
                    "recentDesigns",
                    "totals"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "tags": [
          "Projects"
        ],
        "summary": "Create a project with 4 concepts",
        "description": "Creates a project for one video from a title or a YouTube link and writes 4 thumbnail concepts for it with `mistral-small-latest` (hook, emotion, scene, composition, headline, where to leave room for text). Free; 40 concept requests an hour per account (shared with `moreConcepts`). The text is moderated first. If the concept model fails, the project is still created, without concepts, and `conceptsError` says so: call `moreConcepts` to retry.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The project, with its concepts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/Project"
                    },
                    "conceptsError": {
                      "type": "string",
                      "description": "Present when the concepts couldn't be written: the project has none yet."
                    }
                  },
                  "required": [
                    "project"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request (invalid JSON, unknown field, invalid personaId), invalid_youtube_url, title_required (under 3 characters), unknown_persona."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "video_not_found: the YouTube video isn't public or doesn't exist."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 40 concept requests an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the project couldn't be saved."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "youtube_error: YouTube couldn't be reached."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_unavailable."
          }
        }
      }
    },
    "/v1/projects/{id}": {
      "get": {
        "operationId": "getProject",
        "tags": [
          "Projects"
        ],
        "summary": "Get a project",
        "description": "One project with its concepts, kept titles and description, its images (newest first, at most 100, reaction faces excluded) and its saved designs (at most 100). Free.",
        "responses": {
          "200": {
            "description": "The project, its images and designs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/Project"
                    },
                    "generations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Generation"
                      },
                      "description": "Its images, newest first."
                    },
                    "designs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Design"
                      },
                      "description": "Its saved designs, most recently saved first."
                    }
                  },
                  "required": [
                    "project",
                    "generations",
                    "designs"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      },
      "delete": {
        "operationId": "deleteProject",
        "tags": [
          "Projects"
        ],
        "summary": "Delete a project",
        "description": "Deletes the project with its images, references and designs, and their stored files. Irreversible. Free.",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "deleted"
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/projects/{id}/concepts": {
      "post": {
        "operationId": "moreConcepts",
        "tags": [
          "Projects"
        ],
        "summary": "Write 4 more concepts",
        "description": "Adds 4 new concepts to the project, different from its existing headlines, using its title, description, script, layout and faceless setting. The project keeps its latest 24 concepts. No request body. Free; 40 concept requests an hour per account (shared with `createProject`).",
        "responses": {
          "200": {
            "description": "The updated project and the new concepts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/Project"
                    },
                    "added": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Concept"
                      },
                      "description": "The 4 new concepts."
                    }
                  },
                  "required": [
                    "project",
                    "added"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 40 concept requests an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the concepts couldn't be saved."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "ai_error."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_unavailable, ai_busy."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/generations": {
      "get": {
        "operationId": "listGenerations",
        "tags": [
          "Images"
        ],
        "summary": "List images",
        "description": "Your latest images, newest first (at most 60; reaction faces excluded), optionally for one project. Free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Images.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Generation"
                      },
                      "description": "Newest first."
                    }
                  },
                  "required": [
                    "generations"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request: invalid projectId."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      },
      "post": {
        "operationId": "createGeneration",
        "tags": [
          "Images"
        ],
        "summary": "Render a concept as an image",
        "description": "Charges 1 credit per image and queues it for `gemini-3.1-flash-lite-image`; answers 202 at once. Poll `getGeneration` every 2-3 seconds until `status` is `succeeded` (most take about 10 seconds) or `failed` (the credit is refunded automatically). On a paid plan one request can queue 1-4 images (`count` takes of one style, or `styles`: one per style); the free plan makes one at a time. A batch is refused up front when the balance can't cover it. An identical request within 2 minutes returns the same images without charging (`deduped`). The scene, headline and extra text are rewritten if they look likely to be refused by the image model's safety filter, then moderated. 30 images per 10 minutes per account; a platform-wide daily cap applies.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateGenerationRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedGenerations"
                }
              }
            },
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request (invalid JSON, unknown field, invalid projectId or personaId), bad_aspect, persona_and_no_person, scene_required (under 10 characters), unknown_persona."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "plan_required: more than one image per request on the free plan; verify_email: no credits and the email isn't confirmed (the free credits come with confirmation)."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: the project."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 30 images per 10 minutes per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the image couldn't be queued."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity: today's platform-wide image capacity is reached."
          }
        }
      }
    },
    "/v1/generations/{id}": {
      "get": {
        "operationId": "getGeneration",
        "tags": [
          "Images"
        ],
        "summary": "Get an image (poll this)",
        "description": "An image's status and details. Poll every 2-3 seconds after queuing until `status` is `succeeded` (then `imageUrl` is a 15-minute link) or `failed` (`error` says why; the credit was refunded). With textMode `image`, `textOk` says whether the drawn headline is spelled right. Works for reaction faces too. Free.",
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generation": {
                      "$ref": "#/components/schemas/Generation"
                    }
                  },
                  "required": [
                    "generation"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/generations/{id}/download": {
      "get": {
        "operationId": "getGenerationDownload",
        "tags": [
          "Images"
        ],
        "summary": "Get a download link",
        "description": "A presigned URL (valid 15 minutes) that downloads the full-size image as `clickframe-<id>.png|.jpg`. Unless the account turned the AI label on, the file is a copy without metadata (EXIF, XMP, C2PA; pixels untouched). Free.",
        "responses": {
          "200": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloadUrl": {
                      "type": "string",
                      "description": "Presigned download URL, valid 15 minutes.",
                      "format": "uri"
                    }
                  },
                  "required": [
                    "downloadUrl"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: no such image, or it has no file yet."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "storage_error."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/generations/{id}/versions": {
      "get": {
        "operationId": "listGenerationVersions",
        "tags": [
          "Images"
        ],
        "summary": "List an image's versions",
        "description": "The whole edit chain the image belongs to: the first image and every edit made from it (and from those edits), oldest first, failed attempts left out, at most 60. Free.",
        "responses": {
          "200": {
            "description": "The versions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Generation"
                      },
                      "description": "Oldest first."
                    }
                  },
                  "required": [
                    "versions"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/generations/{id}/edit": {
      "post": {
        "operationId": "editGeneration",
        "tags": [
          "Edits"
        ],
        "summary": "Edit an image with AI",
        "description": "Makes a new version of a succeeded image: 1 credit, queued like `createGeneration` (answers 202; poll `getGeneration`). Send one of: `{instruction}`; `{instruction, point}` (at a spot); `{instruction, mask}` (inside a painted area; the rest of the picture comes back exactly); `{expression}` (change the face's expression; our own prompt, so no moderation); `{text: {words, style}}` (paint new headline words); `{removeText: true}`; `{fixes}` (an AI score's fixes). Free-form instructions are rewritten for the image model's safety filter if needed and moderated. If several are sent, text/removeText wins, then expression, then fixes, then the instruction (a mask wins over a point). An identical edit within 2 minutes returns the same image without charging. 30 images per 10 minutes per account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedGenerations"
                }
              }
            },
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, bad_point (not 0-1), unknown_expression, fixes_text_only (only text advice: change the text in the editor), instruction_required (under 3 characters), bad_mask (empty, over 300 strokes or 6,000 points, a brush outside 0.002-0.3, points outside 0-1, too detailed, under 0.1% or over 90% of the picture), words_required."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email (no credits, email not confirmed); not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: no such image, or it hasn't succeeded."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 30 images per 10 minutes per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the edit couldn't be queued."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/recreate": {
      "post": {
        "operationId": "recreateThumbnail",
        "tags": [
          "Images"
        ],
        "summary": "Recreate a reference thumbnail's style",
        "description": "One new image (1 credit, kind `recreate`) for the project's video that follows a reference thumbnail's layout, colours and lighting (`match: layout`) or only its colours, lighting and finish (`match: style`), never its people, logos, brands or words. The reference is an upload of yours or a public YouTube video's current thumbnail; it is scaled to at most 1,024 px and kept with the image. Answers 202; poll `getGeneration`. The subject is rewritten for the safety filter if needed, and moderated with the headline. 30 recreates an hour per account, which also count towards 30 images per 10 minutes. An identical request within 2 minutes returns the same image without charging.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecreateRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedGenerations"
                }
              }
            },
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, bad_aspect, persona_and_no_person, unknown_persona, invalid_youtube_url, video_not_found (here a 400), reference_required, bad_reference (not a JPG or PNG of 64 to 8,000 px a side)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "forbidden (the key isn't a confirmed upload of yours), verify_email (no credits, email not confirmed)."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: the project."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 30 recreates an hour or 30 images per 10 minutes per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the image couldn't be queued."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "storage_error, youtube_error (the video's thumbnail couldn't be downloaded)."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity."
          }
        }
      }
    },
    "/v1/score": {
      "post": {
        "operationId": "scoreThumbnail",
        "tags": [
          "Scores and text"
        ],
        "summary": "Score a thumbnail with AI",
        "description": "An AI critique of a thumbnail (`mistral-medium-latest`): overall 0-100, six pillars with a checklist, strengths and concrete fixes. Score a Clickframe image (`generationId`, optionally with your upload of it composed with its headline as `key`), a saved design (`designId`), a project video's current YouTube thumbnail (`projectId` + `original: true`), an upload (`key`) or any public video (`youtubeUrl`). Free, for confirmed emails. New critiques are capped per account and UTC day: 5 on the free plan, 30 on Creator and Pro; cached scores (`cached: true`) and an identical image and title scored recently cost nothing. The project's title is the default title.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoreRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The score.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "score": {
                      "$ref": "#/components/schemas/Score"
                    },
                    "cached": {
                      "type": "boolean",
                      "const": true,
                      "description": "Present (true) when the stored score was returned without a new critique."
                    }
                  },
                  "required": [
                    "score"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, no_video (the project wasn't made from a YouTube link), invalid_youtube_url, image_required (no source given)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key, unauthorized, invalid_token, or account_gone."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email, forbidden (the key isn't a confirmed upload of yours)."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: the image, design or project (or the design has no export)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "score_limit: today's new AI scores are used up (5 free, 30 on paid plans; resets at midnight UTC); or rate_limited by the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "ai_error, youtube_error, storage_error."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_unavailable, ai_busy."
          }
        }
      }
    },
    "/v1/click-test": {
      "post": {
        "operationId": "clickTest",
        "tags": [
          "Scores and text"
        ],
        "summary": "Estimate clicks against competitors",
        "description": "Creator and Pro plans, confirmed email. Puts 1-3 versions of your thumbnail, with your title, among 2-7 competing public videos (their current thumbnails are fetched from YouTube) and has `mistral-medium-latest` play 100 viewers sharing 100 clicks, once per rotation of the list, averaged (about ±1.5 points). Each version is tested separately against the same competitors. 1 credit per version tested afresh, charged after the inputs and thumbnails check out and refunded for versions the model fails on; identical tests within 24 hours are answered from memory, free. 20 fresh tests an hour per account. An AI estimate of packaging only: not YouTube data and not a CTR.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClickTestRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The estimate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClickTestResult"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request (not 1-3 versions, a version without exactly one of key or generationId, an invalid or repeated videoId), competitors_required (not 2-7), title_required, bad_image."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key, unauthorized, invalid_token, or account_gone."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits: the balance can't cover every new version."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "plan_required (free plan), verify_email, forbidden (a key that isn't a confirmed upload of yours)."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: a generationId."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 20 fresh tests an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the credits couldn't be charged."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "storage_error, youtube_error (fewer than 2 competitors' thumbnails loaded), ai_error (failed versions are refunded)."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_unavailable, ai_busy."
          }
        }
      }
    },
    "/v1/titles": {
      "post": {
        "operationId": "suggestTitles",
        "tags": [
          "Scores and text"
        ],
        "summary": "Suggest video titles",
        "description": "About 8 honest, clickable title ideas from different angles (`ministral-8b-latest`), each under 70 characters. With `projectId` they are kept on the project (`titleIdeas`). Free, for confirmed emails; 40 text requests an hour per account (shared with `writeDescription`). The text is moderated first.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TitlesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Title ideas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "titles": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "$ref": "#/components/schemas/TitleIdea"
                      },
                      "description": "The ideas."
                    }
                  },
                  "required": [
                    "titles"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, topic_required (under 3 characters)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key, unauthorized, invalid_token, or account_gone."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: the project."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 40 text requests an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "ai_error."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_unavailable, ai_busy."
          }
        }
      }
    },
    "/v1/description": {
      "post": {
        "operationId": "writeDescription",
        "tags": [
          "Scores and text"
        ],
        "summary": "Write a video description",
        "description": "A ready-to-paste YouTube description, up to 3 hashtags and search tags (`mistral-small-latest`). The model writes only the hook, body and call to action, in the first person, without invented results; your links and chapters are pasted in verbatim. With `projectId` it is kept on the project (`descriptionDraft`). Free, for confirmed emails; 40 text requests an hour per account (shared with `suggestTitles`). The text is moderated first.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DescriptionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DescriptionResult"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, title_required (under 3 characters)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key, unauthorized, invalid_token, or account_gone."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: the project."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 40 text requests an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "ai_error."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_unavailable, ai_busy."
          }
        }
      }
    },
    "/v1/youtube/lookup": {
      "post": {
        "operationId": "youtubeLookup",
        "tags": [
          "YouTube"
        ],
        "summary": "Look up a YouTube video",
        "description": "A public video's title, channel and thumbnail URL (YouTube oEmbed: no category), plus a niche from the catalog's list detected by `ministral-8b-latest` (empty when unclear). Free; 60 lookups an hour per account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LookupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The video.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "video": {
                      "$ref": "#/components/schemas/Video"
                    },
                    "niche": {
                      "type": "string",
                      "description": "A niche from the catalog's `niches`, or empty."
                    }
                  },
                  "required": [
                    "video",
                    "niche"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, invalid_youtube_url."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "video_not_found: private, deleted or nonexistent."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 60 lookups an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "youtube_error."
          }
        }
      }
    },
    "/v1/personas": {
      "get": {
        "operationId": "listPersonas",
        "tags": [
          "Personas and faces"
        ],
        "summary": "List personas",
        "description": "Your personas (sets of 1-4 photos of your face, created in the app), newest first, usable as `personaId`. Free.",
        "responses": {
          "200": {
            "description": "Personas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "personas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Persona"
                      },
                      "description": "Newest first."
                    },
                    "max": {
                      "type": "integer",
                      "description": "Most personas an account can have (3)."
                    }
                  },
                  "required": [
                    "personas",
                    "max"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      }
    },
    "/v1/personas/{id}/faces": {
      "post": {
        "operationId": "createFaces",
        "tags": [
          "Personas and faces"
        ],
        "summary": "Make a reaction face pack",
        "description": "Square portraits of the persona, one per expression, on a plain light grey background (kind `face`, aspect 1:1, no project): 1 credit each, 1-6 per request. Answers 202; poll each with `getGeneration`. Several faces are refused up front when the balance can't cover them. They count towards 30 images per 10 minutes per account. An identical face within 2 minutes is returned without charging.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FacesRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedGenerations"
                }
              }
            },
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request, unknown_expression, expressions_required (not 1-6)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email (no credits, email not confirmed); not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: the persona."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 30 images per 10 minutes per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: the faces couldn't be queued."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/faces": {
      "get": {
        "operationId": "listFaces",
        "tags": [
          "Personas and faces"
        ],
        "summary": "List reaction faces",
        "description": "Your reaction faces (kind `face`), newest first, at most 120. Free.",
        "responses": {
          "200": {
            "description": "Faces.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "faces": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Generation"
                      },
                      "description": "Newest first."
                    }
                  },
                  "required": [
                    "faces"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      }
    },
    "/v1/designs": {
      "get": {
        "operationId": "listDesigns",
        "tags": [
          "Designs"
        ],
        "summary": "List saved designs",
        "description": "Designs saved in Clickframe's editor, most recently saved first (at most 100), optionally for one project, with their documents and a link to their last export. Free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Designs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "designs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Design"
                      },
                      "description": "Most recently saved first."
                    }
                  },
                  "required": [
                    "designs"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request: invalid projectId."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      }
    },
    "/v1/designs/{id}": {
      "get": {
        "operationId": "getDesign",
        "tags": [
          "Designs"
        ],
        "summary": "Get a saved design",
        "description": "One saved design with its editor document and a link to its last export. Free.",
        "responses": {
          "200": {
            "description": "The design.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "design": {
                      "$ref": "#/components/schemas/Design"
                    }
                  },
                  "required": [
                    "design"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "not_available_with_api_key: with an API key, the id in the path must be a lowercase UUID; any other id is refused by the key check."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/files/upload-url": {
      "post": {
        "operationId": "createUploadUrl",
        "tags": [
          "Files"
        ],
        "summary": "Get an upload URL",
        "description": "Step 1 of an upload (a reference to recreate, a thumbnail to score or click-test). Returns a presigned URL: PUT the bytes there with the same Content-Type and exactly `sizeBytes` bytes (the size is signed into the URL), then call `confirmUpload`. Unconfirmed uploads are deleted after a day. Free; 120 upload URLs an hour per account; 2 GB of uploads per account (100 MB until the email is confirmed).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadUrlRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where to PUT the file.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "The object key: pass it to `confirmUpload`, then as `key` elsewhere."
                    },
                    "uploadUrl": {
                      "type": "string",
                      "description": "Presigned PUT URL, valid until `expiresAt` (15 minutes).",
                      "format": "uri"
                    },
                    "method": {
                      "type": "string",
                      "const": "PUT"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the URL expires."
                    },
                    "headers": {
                      "type": "object",
                      "description": "Headers to send with the PUT.",
                      "properties": {
                        "Content-Type": {
                          "type": "string",
                          "description": "The contentType you declared."
                        }
                      },
                      "required": [
                        "Content-Type"
                      ]
                    }
                  },
                  "required": [
                    "key",
                    "uploadUrl",
                    "method",
                    "expiresAt",
                    "headers"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request: invalid JSON, a contentType other than image/png, image/jpeg or image/webp, or a missing sizeBytes."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key, unauthorized, invalid_token, or account_gone."
          },
          "413": {
            "$ref": "#/components/responses/ContentTooLarge",
            "description": "too_large (over 10 MB), upload_quota."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: 120 upload URLs an hour per account, or the per-key limit."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "storage_error."
          }
        }
      }
    },
    "/v1/files/confirm": {
      "post": {
        "operationId": "confirmUpload",
        "tags": [
          "Files"
        ],
        "summary": "Confirm an upload",
        "description": "Step 2 of an upload: checks the object exists in storage and marks it uploaded. Only confirmed uploads can be used as `key` elsewhere. Free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "The key."
                    },
                    "sizeBytes": {
                      "type": "integer",
                      "description": "The stored size in bytes."
                    },
                    "status": {
                      "type": "string",
                      "const": "uploaded"
                    }
                  },
                  "required": [
                    "key",
                    "sizeBytes",
                    "status"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request: key is required."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "forbidden: not your file."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_uploaded: the object isn't in storage (the PUT didn't happen or failed)."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          }
        }
      }
    },
    "/v1/files/download-url": {
      "post": {
        "operationId": "createDownloadUrl",
        "tags": [
          "Files"
        ],
        "summary": "Get a download URL for an upload",
        "description": "A presigned GET URL (15 minutes) for one of your uploaded files. Free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "The key."
                    },
                    "downloadUrl": {
                      "type": "string",
                      "description": "Presigned GET URL.",
                      "format": "uri"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When it expires."
                    }
                  },
                  "required": [
                    "key",
                    "downloadUrl",
                    "expiresAt"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request: key is required."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "forbidden: not your file."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found: unknown file."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests",
            "description": "rate_limited: more than 1,200 requests in an hour with this API key (Retry-After: seconds until the window resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error (the API key lookup included)."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "storage_error."
          }
        }
      }
    },
    "/public/catalog": {
      "get": {
        "operationId": "getCatalog",
        "tags": [
          "Catalog"
        ],
        "summary": "List the options",
        "description": "The options the API accepts (styles, formats, layouts, niches, text styles, expressions, recreate matches), what actions cost, and the paid plans. No authentication; cached for 5 minutes.",
        "security": [],
        "responses": {
          "200": {
            "description": "The catalog.",
            "headers": {
              "Cache-Control": {
                "description": "`public, max-age=300`.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Catalog"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "health",
        "tags": [
          "Catalog"
        ],
        "summary": "Health check",
        "description": "Liveness and database reachability. No authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "The database is unreachable (`status: degraded`, `db: down`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "tags": [
          "MCP"
        ],
        "summary": "Model Context Protocol endpoint",
        "description": "The remote MCP server for AI apps (Claude, ChatGPT, Claude Code, Cursor...): point an MCP client at `https://clickframe-api.norvane-studio.com/mcp` rather than calling it directly (setup: https://clickframe.norvane-studio.com/integrations). Streamable HTTP carrying JSON-RPC 2.0; every answer is a single JSON response (no SSE, no session id); GET and DELETE answer 405. It serves modern clients (protocol 2026-07-28: version and client capabilities in `params._meta`, mirrored in the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers, which must match) and legacy ones that start with `initialize` (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05; JSON-RPC batches of up to 20 messages). Methods: `initialize`, `server/discover`, `ping`, `tools/list`, `tools/call`. Tools: create_concepts, more_concepts, generate_thumbnail, recreate_thumbnail, get_thumbnail, edit_thumbnail, score_thumbnail, suggest_titles, list_projects, list_personas, get_account. Each tool calls the same `/v1` handlers as the account, so credits, limits, moderation and refunds are identical; image tools wait up to 45 seconds and return a JPEG preview (at most 768 px), an editor link and a 15-minute download link. Authenticates with an OAuth access token (not an API key). Requests up to 1 MB.",
        "security": [
          {
            "oauth2": [
              "thumbnails"
            ]
          }
        ],
        "parameters": [
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "required": false,
            "description": "The protocol version; for 2026-07-28 it must equal `_meta`'s.",
            "schema": {
              "type": "string",
              "enum": [
                "2026-07-28",
                "2025-11-25",
                "2025-06-18",
                "2025-03-26",
                "2024-11-05"
              ]
            }
          },
          {
            "name": "Mcp-Method",
            "in": "header",
            "required": false,
            "description": "Protocol 2026-07-28: the JSON-RPC method, mirrored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Mcp-Name",
            "in": "header",
            "required": false,
            "description": "Protocol 2026-07-28, `tools/call`: the tool name, mirrored (may be `=?base64?...?=` encoded).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "A JSON-RPC 2.0 request or notification (or, for protocol 2025-03-26, a batch of up to 20).",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/JsonRpcRequest"
                  },
                  {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/JsonRpcRequest"
                    },
                    "minItems": 1,
                    "maxItems": 20
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The JSON-RPC response (an array for a batch). Tool failures come back as results with `isError: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/JsonRpcResponse"
                    },
                    {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JsonRpcResponse"
                      }
                    }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Accepted: the message was a notification (or a batch of them); no body."
          },
          "400": {
            "description": "A JSON-RPC error: parse error, invalid request (`jsonrpc` not \"2.0\", unsupported MCP-Protocol-Version), invalid params, header mismatch (-32020) or unsupported protocol version (-32022).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "401": {
            "description": "No valid access token: `{\"error\": \"unauthorized\", \"message\": \"Connect your Clickframe account: ...\"}`. Follow WWW-Authenticate to the OAuth metadata.",
            "headers": {
              "WWW-Authenticate": {
                "description": "`Bearer resource_metadata=\"https://clickframe-api.norvane-studio.com/.well-known/oauth-protected-resource/mcp\", scope=\"thumbnails\"`, plus `, error=\"invalid_token\", error_description=\"The access token is missing, expired or revoked\"` when a token was sent but is unknown or expired.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A JSON-RPC error: Origin not allowed (a browser origin that is neither https nor localhost).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "404": {
            "description": "Protocol 2026-07-28: method not found (-32601). Legacy clients get it with status 200.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "413": {
            "description": "A JSON-RPC error: request too large (over 1 MB).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "503": {
            "description": "A JSON-RPC error: temporarily unavailable (the token couldn't be checked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cf_live_<32 characters>",
        "description": "An API key, `Authorization: Bearer cf_live_` followed by 32 base-62 characters. Create keys on the website under Account → API keys (a confirmed email is needed; up to 5 keys); the secret is shown once. A key acts as its account and spends its credits; 1,200 requests an hour per key, polling included. Keys can't reach account, billing, API-key or connected-app routes (403 not_available_with_api_key). Keep keys on a server."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 for AI apps connecting over MCP. Authorization code with PKCE (S256 required); dynamic client registration at `https://clickframe-api.norvane-studio.com/oauth/register`; metadata at `https://clickframe-api.norvane-studio.com/.well-known/oauth-authorization-server` and `https://clickframe-api.norvane-studio.com/.well-known/oauth-protected-resource/mcp`. Access tokens last an hour; refresh tokens rotate. These tokens are accepted on `/mcp` only, not on `/v1`.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://clickframe-api.norvane-studio.com/oauth/authorize",
            "tokenUrl": "https://clickframe-api.norvane-studio.com/oauth/token",
            "refreshUrl": "https://clickframe-api.norvane-studio.com/oauth/token",
            "scopes": {
              "thumbnails": "Use Clickframe as the account through the MCP tools: write concepts and titles and score thumbnails (free), make and edit images with the account's credits (1 credit per image), and see its projects, images, personas and credits. No access to the password, payment details or account settings, and nothing can be deleted."
            }
          }
        }
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The resource's id: a lowercase UUID.",
        "schema": {
          "type": "string",
          "format": "uuid",
          "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
        }
      },
      "ProjectIdQuery": {
        "name": "projectId",
        "in": "query",
        "required": false,
        "description": "Only this project's items.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error from the API: an HTTP status plus this JSON body.",
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable machine-readable code (e.g. `no_credits`, `verify_email`, `rate_limited`). Branch on this."
          },
          "message": {
            "type": "string",
            "description": "Human-readable text, suitable to show to a person. It may change; don't parse it."
          },
          "docs": {
            "type": "string",
            "description": "On some errors (missing or invalid credentials, unknown address or method): a link to the relevant part of the docs.",
            "format": "uri"
          }
        },
        "required": [
          "error",
          "message"
        ]
      },
      "Aspect": {
        "type": "string",
        "enum": [
          "16:9",
          "9:16",
          "1:1",
          "4:5"
        ],
        "description": "Format: 16:9 YouTube thumbnail, 9:16 Shorts cover, 1:1 square post, 4:5 portrait post."
      },
      "Style": {
        "type": "string",
        "enum": [
          "realistic",
          "bold",
          "cinematic",
          "3d",
          "illustration",
          "gaming",
          "minimal"
        ],
        "description": "Image style preset (`ai.Styles`). Unknown values fall back to `realistic`."
      },
      "Layout": {
        "type": "string",
        "enum": [
          "",
          "center",
          "reaction",
          "pointing",
          "object",
          "before_after",
          "versus",
          "closeup",
          "lineup",
          "mystery",
          "scene"
        ],
        "description": "A named composition (`ai.Layouts`); `\"\"` lets the model choose. `reaction`, `pointing` and `object` are built around a person and are dropped for faceless projects and `noPerson` images. Unknown values are treated as `\"\"`."
      },
      "TextMode": {
        "type": "string",
        "enum": [
          "layer",
          "image"
        ],
        "description": "Who draws the headline. `layer` (default): the image has no text and keeps room for it; the editor typesets it. `image`: the image model paints the headline and Clickframe reads it back to check the spelling (`textRead`, `textOk`). Unknown values mean `layer`; without a headline the mode is always `layer`."
      },
      "TextStyle": {
        "type": "string",
        "enum": [
          "auto",
          "bold",
          "3d",
          "behind",
          "kicker",
          "glow",
          "banner",
          "gold"
        ],
        "description": "Look of AI-drawn text (`ai.TextStyles`), used with textMode `image`. Unknown values fall back to `auto`."
      },
      "Expression": {
        "type": "string",
        "enum": [
          "shocked",
          "excited",
          "happy",
          "laughing",
          "curious",
          "confused",
          "worried",
          "scared",
          "angry",
          "disappointed",
          "confident",
          "determined"
        ],
        "description": "A facial expression id (`ai.Expressions`)."
      },
      "Concept": {
        "type": "object",
        "description": "A thumbnail concept. A recreate's concept has only `headline` and `scene` filled (the other fields are empty, `palette` null).",
        "properties": {
          "id": {
            "type": "string",
            "description": "Concept id within its project (`c1`, `c2`, ...)."
          },
          "name": {
            "type": "string",
            "description": "Short name of the idea."
          },
          "hook": {
            "type": "string",
            "description": "The curiosity hook: why someone would click."
          },
          "emotion": {
            "type": "string",
            "description": "The emotion it plays on (lower case)."
          },
          "headline": {
            "type": "string",
            "description": "The headline, upper case, at most 5 words and 40 characters (may be empty)."
          },
          "scene": {
            "type": "string",
            "description": "What the picture shows: the image prompt's core."
          },
          "composition": {
            "type": "string",
            "description": "How the frame is arranged."
          },
          "textPosition": {
            "type": "string",
            "enum": [
              "left",
              "right",
              "top",
              "bottom"
            ],
            "description": "Where the frame leaves room for the headline."
          },
          "palette": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "pattern": "^#[0-9A-F]{6}$"
            },
            "maxItems": 4,
            "description": "Up to 4 hex colours."
          },
          "why": {
            "type": "string",
            "description": "Why it should work."
          },
          "hasPerson": {
            "type": "boolean",
            "description": "Whether the scene has a person in it."
          }
        },
        "required": [
          "id",
          "name",
          "hook",
          "emotion",
          "headline",
          "scene",
          "composition",
          "textPosition",
          "palette",
          "why",
          "hasPerson"
        ]
      },
      "ConceptInput": {
        "type": "object",
        "description": "A concept to render: one of the project's `concepts` as returned, or your own `{scene, headline}`. Unknown fields are refused.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Optional: the concept's id, if it came from the project."
          },
          "name": {
            "type": "string",
            "description": "Optional short name (cut to 60 characters).",
            "maxLength": 60
          },
          "hook": {
            "type": "string",
            "description": "Optional hook (cut to 240 characters).",
            "maxLength": 240
          },
          "emotion": {
            "type": "string",
            "description": "Optional emotion (cut to 30 characters).",
            "maxLength": 30
          },
          "headline": {
            "type": "string",
            "description": "The headline. Upper-cased and cut to 5 words and 40 characters. Leave it empty for an image without a headline (textMode is then `layer`).",
            "maxLength": 40
          },
          "scene": {
            "type": "string",
            "description": "Required: what the picture shows, at least 10 characters after trimming (cut to 900).",
            "minLength": 10,
            "maxLength": 900
          },
          "composition": {
            "type": "string",
            "description": "Optional composition (cut to 300 characters).",
            "maxLength": 300
          },
          "textPosition": {
            "type": "string",
            "enum": [
              "left",
              "right",
              "top",
              "bottom"
            ],
            "description": "Where to leave room for the headline; anything else means `right`."
          },
          "palette": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^#[0-9A-Fa-f]{6}$"
            },
            "description": "Hex colours; invalid entries are dropped and only the first 4 kept (default `#FFD400`, `#111111`).",
            "maxItems": 4
          },
          "why": {
            "type": "string",
            "description": "Optional reasoning (cut to 300 characters).",
            "maxLength": 300
          },
          "hasPerson": {
            "type": "boolean",
            "description": "Whether the scene has a person in it."
          }
        },
        "required": [
          "scene"
        ],
        "additionalProperties": false
      },
      "TitleIdea": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "A title (at most 100 characters)."
          },
          "angle": {
            "type": "string",
            "description": "Its angle in one word (curiosity, result, challenge, list, mistake, comparison, story...)."
          }
        },
        "required": [
          "title",
          "angle"
        ]
      },
      "Score": {
        "type": "object",
        "description": "The AI critique of a thumbnail (`ai.ScoreResult`). Scored by `mistral-medium-latest` at temperature 0; the same image and title always get the same answer.",
        "properties": {
          "overall": {
            "type": "integer",
            "description": "Overall score: the weighted mean of the pillar scores.",
            "minimum": 0,
            "maximum": 100
          },
          "verdict": {
            "type": "string",
            "description": "One-sentence verdict."
          },
          "pillars": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/ScorePillar"
            },
            "description": "Clarity, Emotion, Contrast, Text, Curiosity and, when a title was given, Title fit."
          },
          "strengths": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 4,
            "description": "Up to 4 strengths."
          },
          "fixes": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 6,
            "description": "Up to 6 concrete fixes, most important first. Visual ones can be applied with `editGeneration` `{fixes}`."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When the image was scored."
          }
        },
        "required": [
          "overall",
          "verdict",
          "pillars",
          "strengths",
          "fixes"
        ]
      },
      "ScorePillar": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "enum": [
              "Clarity",
              "Emotion",
              "Contrast",
              "Text",
              "Curiosity",
              "Title fit"
            ],
            "description": "The pillar."
          },
          "score": {
            "type": "integer",
            "description": "Pillar score: 65% the model's number, 35% the checklist's pass rate.",
            "minimum": 0,
            "maximum": 100
          },
          "note": {
            "type": "string",
            "description": "Short reason."
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoreCheck"
            },
            "description": "The checklist questions for this pillar and their answers (absent on older scores)."
          }
        },
        "required": [
          "name",
          "score",
          "note"
        ]
      },
      "ScoreCheck": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "The checklist question."
          },
          "answer": {
            "type": "string",
            "enum": [
              "yes",
              "partly",
              "partial",
              "somewhat",
              "no",
              "na"
            ],
            "description": "The model's answer as written (`partial` and `somewhat` count like `partly`); `na` = not applicable or unanswered."
          }
        },
        "required": [
          "label",
          "answer"
        ]
      },
      "TitleIdeas": {
        "type": "object",
        "description": "Titles kept on a project by `suggestTitles` with a projectId.",
        "properties": {
          "titles": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/TitleIdea"
            },
            "description": "The titles last written for the project."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When they were written."
          }
        },
        "required": [
          "titles",
          "at"
        ]
      },
      "DescriptionDraft": {
        "type": "object",
        "description": "The description kept on a project by `writeDescription` with a projectId (or edited in the studio).",
        "properties": {
          "description": {
            "type": "string",
            "description": "The description."
          },
          "hashtags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Hashtags."
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Search tags."
          },
          "links": {
            "type": "string",
            "description": "The creator's links, as given."
          },
          "chapters": {
            "type": "string",
            "description": "The creator's chapters, as given."
          },
          "keywords": {
            "type": "string",
            "description": "The keywords, as given."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When it was written or last edited."
          },
          "edited": {
            "type": "boolean",
            "description": "Present (true) once the creator edited it in the studio."
          }
        },
        "required": [
          "description",
          "hashtags",
          "tags",
          "at"
        ]
      },
      "Project": {
        "type": "object",
        "description": "A project: one video, with its concepts and the titles and description written for it.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Project id."
          },
          "title": {
            "type": "string",
            "description": "The video's title or topic (at most 200 characters)."
          },
          "description": {
            "type": "string",
            "description": "What the video is about (at most 2,000 characters)."
          },
          "niche": {
            "type": "string",
            "description": "The channel's niche (free text, usually one of the catalog's `niches`; may be empty)."
          },
          "style": {
            "$ref": "#/components/schemas/Style",
            "description": "Default image style for the project."
          },
          "youtubeVideoId": {
            "type": "string",
            "description": "The YouTube video id when the project was made from a link, else empty."
          },
          "channel": {
            "type": "string",
            "description": "The YouTube channel name when made from a link, else empty."
          },
          "personaId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The persona the concepts were written for, or null."
          },
          "concepts": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Concept"
            },
            "description": "The project's concepts, oldest first (at most 24). Null in `listProjects`."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Created."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last activity."
          },
          "originalScore": {
            "$ref": "#/components/schemas/Score",
            "description": "Cached score of the linked video's current YouTube thumbnail (`scoreThumbnail` `{projectId, original: true}`)."
          },
          "script": {
            "type": "string",
            "description": "The pasted script or transcript (absent when empty)."
          },
          "layout": {
            "$ref": "#/components/schemas/Layout",
            "description": "The named layout every concept uses, or `\"\"`."
          },
          "faceless": {
            "type": "boolean",
            "description": "No people in concepts or images."
          },
          "titleIdeas": {
            "$ref": "#/components/schemas/TitleIdeas",
            "description": "Titles last written for the project (absent until then, and in `listProjects`)."
          },
          "descriptionDraft": {
            "$ref": "#/components/schemas/DescriptionDraft",
            "description": "Description last written for the project (absent until then, and in `listProjects`)."
          },
          "generationCount": {
            "type": "integer",
            "description": "Succeeded images: filled in `listProjects` only, 0 elsewhere."
          },
          "pendingCount": {
            "type": "integer",
            "description": "`listProjects` only: images queued or rendering (absent when 0)."
          },
          "designCount": {
            "type": "integer",
            "description": "`listProjects` only: saved designs (absent when 0)."
          },
          "bestScore": {
            "type": "integer",
            "description": "`listProjects` only: best cached overall score of its images and designs (absent when none)."
          },
          "latestGenerationId": {
            "type": "string",
            "format": "uuid",
            "description": "`listProjects` only: the newest succeeded image (absent when none)."
          },
          "coverUrl": {
            "type": "string",
            "description": "`listProjects` only: a presigned URL (15 minutes) of the newest image or design export (absent when none).",
            "format": "uri"
          }
        },
        "required": [
          "id",
          "title",
          "description",
          "niche",
          "style",
          "youtubeVideoId",
          "channel",
          "personaId",
          "concepts",
          "createdAt",
          "updatedAt",
          "layout",
          "faceless",
          "generationCount"
        ]
      },
      "EditPoint": {
        "type": "object",
        "description": "A spot on the image, as fractions of its width and height.",
        "properties": {
          "x": {
            "type": "number",
            "description": "0 = left edge, 1 = right edge.",
            "minimum": 0,
            "maximum": 1
          },
          "y": {
            "type": "number",
            "description": "0 = top edge, 1 = bottom edge.",
            "minimum": 0,
            "maximum": 1
          }
        },
        "required": [
          "x",
          "y"
        ],
        "additionalProperties": false
      },
      "MaskStroke": {
        "type": "object",
        "description": "One brush stroke.",
        "properties": {
          "r": {
            "type": "number",
            "description": "Brush radius as a fraction of the image width.",
            "minimum": 0.002,
            "maximum": 0.3
          },
          "erase": {
            "type": "boolean",
            "description": "An eraser stroke: removes paint."
          },
          "points": {
            "type": "array",
            "items": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                }
              ],
              "minItems": 2,
              "maxItems": 2,
              "description": "`[x, y]` as fractions of the width and height."
            },
            "description": "The stroke's points, at least one.",
            "minItems": 1
          }
        },
        "required": [
          "r",
          "points"
        ],
        "additionalProperties": false
      },
      "EditMask": {
        "type": "object",
        "description": "An area painted on the image. Only that area changes; the rest comes back exactly (unless the model re-framed the picture).",
        "properties": {
          "strokes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MaskStroke"
            },
            "description": "1 to 300 strokes with at most 6,000 points in all. The painted area must cover at least 0.1% and at most 90% of the picture, and must not be too detailed to rasterise (`bad_mask`).",
            "minItems": 1,
            "maxItems": 300
          }
        },
        "required": [
          "strokes"
        ],
        "additionalProperties": false
      },
      "Generation": {
        "type": "object",
        "description": "An image made (or being made) by the image model.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Image id."
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Its project; null for reaction faces."
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The image it was edited from; null for a first image."
          },
          "kind": {
            "type": "string",
            "enum": [
              "generate",
              "edit",
              "recreate",
              "face"
            ],
            "description": "How it was made."
          },
          "concept": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Concept"
              },
              {
                "type": "null"
              }
            ],
            "description": "The concept it renders (null for reaction faces)."
          },
          "instruction": {
            "type": "string",
            "description": "For edits, recreates and faces: what was asked, in words (e.g. `Expression: Shocked`, `Text: DAY 30`, `In the painted area: ...`); empty for a first image."
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect"
          },
          "style": {
            "$ref": "#/components/schemas/Style"
          },
          "personaId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The persona whose face was used, or null."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "succeeded",
              "failed"
            ],
            "description": "Poll until `succeeded` or `failed`. A failed image's credit is refunded."
          },
          "error": {
            "type": "string",
            "description": "Why it failed (absent otherwise)."
          },
          "model": {
            "type": "string",
            "description": "The image model."
          },
          "creditsCost": {
            "type": "integer",
            "description": "Credits it cost."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Queued."
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Finished, or null."
          },
          "textMode": {
            "$ref": "#/components/schemas/TextMode"
          },
          "textStyle": {
            "$ref": "#/components/schemas/TextStyle",
            "description": "The text style, for textMode `image` (absent otherwise)."
          },
          "textRead": {
            "type": "string",
            "description": "textMode `image`: the headline as the vision model read it back (absent until checked)."
          },
          "textOk": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "textMode `image`: whether the drawn headline matches the words (case, spacing and punctuation other than apostrophes ignored); null when not checked or not applicable."
          },
          "score": {
            "$ref": "#/components/schemas/Score",
            "description": "Cached AI score (absent until scored)."
          },
          "layout": {
            "$ref": "#/components/schemas/Layout",
            "description": "The named layout used (absent when none)."
          },
          "point": {
            "$ref": "#/components/schemas/EditPoint",
            "description": "For an edit at a spot: where (absent otherwise)."
          },
          "scoreSig": {
            "type": "string",
            "description": "When `score` is of an editor design built on this image rather than the image alone: that design's signature (absent otherwise)."
          },
          "mask": {
            "$ref": "#/components/schemas/EditMask",
            "description": "For an edit inside a painted area: the area (absent otherwise)."
          },
          "imageUrl": {
            "type": "string",
            "description": "A presigned URL of the image, valid 15 minutes, once it exists (absent in queue answers and before success). For a file without metadata use `getGenerationDownload`.",
            "format": "uri"
          }
        },
        "required": [
          "id",
          "projectId",
          "parentId",
          "kind",
          "concept",
          "instruction",
          "aspect",
          "style",
          "personaId",
          "status",
          "model",
          "creditsCost",
          "createdAt",
          "completedAt",
          "textMode",
          "textOk"
        ]
      },
      "QueuedGenerations": {
        "type": "object",
        "description": "Images queued. Poll each with `getGeneration`.",
        "properties": {
          "generation": {
            "$ref": "#/components/schemas/Generation",
            "description": "The first queued image (status `queued`, or the existing row when deduplicated)."
          },
          "generations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Generation"
            },
            "description": "Every image the request queued, in order."
          },
          "credits": {
            "type": "integer",
            "description": "The account's credits after charging."
          },
          "deduped": {
            "type": "boolean",
            "description": "True when every image was a repeat of an identical request from the last 2 minutes: the existing images come back and nothing is charged."
          },
          "partial": {
            "type": "string",
            "description": "When a batch could not be queued whole (credits ran out, daily capacity): how many were queued and why (absent otherwise)."
          },
          "skippedFixes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`editGeneration` with fixes: the fixes about text, which were not applied (apply them in the editor, free). Absent otherwise."
          }
        },
        "required": [
          "generation",
          "generations",
          "credits",
          "deduped"
        ]
      },
      "Design": {
        "type": "object",
        "description": "A design saved in Clickframe's editor (a canvas document on top of an image).",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Design id."
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Its project, or null."
          },
          "generationId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The image it is built on, or null."
          },
          "name": {
            "type": "string",
            "description": "Name (at most 120 characters)."
          },
          "layers": {
            "description": "The editor document (version 2: `layers`, `adjust`, `bg`, `shade`, `subject`, `assets`; under 128 KB), stored as given. Null in `listProjects`' recentDesigns."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Created."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last saved."
          },
          "score": {
            "$ref": "#/components/schemas/Score",
            "description": "Cached AI score of its export (cleared on every save; absent when none)."
          },
          "exportUrl": {
            "type": "string",
            "description": "Presigned URL (15 minutes) of the design's last exported image (absent when none).",
            "format": "uri"
          }
        },
        "required": [
          "id",
          "projectId",
          "generationId",
          "name",
          "layers",
          "createdAt",
          "updatedAt"
        ]
      },
      "Persona": {
        "type": "object",
        "description": "A set of photos of the creator, used to put their face in images.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Persona id: pass it as `personaId`."
          },
          "name": {
            "type": "string",
            "description": "Name."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Created."
          },
          "photoUrls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Presigned URLs (15 minutes) of its 1 to 4 photos."
          }
        },
        "required": [
          "id",
          "name",
          "createdAt",
          "photoUrls"
        ]
      },
      "User": {
        "type": "object",
        "description": "The account.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Account id."
          },
          "email": {
            "type": "string",
            "description": "Email address.",
            "format": "email"
          },
          "name": {
            "type": "string",
            "description": "Name (may be empty)."
          },
          "plan": {
            "type": "string",
            "description": "`free`, `creator` or `pro`."
          },
          "planStatus": {
            "type": "string",
            "description": "The subscription's Stripe status (e.g. `active`, `past_due`), or empty."
          },
          "planInterval": {
            "type": "string",
            "enum": [
              "month",
              "year"
            ],
            "description": "Billing interval of the plan."
          },
          "planRenewsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the plan renews, or null."
          },
          "credits": {
            "type": "integer",
            "description": "Credits left (they never expire).",
            "minimum": 0
          },
          "emailVerified": {
            "type": "boolean",
            "description": "Whether the email is confirmed. Scores, titles, descriptions, the click test and the free credits need it."
          },
          "aiLabel": {
            "type": "boolean",
            "description": "Whether downloads carry \"made with AI\" metadata (raw downloads without it carry no metadata)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Signed up."
          }
        },
        "required": [
          "id",
          "email",
          "name",
          "plan",
          "planStatus",
          "planInterval",
          "planRenewsAt",
          "credits",
          "emailVerified",
          "aiLabel",
          "createdAt"
        ]
      },
      "Video": {
        "type": "object",
        "description": "A public YouTube video.",
        "properties": {
          "videoId": {
            "type": "string",
            "description": "YouTube video id (11 characters)."
          },
          "title": {
            "type": "string",
            "description": "Title (from YouTube's oEmbed)."
          },
          "channel": {
            "type": "string",
            "description": "Channel name."
          },
          "thumbnailUrl": {
            "type": "string",
            "description": "`https://i.ytimg.com/vi/<id>/hqdefault.jpg`.",
            "format": "uri"
          }
        },
        "required": [
          "videoId",
          "title",
          "channel",
          "thumbnailUrl"
        ]
      },
      "ClickShare": {
        "type": "object",
        "properties": {
          "share": {
            "type": "number",
            "description": "Estimated share of 100 clicks (percent; all videos sum to 100)."
          },
          "why": {
            "type": "string",
            "description": "What helps or hurts it (at most about 14 words)."
          }
        },
        "required": [
          "share",
          "why"
        ]
      },
      "ClickTestResult": {
        "type": "object",
        "description": "An AI estimate of how 100 viewers would share their clicks: packaging only, every channel treated as equally unknown. Not YouTube data and not a CTR.",
        "properties": {
          "videos": {
            "type": "integer",
            "description": "Videos in each test: your version plus the competitors whose thumbnails loaded."
          },
          "fairShare": {
            "type": "number",
            "description": "100 / videos, rounded down to one decimal: the share of an average video."
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "mine": {
                  "$ref": "#/components/schemas/ClickShare"
                },
                "rank": {
                  "type": "integer",
                  "description": "1 + the number of competitors with a strictly higher share.",
                  "minimum": 1
                },
                "competitors": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "videoId": {
                        "type": "string",
                        "description": "The competitor's video id."
                      },
                      "share": {
                        "type": "number",
                        "description": "Its estimated share."
                      },
                      "why": {
                        "type": "string",
                        "description": "What helps or hurts it."
                      }
                    },
                    "required": [
                      "videoId",
                      "share",
                      "why"
                    ]
                  },
                  "description": "The competitors, in the order given (skipped ones left out)."
                }
              },
              "required": [
                "mine",
                "rank",
                "competitors"
              ]
            },
            "description": "One result per version, in the order given."
          },
          "skipped": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Competitors whose thumbnail couldn't be loaded from YouTube (null when none)."
          },
          "charged": {
            "type": "integer",
            "description": "Credits charged: 1 per version tested afresh; remembered tests are free."
          }
        },
        "required": [
          "videos",
          "fairShare",
          "results",
          "skipped",
          "charged"
        ]
      },
      "DescriptionResult": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "A ready-to-paste description (at most 5,000 characters): hook, body, your links and chapters verbatim, a call to action, the hashtags."
          },
          "hashtags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 3,
            "description": "Up to 3 hashtags."
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Search tags, under 500 characters in total as YouTube counts them."
          }
        },
        "required": [
          "description",
          "hashtags",
          "tags"
        ]
      },
      "CreateProjectRequest": {
        "type": "object",
        "description": "Give `title` or `youtubeUrl` (or both). Unknown fields are refused.",
        "properties": {
          "title": {
            "type": "string",
            "description": "The video title or topic, at least 3 characters (cut to 200). Omit it to use the linked video's title.",
            "minLength": 3,
            "maxLength": 200
          },
          "youtubeUrl": {
            "type": "string",
            "description": "A YouTube video link: its title and channel are looked up (oEmbed) and the niche detected when none is given."
          },
          "description": {
            "type": "string",
            "description": "What happens in the video (cut to 2,000 characters).",
            "maxLength": 2000
          },
          "niche": {
            "type": "string",
            "description": "The channel's niche, free text (cut to 60 characters); usually one of the catalog's `niches`: Gaming, Tech & reviews, Finance & business, Education, Vlog & lifestyle, Fitness & health, Food & cooking, Travel, Beauty & fashion, Entertainment & commentary, Science, History & stories, Music, Sports, Podcast, Kids & family, Automotive, Other.",
            "maxLength": 60,
            "examples": [
              "Fitness & health"
            ]
          },
          "style": {
            "$ref": "#/components/schemas/Style"
          },
          "personaId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Write the concepts for this persona's face (ignored when faceless). Must be one of your personas."
          },
          "script": {
            "type": "string",
            "description": "A script or transcript (cut to 20,000 characters; the first 12,000 go to the model).",
            "maxLength": 20000
          },
          "layout": {
            "$ref": "#/components/schemas/Layout"
          },
          "faceless": {
            "type": "boolean",
            "description": "No people in concepts or images (drops the persona and the person layouts)."
          }
        },
        "anyOf": [
          {
            "required": [
              "title"
            ]
          },
          {
            "required": [
              "youtubeUrl"
            ]
          }
        ],
        "additionalProperties": false
      },
      "CreateGenerationRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project (required)."
          },
          "concept": {
            "$ref": "#/components/schemas/ConceptInput"
          },
          "style": {
            "$ref": "#/components/schemas/Style",
            "description": "Image style (default `realistic`)."
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect",
            "description": "Format (default `16:9`)."
          },
          "personaId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Put this persona's face in the image. Not with `noPerson`."
          },
          "extra": {
            "type": "string",
            "description": "Free-form extra direction for the image (the first 400 characters are used).",
            "maxLength": 400
          },
          "variation": {
            "type": "integer",
            "description": "Alternative-take number: a different camera angle, pose and framing for the same request (0 = none)."
          },
          "textMode": {
            "$ref": "#/components/schemas/TextMode"
          },
          "textStyle": {
            "$ref": "#/components/schemas/TextStyle"
          },
          "count": {
            "type": "integer",
            "description": "Paid plans: 1 to 4 takes of one style (1 credit each; values outside are clamped). More than 1 on the free plan: 403 plan_required.",
            "minimum": 1,
            "maximum": 4,
            "default": 1
          },
          "styles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Style"
            },
            "description": "Paid plans: one image per style instead of `count` (deduplicated, at most 4 used; needs 2 or more distinct styles, else `style` and `count` apply).",
            "maxItems": 4
          },
          "layout": {
            "$ref": "#/components/schemas/Layout"
          },
          "noPerson": {
            "type": "boolean",
            "description": "No people in the image (a hand at most). Not with `personaId`."
          }
        },
        "required": [
          "projectId",
          "concept"
        ],
        "additionalProperties": false
      },
      "EditRequest": {
        "description": "Exactly one kind of edit. Every edit costs 1 credit and makes a new version (kind `edit`, `parentId` = the image). Edits of an `image`-mode picture keep its headline unless the edit is about the text.",
        "oneOf": [
          {
            "type": "object",
            "description": "A change to the whole picture.",
            "properties": {
              "instruction": {
                "type": "string",
                "description": "What to change, e.g. \"make the background bright red\" (at least 3 characters, cut to 500).",
                "minLength": 3,
                "maxLength": 500
              }
            },
            "required": [
              "instruction"
            ],
            "title": "Instruction",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "A change at one spot: the model gets a copy with a red ring there.",
            "properties": {
              "instruction": {
                "type": "string",
                "description": "What to change at the spot (at least 3 characters, cut to 500).",
                "minLength": 3,
                "maxLength": 500
              },
              "point": {
                "$ref": "#/components/schemas/EditPoint"
              }
            },
            "required": [
              "instruction",
              "point"
            ],
            "title": "Instruction at a point",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "A change inside a painted area, blended back so the rest stays exactly as it was.",
            "properties": {
              "instruction": {
                "type": "string",
                "description": "What to change inside the painted area (at least 3 characters, cut to 500).",
                "minLength": 3,
                "maxLength": 500
              },
              "mask": {
                "$ref": "#/components/schemas/EditMask"
              }
            },
            "required": [
              "instruction",
              "mask"
            ],
            "title": "Instruction inside a mask",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "Change the main person's facial expression only (same identity, pose, light and background).",
            "properties": {
              "expression": {
                "$ref": "#/components/schemas/Expression"
              }
            },
            "required": [
              "expression"
            ],
            "title": "Expression",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "Paint these headline words, replacing drawn text or adding text to a `layer` image. The result is textMode `image` and spell-checked.",
            "properties": {
              "text": {
                "type": "object",
                "properties": {
                  "words": {
                    "type": "string",
                    "description": "The headline words to paint (quotes removed, cut to 6 words and 48 characters; required).",
                    "minLength": 1,
                    "maxLength": 48
                  },
                  "style": {
                    "$ref": "#/components/schemas/TextStyle",
                    "description": "Text look (default `auto`)."
                  }
                },
                "required": [
                  "words"
                ],
                "additionalProperties": false
              }
            },
            "required": [
              "text"
            ],
            "title": "New text",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "Remove the drawn headline.",
            "properties": {
              "removeText": {
                "type": "boolean",
                "const": true,
                "description": "Remove the drawn text, leaving clean space (the result is textMode `layer`)."
              }
            },
            "required": [
              "removeText"
            ],
            "title": "Remove text",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "Apply an AI score's fixes.",
            "properties": {
              "fixes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Fixes from an AI score (only the first 8 are read). Up to four visual fixes become one instruction; text advice comes back as `skippedFixes`. Only text advice: 400 fixes_text_only.",
                "minItems": 1,
                "maxItems": 8
              }
            },
            "required": [
              "fixes"
            ],
            "title": "Score fixes",
            "additionalProperties": false
          }
        ]
      },
      "RecreateRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project: its title is the new video."
          },
          "reference": {
            "description": "The reference thumbnail: an upload of yours (`key`), or a public video's current thumbnail (`youtubeUrl`). Scaled to at most 1,024 px and kept with the image. Only use references you have the right to use.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "An uploaded, confirmed file of yours (`createUploadUrl` + `confirmUpload`): JPG or PNG, 64 to 8,000 px a side."
                  }
                },
                "required": [
                  "key"
                ],
                "title": "Upload",
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "youtubeUrl": {
                    "type": "string",
                    "description": "A public YouTube video link."
                  }
                },
                "required": [
                  "youtubeUrl"
                ],
                "title": "YouTube video",
                "additionalProperties": false
              }
            ]
          },
          "match": {
            "type": "string",
            "enum": [
              "layout",
              "style"
            ],
            "default": "layout",
            "description": "`layout`: its composition, colours and lighting. `style`: only its colours, lighting and finish, in a new composition. Anything else means `layout`."
          },
          "subject": {
            "type": "string",
            "description": "What the new thumbnail shows (cut to 600 characters; default: what the title promises).",
            "maxLength": 600
          },
          "headline": {
            "type": "string",
            "description": "The headline words (quotes removed, cut to 6 words and 48 characters). Without one, textMode is `layer`.",
            "maxLength": 48
          },
          "textMode": {
            "$ref": "#/components/schemas/TextMode"
          },
          "textStyle": {
            "$ref": "#/components/schemas/TextStyle",
            "description": "With textMode `image`: a text style, or `auto` (default) to letter it like the reference."
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect",
            "description": "Format (default `16:9`)."
          },
          "personaId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Put this persona's face in the image. Not with `noPerson`."
          },
          "noPerson": {
            "type": "boolean",
            "description": "No people in the image. Not with `personaId`."
          }
        },
        "required": [
          "projectId",
          "reference"
        ],
        "additionalProperties": false
      },
      "ScoreRequest": {
        "description": "What to score: exactly one source. Unknown fields are refused.",
        "oneOf": [
          {
            "type": "object",
            "description": "A Clickframe image (cached on it).",
            "properties": {
              "generationId": {
                "type": "string",
                "format": "uuid",
                "description": "A Clickframe image of yours. Its cached score comes back free."
              },
              "key": {
                "type": "string",
                "description": "Optional: your confirmed upload of this image composed with its headline layer (what the editor exports). Scored instead of the bare image."
              },
              "sig": {
                "type": "string",
                "description": "Optional, with `key`: a signature of the design (cut to 80 characters); the score is kept with it and returned free while the signature matches.",
                "maxLength": 80
              },
              "title": {
                "type": "string",
                "description": "The video title, to judge how picture and title work together (only the first 200 characters are used). Defaults to the project's title, or the YouTube video's.",
                "maxLength": 200
              },
              "refresh": {
                "type": "boolean",
                "description": "Score again even when a cached score exists (counts against the daily allowance unless the exact image and title were scored recently)."
              }
            },
            "required": [
              "generationId"
            ],
            "title": "Image",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "A saved design's export.",
            "properties": {
              "designId": {
                "type": "string",
                "format": "uuid",
                "description": "A saved design with an export. Cached on the design until it is saved again."
              },
              "title": {
                "type": "string",
                "description": "The video title, to judge how picture and title work together (only the first 200 characters are used). Defaults to the project's title, or the YouTube video's.",
                "maxLength": 200
              },
              "refresh": {
                "type": "boolean",
                "description": "Score again even when a cached score exists (counts against the daily allowance unless the exact image and title were scored recently)."
              }
            },
            "required": [
              "designId"
            ],
            "title": "Design",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "The project video's current thumbnail.",
            "properties": {
              "projectId": {
                "type": "string",
                "format": "uuid",
                "description": "A project made from a YouTube link."
              },
              "original": {
                "type": "boolean",
                "const": true,
                "description": "Score the video's current YouTube thumbnail (the \"before\"); cached on the project."
              },
              "title": {
                "type": "string",
                "description": "The video title, to judge how picture and title work together (only the first 200 characters are used). Defaults to the project's title, or the YouTube video's.",
                "maxLength": 200
              },
              "refresh": {
                "type": "boolean",
                "description": "Score again even when a cached score exists (counts against the daily allowance unless the exact image and title were scored recently)."
              }
            },
            "required": [
              "projectId",
              "original"
            ],
            "title": "Project's original thumbnail",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "An uploaded image.",
            "properties": {
              "key": {
                "type": "string",
                "description": "An uploaded, confirmed image of yours (not cached)."
              },
              "title": {
                "type": "string",
                "description": "The video title, to judge how picture and title work together (only the first 200 characters are used). Defaults to the project's title, or the YouTube video's.",
                "maxLength": 200
              },
              "refresh": {
                "type": "boolean",
                "description": "Score again even when a cached score exists (counts against the daily allowance unless the exact image and title were scored recently)."
              }
            },
            "required": [
              "key"
            ],
            "title": "Upload",
            "additionalProperties": false
          },
          {
            "type": "object",
            "description": "A YouTube video's thumbnail.",
            "properties": {
              "youtubeUrl": {
                "type": "string",
                "description": "Any public YouTube video link: its current thumbnail is scored (not cached)."
              },
              "title": {
                "type": "string",
                "description": "The video title, to judge how picture and title work together (only the first 200 characters are used). Defaults to the project's title, or the YouTube video's.",
                "maxLength": 200
              },
              "refresh": {
                "type": "boolean",
                "description": "Score again even when a cached score exists (counts against the daily allowance unless the exact image and title were scored recently)."
              }
            },
            "required": [
              "youtubeUrl"
            ],
            "title": "YouTube video",
            "additionalProperties": false
          }
        ]
      },
      "ClickTestRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "versions": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "An uploaded, confirmed image of yours (JPG or PNG up to 8,000 px a side).",
                      "minLength": 1
                    }
                  },
                  "required": [
                    "key"
                  ],
                  "title": "Upload",
                  "additionalProperties": false
                },
                {
                  "type": "object",
                  "properties": {
                    "generationId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "A Clickframe image of yours."
                    }
                  },
                  "required": [
                    "generationId"
                  ],
                  "title": "Image",
                  "additionalProperties": false
                }
              ]
            },
            "description": "1 to 3 versions of your thumbnail; each is tested on its own against the same competitors.",
            "minItems": 1,
            "maxItems": 3
          },
          "title": {
            "type": "string",
            "description": "Your video's title (required; cut to 100 characters).",
            "minLength": 1,
            "maxLength": 100
          },
          "channel": {
            "type": "string",
            "description": "Your channel name (cut to 60 characters; default \"Your channel\").",
            "maxLength": 60
          },
          "competitors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "videoId": {
                  "type": "string",
                  "description": "A public YouTube video id, different for each competitor.",
                  "pattern": "^[A-Za-z0-9_-]{11}$"
                },
                "title": {
                  "type": "string",
                  "description": "Its title (cut to 120 characters).",
                  "maxLength": 120
                },
                "channel": {
                  "type": "string",
                  "description": "Its channel (cut to 60 characters).",
                  "maxLength": 60
                }
              },
              "required": [
                "videoId"
              ],
              "additionalProperties": false
            },
            "description": "2 to 7 competing public videos. Their current thumbnails are fetched from YouTube; at least 2 must load.",
            "minItems": 2,
            "maxItems": 7
          }
        },
        "required": [
          "versions",
          "title",
          "competitors"
        ],
        "additionalProperties": false
      },
      "TitlesRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "topic": {
            "type": "string",
            "description": "What the video is about (at least 3 characters; cut to 300).",
            "minLength": 3,
            "maxLength": 300
          },
          "description": {
            "type": "string",
            "description": "More detail (cut to 1,000 characters).",
            "maxLength": 1000
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Keep the titles on this project (`titleIdeas`)."
          }
        },
        "required": [
          "topic"
        ],
        "additionalProperties": false
      },
      "DescriptionRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "title": {
            "type": "string",
            "description": "The video title (at least 3 characters; cut to 200).",
            "minLength": 3,
            "maxLength": 200
          },
          "about": {
            "type": "string",
            "description": "What happens in the video (cut to 2,000 characters).",
            "maxLength": 2000
          },
          "keywords": {
            "type": "string",
            "description": "Comma-separated search terms to work in (cut to 300 characters).",
            "maxLength": 300
          },
          "links": {
            "type": "string",
            "description": "Your links and calls to action, pasted in verbatim (cut to 1,000 characters).",
            "maxLength": 1000
          },
          "chapters": {
            "type": "string",
            "description": "`0:00 Intro` lines, pasted in verbatim under \"Chapters\" (cut to 1,500 characters).",
            "maxLength": 1500
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Keep the description on this project (`descriptionDraft`)."
          }
        },
        "required": [
          "title"
        ],
        "additionalProperties": false
      },
      "LookupRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "url": {
            "type": "string",
            "description": "A YouTube video link (watch, youtu.be, Shorts...)."
          }
        },
        "required": [
          "url"
        ],
        "additionalProperties": false
      },
      "FacesRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "expressions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Expression"
            },
            "description": "1 to 6 distinct expressions (duplicates are merged); 1 credit each.",
            "minItems": 1,
            "maxItems": 6,
            "uniqueItems": true
          }
        },
        "required": [
          "expressions"
        ],
        "additionalProperties": false
      },
      "UploadUrlRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "filename": {
            "type": "string",
            "description": "The file's name; only its extension is kept (`.jpg`, `.png`...)."
          },
          "contentType": {
            "type": "string",
            "enum": [
              "image/png",
              "image/jpeg",
              "image/webp"
            ],
            "description": "The image type; the PUT must send the same Content-Type."
          },
          "sizeBytes": {
            "type": "integer",
            "description": "The exact size in bytes (signed into the upload URL; storage refuses any other length). At most MAX_UPLOAD_BYTES, 10 MiB by default.",
            "minimum": 1,
            "maximum": 10485760
          }
        },
        "required": [
          "contentType",
          "sizeBytes"
        ],
        "additionalProperties": false
      },
      "KeyRequest": {
        "type": "object",
        "description": "Unknown fields are refused.",
        "properties": {
          "key": {
            "type": "string",
            "description": "The object key from `createUploadUrl` (`users/<account>/<uuid><ext>`).",
            "minLength": 1
          },
          "sizeBytes": {
            "type": "integer",
            "description": "Accepted and ignored."
          }
        },
        "required": [
          "key"
        ],
        "additionalProperties": false
      },
      "Catalog": {
        "type": "object",
        "description": "The options the API accepts and what they cost.",
        "properties": {
          "styles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/Style"
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                }
              },
              "required": [
                "id",
                "label"
              ]
            },
            "description": "Image styles, in display order."
          },
          "aspects": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/Aspect"
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                }
              },
              "required": [
                "id",
                "label"
              ]
            },
            "description": "Formats, in display order."
          },
          "layouts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Layout id.",
                  "enum": [
                    "center",
                    "reaction",
                    "pointing",
                    "object",
                    "before_after",
                    "versus",
                    "closeup",
                    "lineup",
                    "mystery",
                    "scene"
                  ]
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                },
                "hint": {
                  "type": "string",
                  "description": "Hint."
                },
                "person": {
                  "type": "boolean",
                  "description": "Built around a person (hidden for faceless channels)."
                }
              },
              "required": [
                "id",
                "label",
                "hint",
                "person"
              ]
            },
            "description": "Named layouts, in display order."
          },
          "maxBatch": {
            "type": "integer",
            "description": "Most images one request can queue (4)."
          },
          "niches": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Niches offered, in display order."
          },
          "signupCredits": {
            "type": "integer",
            "description": "Free credits granted when a new account confirms its email."
          },
          "creditCost": {
            "type": "object",
            "description": "Credits per action (the click test, 1 per version, isn't listed).",
            "properties": {
              "generate": {
                "type": "integer",
                "description": "Per image."
              },
              "edit": {
                "type": "integer",
                "description": "Per edit."
              },
              "face": {
                "type": "integer",
                "description": "Per reaction face."
              },
              "recreate": {
                "type": "integer",
                "description": "Per recreate."
              },
              "concepts": {
                "type": "integer",
                "description": "Concepts."
              },
              "score": {
                "type": "integer",
                "description": "AI score."
              },
              "titles": {
                "type": "integer",
                "description": "Titles."
              },
              "description": {
                "type": "integer",
                "description": "Description."
              }
            },
            "required": [
              "generate",
              "edit",
              "face",
              "recreate",
              "concepts",
              "score",
              "titles",
              "description"
            ]
          },
          "recreateMatches": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Match id (`layout` or `style`), the value of `match` in recreateThumbnail."
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                },
                "hint": {
                  "type": "string",
                  "description": "Hint."
                }
              },
              "required": [
                "id",
                "label",
                "hint"
              ]
            },
            "description": "Ways a reference can be followed (recreateThumbnail's `match`)."
          },
          "batchPlans": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Plans that may queue several images per request (`creator`, `pro`)."
          },
          "expressions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/Expression"
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                }
              },
              "required": [
                "id",
                "label"
              ]
            },
            "description": "Facial expressions, in display order."
          },
          "textStyles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/TextStyle"
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                },
                "hint": {
                  "type": "string",
                  "description": "Hint."
                }
              },
              "required": [
                "id",
                "label",
                "hint"
              ]
            },
            "description": "Looks for AI-drawn text, in display order."
          },
          "youtubeSearch": {
            "type": "boolean",
            "description": "Whether keyword search for competitors is enabled (it is not part of the API-key routes)."
          },
          "plans": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Plan id."
                },
                "name": {
                  "type": "string",
                  "description": "Name."
                },
                "priceCents": {
                  "type": "integer",
                  "description": "Monthly price in US cents, billed monthly."
                },
                "credits": {
                  "type": "integer",
                  "description": "Credits per month."
                },
                "yearlyCents": {
                  "type": "integer",
                  "description": "Yearly price in US cents, billed yearly."
                }
              },
              "required": [
                "id",
                "name",
                "priceCents",
                "credits",
                "yearlyCents"
              ]
            },
            "description": "Paid plans, cheapest first."
          },
          "supportEmail": {
            "type": "string",
            "description": "Support address.",
            "format": "email"
          }
        },
        "required": [
          "styles",
          "aspects",
          "layouts",
          "maxBatch",
          "niches",
          "signupCredits",
          "creditCost",
          "recreateMatches",
          "batchPlans",
          "expressions",
          "textStyles",
          "youtubeSearch",
          "plans",
          "supportEmail"
        ]
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ],
            "description": "`degraded` when the database is unreachable."
          },
          "db": {
            "type": "string",
            "enum": [
              "ok",
              "down"
            ],
            "description": "Database reachability (2-second ping)."
          },
          "app": {
            "type": "string",
            "description": "The app's name."
          }
        },
        "required": [
          "status",
          "db",
          "app"
        ]
      },
      "JsonRpcRequest": {
        "type": "object",
        "description": "A JSON-RPC 2.0 request or notification.",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0",
            "description": "Must be \"2.0\"."
          },
          "id": {
            "type": [
              "string",
              "integer"
            ],
            "description": "Request id; omit for a notification (answered 202 with no body)."
          },
          "method": {
            "type": "string",
            "description": "e.g. `initialize`, `server/discover`, `ping`, `tools/list`, `tools/call`."
          },
          "params": {
            "type": "object",
            "description": "Method parameters. Modern (2026-07-28) requests carry `_meta` with `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`; `tools/call` takes `{name, arguments}`."
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ]
      },
      "JsonRpcError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "integer",
            "description": "JSON-RPC error code: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal, -32020 header mismatch, -32022 unsupported protocol version."
          },
          "message": {
            "type": "string",
            "description": "Error text."
          },
          "data": {
            "description": "Extra detail, e.g. `{supported, requested}` for an unsupported version."
          }
        },
        "required": [
          "code",
          "message"
        ]
      },
      "JsonRpcResponse": {
        "type": "object",
        "description": "A JSON-RPC 2.0 response: `result` or `error`.",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer"
            ],
            "description": "The request's id (absent when the request couldn't be read)."
          },
          "result": {
            "type": "object",
            "description": "The result. Modern results carry `resultType: \"complete\"` and the server's identity in `_meta`; `tools/call` results are `{content, isError, structuredContent?}`."
          },
          "error": {
            "$ref": "#/components/schemas/JsonRpcError"
          }
        },
        "required": [
          "jsonrpc"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is invalid (bad_request: malformed JSON, an unknown field, a body over 64 KB, or an invalid id; or a specific code).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Not authenticated: unauthorized (no bearer token), invalid_token (a bearer token that is neither a cf_live_ key nor a valid session), invalid_api_key (the key is wrong or was revoked), account_gone (the account no longer exists).",
        "headers": {
          "WWW-Authenticate": {
            "description": "`Bearer realm=\"clickframe\"` (with `error=\"invalid_token\"` for an invalid token) on unauthorized and invalid_token; absent otherwise.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "no_credits: not enough credits for the request (a batch is refused whole, never half charged).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Refused: verify_email (confirm the account's email first), plan_required (Creator or Pro only), not_available_with_api_key (route or id not open to API keys), forbidden (a file that isn't a confirmed upload of yours).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "not_found: no such resource on this account.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ContentTooLarge": {
        "description": "too_large: the file is over the upload limit; upload_quota: the account's upload space is used up (2 GB, 100 MB until the email is confirmed).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnprocessableContent": {
        "description": "content_refused: the text goes against the content rules (moderation). Rephrase it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "rate_limited: too many requests (per key: 1,200 an hour; per account: limits per feature); score_limit: today's new AI scores are used up (5 a day free, 30 on paid plans; they come back at midnight UTC). Retry-After says when to try again, and RateLimit/RateLimit-Policy which limit was used up.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "db_error: a server-side error. Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadGateway": {
        "description": "An upstream service failed: ai_error (the AI couldn't complete the request), youtube_error, storage_error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Try again later: capacity (today's platform-wide image capacity is reached), ai_busy (the AI provider is rate-limiting), ai_unavailable (AI features aren't configured).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "The limit that applies, as the IETF RateLimit header fields describe it (draft-ietf-httpapi-ratelimit-headers): the policy's name, its quota `q` and its window `w` in seconds. Sent on every answer to an API key (`\"api-key\";q=1200;w=3600`), on the catalog (per IP), and on a 429 for the limit that was used up.",
        "schema": {
          "type": "string"
        },
        "example": "\"api-key\";q=1200;w=3600"
      },
      "RateLimit": {
        "description": "What is left of that limit: requests remaining `r` and seconds `t` until the window resets.",
        "schema": {
          "type": "string"
        },
        "example": "\"api-key\";r=1187;t=2940"
      },
      "Retry-After": {
        "description": "Seconds until the used-up limit resets (for `score_limit`, until midnight UTC). Wait at least this long before trying again.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Location": {
        "description": "Where to follow the queued work: the image to poll with getGeneration (`https://clickframe-api.norvane-studio.com/v1/generations/{id}`), the first one when the request queued several. Poll it every 2-3 seconds until `status` is `succeeded` or `failed`.",
        "schema": {
          "type": "string",
          "format": "uri"
        }
      }
    }
  }
}
