{
  "openapi": "3.1.0",
  "info": {
    "title": "Colora API",
    "version": "1.0.0",
    "description": "Colora makes printable black-and-white coloring pages with AI. This API is the app's own `/v1` API opened to API keys: pages from a description (name pages included), traced from a photo, or with a saved character; batches for books; page ideas; edits and new versions; characters; books.\n\nIt uses the account's normal credits, with the same limits and safety checks as the app: 1 credit per page, edit or version; ideas, downloads, characters and books are free. Credits never expire. API keys come with the Creator and Pro plans.\n\nPages are asynchronous: a request charges and queues them and answers 202. Poll the `Location` it returns (`GET /v1/pages/{id}`) every 2 seconds until `status` is `succeeded` or `failed` (a failed page's credit is refunded automatically). A page takes about 5 seconds. There are no webhooks. Identical requests within 2 minutes return the same pages without charging again.\n\nRequest bodies are JSON (at most 64 KB; batches 256 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://colora.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, with `Retry-After` in seconds.",
    "termsOfService": "https://colora.norvane-studio.com/terms",
    "contact": {
      "name": "Colora support",
      "url": "https://colora.norvane-studio.com/support",
      "email": "norvane.studio@gmail.com"
    }
  },
  "externalDocs": {
    "description": "Colora API documentation",
    "url": "https://colora.norvane-studio.com/api"
  },
  "servers": [
    {
      "url": "https://colora-api.norvane-studio.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Pages",
      "description": "Coloring pages (1 credit each) and the library."
    },
    {
      "name": "Edits",
      "description": "New versions of a page (1 credit each)."
    },
    {
      "name": "Characters",
      "description": "Characters drawn again and again."
    },
    {
      "name": "Books",
      "description": "Coloring books (PDF export on the website)."
    },
    {
      "name": "Files",
      "description": "Uploads straight to storage through presigned URLs (photos to trace)."
    },
    {
      "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 its limits. Free.",
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "limits": {
                      "$ref": "#/components/schemas/Limits"
                    }
                  },
                  "required": [
                    "user",
                    "limits"
                  ]
                }
              }
            },
            "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/pages": {
      "get": {
        "operationId": "listPages",
        "tags": [
          "Pages"
        ],
        "summary": "List pages",
        "description": "The library, newest first (failed pages included). Free.",
        "parameters": [
          {
            "name": "batch",
            "in": "query",
            "required": false,
            "description": "Only this batch's pages.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Words in the title or the request.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Older than this `createdAt` (paging).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many (default 60).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Page"
                      }
                    }
                  },
                  "required": [
                    "pages"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "bad_request: a malformed batch or before."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      },
      "post": {
        "operationId": "createPage",
        "tags": [
          "Pages"
        ],
        "summary": "Draw a page from a description",
        "description": "Charges 1 credit per page and queues 1-4 versions (more than 1: paid plans); answers 202 with `Location` set to the first page. A page takes about 5 seconds. Failed pages are refunded; the same request within 2 minutes is not charged again.",
        "requestBody": {
          "required": true,
          "description": "What to draw.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedPages"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Location": {
                "$ref": "#/components/headers/Location"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "subject_required, bad_aspect, bad_count, unknown_character, bad_request."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email (confirm the email first), plan_required (count > 1 on the free plan)."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused: against the content rules."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity / free_capacity: today's capacity is used up."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/photo": {
      "post": {
        "operationId": "createPhotoPage",
        "tags": [
          "Pages"
        ],
        "summary": "Trace a page from a photo",
        "description": "Upload the photo first (`/v1/files`), then this charges 1 credit per version and queues the page (202, `Location`). The photo is deleted a day after its last page is made.",
        "requestBody": {
          "required": true,
          "description": "The photo and settings.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PhotoPageRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedPages"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Location": {
                "$ref": "#/components/headers/Location"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "photo_required (not an uploaded file of yours), bad_photo, bad_count, bad_aspect."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email, plan_required."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity / free_capacity."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/batch": {
      "post": {
        "operationId": "createBatch",
        "tags": [
          "Pages"
        ],
        "summary": "Draw a batch of pages",
        "description": "One page per item, 1 credit each, all with the same settings (Creator: up to 10, Pro: up to 50). Refused whole if the credits can't cover it. Get ideas first with `POST /v1/ideas` if you like.",
        "requestBody": {
          "required": true,
          "description": "The pages.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedPages"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Location": {
                "$ref": "#/components/headers/Location"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "pages_required, too_many_pages, unknown_character."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits (with how many are needed)."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "plan_required (batches need Creator or Pro, within the plan's size), verify_email."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity / free_capacity."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/{id}": {
      "get": {
        "operationId": "getPage",
        "tags": [
          "Pages"
        ],
        "summary": "Get a page (poll this)",
        "description": "A page's status. Poll every 2 seconds after queuing until `status` is `succeeded` (`imageUrl` is a 15-minute link) or `failed` (`error` says why; the credit was refunded). Free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "page": {
                      "$ref": "#/components/schemas/Page"
                    }
                  },
                  "required": [
                    "page"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      },
      "delete": {
        "operationId": "deletePage",
        "tags": [
          "Pages"
        ],
        "summary": "Delete a page",
        "description": "Deletes a finished (or failed) page and its image. Books that used it show it as missing. Credits are not returned.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "`deleted`."
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found (or still being drawn)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/{id}/download": {
      "get": {
        "operationId": "downloadPage",
        "tags": [
          "Pages"
        ],
        "summary": "Download link",
        "description": "A 15-minute link that saves the page's PNG under a readable name. Without the AI label the file carries no metadata. The website also makes SVG and print PDFs on the device. Free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloadUrl": {
                      "type": "string",
                      "description": "15-minute link.",
                      "format": "uri"
                    }
                  },
                  "required": [
                    "downloadUrl"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/{id}/versions": {
      "get": {
        "operationId": "pageVersions",
        "tags": [
          "Pages"
        ],
        "summary": "A page's versions",
        "description": "The first page of an edit chain and every edit made from it, oldest first. Free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "The versions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Page"
                      }
                    }
                  },
                  "required": [
                    "versions"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/{id}/edit": {
      "post": {
        "operationId": "editPage",
        "tags": [
          "Edits"
        ],
        "summary": "Edit a page",
        "description": "A new version of a finished page with a change in words (1 credit). The original stays.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedPages"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Location": {
                "$ref": "#/components/headers/Location"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "instruction_required."
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found (or not finished)."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity / free_capacity."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/pages/{id}/again": {
      "post": {
        "operationId": "pageAgain",
        "tags": [
          "Edits"
        ],
        "summary": "Another version",
        "description": "Draws the same request again, differently (1 credit). For a photo page, only while its photo is kept (a day).",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "202": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuedPages"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Location": {
                "$ref": "#/components/headers/Location"
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired",
            "description": "no_credits."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "410": {
            "$ref": "#/components/responses/NotFound",
            "description": "photo_deleted, parent_deleted."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "capacity / free_capacity."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/ideas": {
      "post": {
        "operationId": "pageIdeas",
        "tags": [
          "Pages"
        ],
        "summary": "Page ideas for a theme",
        "description": "Distinct page ideas (title in the account's language, scene in English) for a batch or a book. Free, within a daily allowance (free plan 10, Creator 60, Pro 100 requests a day).",
        "requestBody": {
          "required": true,
          "description": "The theme.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdeasRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ideas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ideas"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "theme_required."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "verify_email."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway",
            "description": "ai_error."
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable",
            "description": "ai_busy, ai_unavailable."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/characters": {
      "get": {
        "operationId": "listCharacters",
        "tags": [
          "Characters"
        ],
        "summary": "List characters",
        "description": "The account's saved characters and how many the plan keeps. Free.",
        "responses": {
          "200": {
            "description": "The characters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "characters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Character"
                      }
                    },
                    "max": {
                      "type": "integer",
                      "description": "How many the plan keeps."
                    }
                  },
                  "required": [
                    "characters",
                    "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 (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      },
      "post": {
        "operationId": "createCharacter",
        "tags": [
          "Characters"
        ],
        "summary": "Save a character",
        "description": "Saves the character on a finished page (its image is copied as the reference). Free; the free plan keeps 1, Creator 5, Pro 100.",
        "requestBody": {
          "required": true,
          "description": "The page and a name.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CharacterRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "character": {
                      "$ref": "#/components/schemas/Character"
                    }
                  },
                  "required": [
                    "character"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "name_required."
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "plan_required."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "409": {
            "$ref": "#/components/responses/BadRequest",
            "description": "character_limit."
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableContent",
            "description": "content_refused."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/books": {
      "get": {
        "operationId": "listBooks",
        "tags": [
          "Books"
        ],
        "summary": "List books",
        "description": "The account's books, most recently saved first. Books are built and exported to PDF on the website. Free.",
        "responses": {
          "200": {
            "description": "The books.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "books": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BookSummary"
                      }
                    }
                  },
                  "required": [
                    "books"
                  ]
                }
              }
            },
            "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 (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/books/{id}": {
      "get": {
        "operationId": "getBook",
        "tags": [
          "Books"
        ],
        "summary": "Get a book",
        "description": "A book's document and 15-minute links to its pages' images. Free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "The book.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "book": {
                      "$ref": "#/components/schemas/Book"
                    },
                    "pages": {
                      "$ref": "#/components/schemas/BookPages"
                    }
                  },
                  "required": [
                    "book",
                    "pages"
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "not_found."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized",
            "description": "invalid_api_key (the key is wrong or was revoked), unauthorized (no bearer token), invalid_token (neither a co_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, or a per-feature limit (Retry-After: seconds until it resets)."
          },
          "500": {
            "$ref": "#/components/responses/InternalError",
            "description": "db_error: a database error."
          }
        }
      }
    },
    "/v1/files/upload-url": {
      "post": {
        "operationId": "createUploadUrl",
        "tags": [
          "Files"
        ],
        "summary": "Get an upload URL",
        "description": "Step 1 of an upload (a photo to trace). 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 co_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 co_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 app offers: levels, page shapes, prices, plan limits, languages. No credentials; 600 requests an hour per IP.",
        "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://colora-api.norvane-studio.com/mcp` rather than calling it directly (setup: https://colora.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_page, plan_pages, create_batch, get_page, edit_page, list_pages, save_character, list_characters, 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": [
              "pages"
            ]
          }
        ],
        "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 Colora account: ...\"}`. Follow WWW-Authenticate to the OAuth metadata.",
            "headers": {
              "WWW-Authenticate": {
                "description": "`Bearer resource_metadata=\"https://colora-api.norvane-studio.com/.well-known/oauth-protected-resource/mcp\", scope=\"pages\"`, 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": "co_live_<32 characters>",
        "description": "An API key, `Authorization: Bearer co_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://colora-api.norvane-studio.com/oauth/register`; metadata at `https://colora-api.norvane-studio.com/.well-known/oauth-authorization-server` and `https://colora-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://colora-api.norvane-studio.com/oauth/authorize",
            "tokenUrl": "https://colora-api.norvane-studio.com/oauth/token",
            "refreshUrl": "https://colora-api.norvane-studio.com/oauth/token",
            "scopes": {
              "pages": "Use Colora as the account through the MCP tools: make and edit coloring pages with the account's credits (1 credit per page), plan pages, save characters, and see its pages, characters and credits. No access to the password, billing or API keys."
            }
          }
        }
      }
    },
    "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}$"
        }
      }
    },
    "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"
        ]
      },
      "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. Pages, ideas and the 10 free pages 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."
          },
          "locale": {
            "type": "string",
            "description": "Language of emails and the app: en, fr, es, de, pt or it.",
            "enum": [
              "en",
              "fr",
              "es",
              "de",
              "pt",
              "it"
            ]
          }
        },
        "required": [
          "id",
          "email",
          "name",
          "plan",
          "planStatus",
          "planInterval",
          "planRenewsAt",
          "credits",
          "emailVerified",
          "aiLabel",
          "createdAt",
          "locale"
        ]
      },
      "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
      },
      "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"
        ]
      },
      "Level": {
        "type": "string",
        "description": "Detail: `kids` (simple, bold lines; default), `medium` (older kids), `detailed` (adult coloring).",
        "enum": [
          "kids",
          "medium",
          "detailed"
        ]
      },
      "Aspect": {
        "type": "string",
        "description": "Page shape: `3:4` portrait for Letter and A4 (default), `4:5` for 8x10 in, `2:3` for 6x9 in, `1:1` square, `4:3` landscape. The image is about 1 megapixel (3:4 is 896x1200); the website traces it to vectors for print.",
        "enum": [
          "3:4",
          "4:5",
          "2:3",
          "1:1",
          "4:3"
        ]
      },
      "Page": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Page id.",
            "format": "uuid"
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "For an edit: the page it was made from."
          },
          "batchId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The batch it was queued in, if any."
          },
          "kind": {
            "type": "string",
            "description": "`page` (from a description, optionally with a character), `photo` (traced from a photo) or `edit`.",
            "enum": [
              "page",
              "photo",
              "edit"
            ]
          },
          "title": {
            "type": "string",
            "description": "Name in the library."
          },
          "instruction": {
            "type": "string",
            "description": "What was asked: the description, the photo note, or the edit."
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect"
          },
          "level": {
            "$ref": "#/components/schemas/Level"
          },
          "frame": {
            "type": "boolean",
            "description": "A border around the page."
          },
          "variation": {
            "type": "integer",
            "description": "0 for the first version; 1, 2... for later versions of the same request.",
            "minimum": 0
          },
          "characterId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The character drawn, if any (null once the character is deleted)."
          },
          "status": {
            "type": "string",
            "description": "`queued`, `running`, `succeeded` or `failed`.",
            "enum": [
              "queued",
              "running",
              "succeeded",
              "failed"
            ]
          },
          "error": {
            "type": "string",
            "description": "Why it failed (the credit was refunded)."
          },
          "model": {
            "type": "string",
            "description": "The image model."
          },
          "creditsCost": {
            "type": "integer",
            "description": "Credits it cost.",
            "minimum": 0
          },
          "createdAt": {
            "type": "string",
            "description": "Queued at.",
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Finished at."
          },
          "imageUrl": {
            "type": "string",
            "description": "A 15-minute link to the PNG, once succeeded.",
            "format": "uri"
          }
        },
        "description": "A coloring page.",
        "required": [
          "id",
          "kind",
          "title",
          "aspect",
          "level",
          "status",
          "createdAt"
        ]
      },
      "QueuedPages": {
        "type": "object",
        "properties": {
          "page": {
            "$ref": "#/components/schemas/Page"
          },
          "pages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Page"
            },
            "description": "Every page queued, first one first."
          },
          "credits": {
            "type": "integer",
            "description": "Credits left after charging.",
            "minimum": 0
          },
          "deduped": {
            "type": "boolean",
            "description": "True when the same request came in the last 2 minutes: the same pages, not charged again."
          },
          "partial": {
            "type": "string",
            "description": "When only some pages could be queued (credits or capacity ran out): why the rest weren't."
          },
          "batchId": {
            "type": "string",
            "description": "The batch's id (batches only).",
            "format": "uuid"
          },
          "ipNote": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Well-known characters named in the request: Colora's commercial license doesn't cover selling pages of them."
          }
        },
        "description": "Pages charged and queued. Poll each page (`GET /v1/pages/{id}`).",
        "required": [
          "page",
          "pages",
          "credits",
          "deduped"
        ]
      },
      "PageOptions": {
        "type": "object",
        "properties": {
          "level": {
            "$ref": "#/components/schemas/Level"
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect"
          },
          "frame": {
            "type": "boolean",
            "description": "Draw a border around the page (default false)."
          },
          "characterId": {
            "type": "string",
            "description": "Draw this saved character (`GET /v1/characters`).",
            "format": "uuid"
          }
        },
        "description": "Settings shared by every way of making pages.",
        "additionalProperties": false
      },
      "PageRequest": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string",
            "description": "What to draw, 3 to 500 characters. Words in quotes are lettered on the page (name pages), e.g. `the name \"EMMA\" with flowers`.",
            "minLength": 3,
            "maxLength": 500
          },
          "level": {
            "$ref": "#/components/schemas/Level"
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect"
          },
          "frame": {
            "type": "boolean",
            "description": "Draw a border around the page (default false)."
          },
          "characterId": {
            "type": "string",
            "description": "Draw this saved character in the scene.",
            "format": "uuid"
          },
          "count": {
            "type": "integer",
            "description": "Different versions to draw, 1 credit each (default 1). More than 1 needs a paid plan.",
            "minimum": 1,
            "maximum": 4
          }
        },
        "required": [
          "subject"
        ],
        "additionalProperties": false
      },
      "PhotoPageRequest": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "The photo's key from `POST /v1/files/upload-url` (then the PUT, then `POST /v1/files/confirm`). JPEG, PNG or WebP, 64 to 8,192 px a side. It is deleted a day after the page is made."
          },
          "note": {
            "type": "string",
            "description": "What to add or change (optional), e.g. `add a party hat`."
          },
          "title": {
            "type": "string",
            "description": "The page's name in the library (optional)."
          },
          "level": {
            "$ref": "#/components/schemas/Level"
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect"
          },
          "frame": {
            "type": "boolean",
            "description": "Draw a border around the page (default false)."
          },
          "count": {
            "type": "integer",
            "description": "Versions, 1 credit each (more than 1 needs a paid plan).",
            "minimum": 1,
            "maximum": 4
          }
        },
        "required": [
          "key"
        ],
        "additionalProperties": false
      },
      "PageIdea": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Short page title."
          },
          "scene": {
            "type": "string",
            "description": "What to draw, at least 3 characters."
          }
        },
        "description": "One planned page.",
        "required": [
          "scene"
        ],
        "additionalProperties": false
      },
      "BatchRequest": {
        "type": "object",
        "properties": {
          "pages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageIdea"
            },
            "minItems": 1,
            "maxItems": 50,
            "description": "One page per item (Creator: up to 10, Pro: up to 50)."
          },
          "level": {
            "$ref": "#/components/schemas/Level"
          },
          "aspect": {
            "$ref": "#/components/schemas/Aspect"
          },
          "frame": {
            "type": "boolean",
            "description": "Draw a border around every page."
          },
          "characterId": {
            "type": "string",
            "description": "Draw this saved character on every page.",
            "format": "uuid"
          }
        },
        "required": [
          "pages"
        ],
        "additionalProperties": false
      },
      "IdeasRequest": {
        "type": "object",
        "properties": {
          "theme": {
            "type": "string",
            "description": "The theme, e.g. `ocean animals for preschoolers`.",
            "minLength": 3,
            "maxLength": 300
          },
          "level": {
            "$ref": "#/components/schemas/Level"
          },
          "count": {
            "type": "integer",
            "description": "How many ideas (default 1).",
            "minimum": 1,
            "maximum": 50
          }
        },
        "required": [
          "theme"
        ],
        "additionalProperties": false
      },
      "Ideas": {
        "type": "object",
        "properties": {
          "pages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageIdea"
            }
          },
          "left": {
            "type": "integer",
            "description": "Idea requests left today.",
            "minimum": 0
          }
        },
        "required": [
          "pages",
          "left"
        ]
      },
      "EditRequest": {
        "type": "object",
        "properties": {
          "instruction": {
            "type": "string",
            "description": "What to change, e.g. `add a dragon on the tower`.",
            "minLength": 3,
            "maxLength": 500
          }
        },
        "required": [
          "instruction"
        ],
        "additionalProperties": false
      },
      "Character": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Character id.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Name."
          },
          "note": {
            "type": "string",
            "description": "A few words about them."
          },
          "createdAt": {
            "type": "string",
            "description": "Saved at.",
            "format": "date-time"
          },
          "imageUrl": {
            "type": "string",
            "description": "A 15-minute link to the reference image.",
            "format": "uri"
          }
        },
        "description": "A character drawn again and again.",
        "required": [
          "id",
          "name",
          "note",
          "createdAt"
        ]
      },
      "CharacterRequest": {
        "type": "object",
        "properties": {
          "pageId": {
            "type": "string",
            "description": "A finished page showing the character; its image becomes the reference.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Name, up to 40 characters.",
            "maxLength": 40
          },
          "note": {
            "type": "string",
            "description": "A few words about them (optional).",
            "maxLength": 200
          }
        },
        "required": [
          "pageId",
          "name"
        ],
        "additionalProperties": false
      },
      "BookSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Book id.",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "description": "Title."
          },
          "pages": {
            "type": "integer",
            "description": "Library pages it uses.",
            "minimum": 0
          },
          "createdAt": {
            "type": "string",
            "description": "Created.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last saved.",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "title",
          "pages",
          "createdAt",
          "updatedAt"
        ]
      },
      "Book": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Book id.",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "description": "Title."
          },
          "doc": {
            "type": "object",
            "description": "The book as the website lays it out: `{\"v\": 1, \"trim\", \"bleed\", \"blankBacks\", \"items\": [{\"t\": \"page\", \"id\"} | {\"t\": \"maze\" | \"words\" | \"dots\" | \"title\" | \"text\", ...}]}`. Activity pages are rebuilt on the device from their settings.",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "description": "Created.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last saved.",
            "format": "date-time"
          }
        },
        "description": "A coloring book. Its PDF is made on the website (free).",
        "required": [
          "id",
          "title",
          "doc",
          "createdAt",
          "updatedAt"
        ]
      },
      "BookPages": {
        "type": "object",
        "description": "The book's library pages by id, with 15-minute image links; deleted pages are missing.",
        "additionalProperties": {
          "type": "object",
          "properties": {
            "imageUrl": {
              "type": "string",
              "description": "15-minute link.",
              "format": "uri"
            },
            "title": {
              "type": "string",
              "description": "Title."
            },
            "aspect": {
              "$ref": "#/components/schemas/Aspect"
            },
            "status": {
              "type": "string",
              "description": "Status."
            }
          }
        }
      },
      "Limits": {
        "type": "object",
        "properties": {
          "batch": {
            "type": "integer",
            "description": "Pages per batch request."
          },
          "versions": {
            "type": "integer",
            "description": "Versions per click."
          },
          "characters": {
            "type": "integer",
            "description": "Saved characters."
          },
          "ideasDaily": {
            "type": "integer",
            "description": "Idea requests per UTC day."
          },
          "apiKeys": {
            "type": "boolean",
            "description": "Whether the plan includes API keys."
          }
        },
        "description": "What a plan allows beyond credits."
      },
      "Catalog": {
        "type": "object",
        "properties": {
          "levels": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Id."
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                }
              }
            }
          },
          "aspects": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Id."
                },
                "label": {
                  "type": "string",
                  "description": "Label."
                }
              }
            }
          },
          "signupCredits": {
            "type": "integer",
            "description": "Free pages on email confirmation, once per inbox."
          },
          "creditCost": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Credits per action."
          },
          "limits": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Limits"
            },
            "description": "By plan: free, creator, pro."
          },
          "plans": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Paid plans with prices (USD cents) and monthly credits."
          },
          "locales": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "supportEmail": {
            "type": "string",
            "description": "Support address.",
            "format": "email"
          }
        },
        "description": "Options, prices and limits, as the app shows them."
      }
    },
    "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 co_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=\"colora\"` (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); ideas_limit: today's page-idea requests are used up (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), 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 `ideas_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 page to poll with getPage (`https://colora-api.norvane-studio.com/v1/pages/{id}`), the first one when the request queued several.",
        "schema": {
          "type": "string",
          "format": "uri"
        }
      }
    }
  }
}
