{
  "openapi": "3.1.0",
  "info": {
    "title": "Cubology public reference and logic API",
    "version": "1.0.0",
    "description": "Free public cubing data and bounded puzzle computation. Any direct or derived user-facing use must visibly credit Cubology and preserve upstream source credits. See the attribution terms. Anonymous quotas: 10 requests per rolling 10 seconds and 50 per rolling 10 minutes. API-key quotas: 20 and 100, shared across REST and MCP and bounded by account and IP.",
    "termsOfService": "https://cubology.app/attribution.md"
  },
  "servers": [{ "url": "https://api.cubology.app" }],
  "externalDocs": { "url": "https://cubology.app/api.md" },
  "security": [{}, { "ApiKey": [] }],
  "paths": {
    "/v1/catalog": {
      "get": {
        "operationId": "getCatalog",
        "summary": "List puzzles, methods, sets, checks and provenance",
        "responses": {
          "200": { "$ref": "#/components/responses/Attributed" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Throttled" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/search": {
      "get": {
        "operationId": "searchCatalog",
        "summary": "Find sets and cases by puzzle, method, name or alias",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "1-200 UTF-8 bytes; every whitespace-delimited word must match",
            "schema": { "type": "string", "minLength": 1, "maxLength": 200 }
          },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Attributed" },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Throttled" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/sets/{set}": {
      "get": {
        "operationId": "getSet",
        "summary": "Read set metadata and a page of source-attributed cases",
        "parameters": [
          { "$ref": "#/components/parameters/Set" },
          { "$ref": "#/components/parameters/Limit" },
          {
            "name": "offset",
            "in": "query",
            "schema": { "type": "integer", "minimum": 0, "maximum": 100000, "default": 0 }
          }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Attributed" },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Throttled" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/sets/{set}/cases/{case}": {
      "get": {
        "operationId": "getCase",
        "summary": "Read one case, algorithms, pre-turns and source credits",
        "parameters": [
          { "$ref": "#/components/parameters/Set" },
          { "name": "case", "in": "path", "required": true, "schema": { "type": "string" }, "example": "aa" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Attributed" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Throttled" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/logic/{operation}": {
      "post": {
        "operationId": "compute",
        "summary": "Transform algorithms, apply moves or check method milestones",
        "description": "32 KiB body limit. Space-separated single moves only, at most 256 moves / 2048 UTF-8 bytes. No grouped repeats or commutators. alg accepts op/rotation, not facelets/method/hold. apply accepts facelets, not op/rotation/method/hold. state accepts facelets/method/hold, not op/rotation. Unknown JSON fields are rejected.",
        "parameters": [
          {
            "name": "operation",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "enum": ["alg", "apply", "state"] }
          }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LogicInput" } } }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Attributed" },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Throttled" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/api-keys": {
      "get": {
        "operationId": "listApiKeys",
        "summary": "List this account's key metadata, never secret values",
        "security": [{ "AccountSession": [] }],
        "responses": {
          "200": {
            "description": "Key list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["keys"],
                  "properties": {
                    "keys": { "type": "array", "items": { "$ref": "#/components/schemas/KeyMetadata" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "summary": "Create a public-read/compute API key, displayed once",
        "security": [{ "AccountSession": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "additionalProperties": false,
                "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 80 } }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Save token now; it cannot be retrieved later",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/KeyMetadata" },
                    {
                      "type": "object",
                      "required": ["token", "scope"],
                      "properties": {
                        "token": { "type": "string", "readOnly": true },
                        "scope": { "type": "string" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "409": { "description": "Five keys already exist; revoke one first" }
        }
      }
    },
    "/v1/api-keys/{id}": {
      "delete": {
        "operationId": "revokeApiKey",
        "summary": "Immediately revoke an owned key",
        "security": [{ "AccountSession": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "204": { "description": "Key revoked" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Account-issued ca_ key. Public reference/compute only. Never use a session token here."
      },
      "AccountSession": {
        "type": "http",
        "scheme": "bearer",
        "description": "Existing account login session; required only for key management."
      }
    },
    "parameters": {
      "Set": {
        "name": "set",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "example": "pll"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum items; defaults to 20 for search, 25 for set browsing",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50 }
      }
    },
    "schemas": {
      "AttributedResponse": {
        "type": "object",
        "required": ["data", "attribution"],
        "properties": {
          "data": {},
          "attribution": {
            "type": "object",
            "required": ["required", "name", "url", "terms", "notice"],
            "properties": {
              "required": { "const": true },
              "name": { "const": "Cubology" },
              "url": { "const": "https://cubology.app" },
              "terms": { "const": "https://cubology.app/attribution.md" },
              "notice": { "type": "string" }
            }
          }
        }
      },
      "LogicInput": {
        "type": "object",
        "required": ["puzzle"],
        "additionalProperties": false,
        "properties": {
          "puzzle": {
            "type": "string",
            "enum": ["2x2", "3x3", "4x4", "5x5", "6x6", "7x7", "pyraminx", "skewb", "fto", "megaminx"]
          },
          "alg": { "type": "string", "maxLength": 2048 },
          "facelets": { "type": "string", "maxLength": 1024 },
          "op": { "type": "string", "enum": ["", "simplify", "invert", "mirror", "fixed", "reorient"] },
          "rotation": { "type": "string", "maxLength": 32 },
          "method": { "type": "string", "maxLength": 64 },
          "hold": { "type": "string", "maxLength": 32 }
        }
      },
      "KeyMetadata": {
        "type": "object",
        "required": ["id", "name", "createdAt"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "createdAt": { "type": "integer", "description": "Unix milliseconds" }
        }
      }
    },
    "responses": {
      "Attributed": {
        "description": "Successful data or computation; visible attribution required",
        "headers": {
          "RateLimit-Policy": { "schema": { "type": "string" } },
          "Link": { "schema": { "type": "string" } }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AttributedResponse" } } }
      },
      "Error": {
        "description": "Request rejected",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["error"],
              "properties": { "error": { "type": "string" } }
            }
          }
        }
      },
      "Throttled": {
        "description": "Rate limit reached; wait before retrying",
        "headers": {
          "Retry-After": { "schema": { "type": "integer", "minimum": 1 } },
          "RateLimit-Policy": { "schema": { "type": "string" } }
        }
      },
      "Unavailable": {
        "description": "Temporary authentication, service or computation capacity pressure",
        "headers": { "Retry-After": { "schema": { "type": "integer", "minimum": 1 } } }
      }
    }
  }
}
