{
  "openapi": "3.1.0",
  "info": {
    "title": "cfy-webservice",
    "version": "1",
    "summary": "Classify chemical structures (SMILES) into the ChemOnt taxonomy, ClassyFire-style.",
    "description": "Clean-room ChemOnt classifier. POST one SMILES or a batch to /classify. JSON in, JSON out, no authentication, no rate limit. Live limits and rule versions are at GET /version. Agent guide: GET /llms.txt."
  },
  "servers": [{"url": "/"}],
  "paths": {
    "/classify": {
      "post": {
        "operationId": "classify",
        "summary": "Classify one SMILES or a batch",
        "description": "Send {\"smiles\": \"...\"} for one compound (response: a Classification) or {\"smiles\": [\"...\", ...]} for a batch of up to max_batch compounds (response: a BatchResult with one entry per input, in input order). A bad SMILES inside a batch becomes an item error and never fails the batch.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/ClassifyRequest"},
              "examples": {
                "single": {"value": {"smiles": "CN1C=NC2=C1C(=O)N(C)C(=O)N2C"}},
                "batch": {"value": {"smiles": ["CCO", "not a smiles", "c1ccccc1O"]}}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A Classification for a single SMILES, or a BatchResult for a list.",
            "content": {"application/json": {"schema": {"oneOf": [
              {"$ref": "#/components/schemas/Classification"},
              {"$ref": "#/components/schemas/BatchResult"}
            ]}}}
          },
          "400": {"$ref": "#/components/responses/Error"},
          "413": {"$ref": "#/components/responses/Error"},
          "415": {"$ref": "#/components/responses/Error"},
          "422": {"$ref": "#/components/responses/Error"},
          "500": {"$ref": "#/components/responses/Error"},
          "503": {"$ref": "#/components/responses/Error"},
          "504": {"$ref": "#/components/responses/Error"}
        }
      }
    },
    "/version": {
      "get": {
        "operationId": "version",
        "summary": "Service, engine and rule versions, and the live limits",
        "responses": {"200": {"description": "Version info", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Version"}}}}}
      }
    },
    "/healthz": {
      "get": {
        "operationId": "health",
        "summary": "Liveness check",
        "responses": {"200": {"description": "Alive", "content": {"application/json": {"schema": {
          "type": "object", "properties": {"status": {"type": "string", "const": "ok"}}, "required": ["status"]}}}}}
      }
    }
  },
  "components": {
    "schemas": {
      "ClassifyRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["smiles"],
        "properties": {
          "smiles": {
            "oneOf": [
              {"type": "string", "maxLength": 2000, "description": "One SMILES."},
              {"type": "array", "minItems": 1, "maxItems": 1000, "items": {"type": "string", "maxLength": 2000}, "description": "A batch of SMILES."}
            ]
          }
        }
      },
      "Node": {
        "type": "object",
        "required": ["name", "chemont_id"],
        "properties": {
          "name": {"type": "string", "examples": ["Xanthines"]},
          "chemont_id": {"type": "string", "pattern": "^CHEMONTID:[0-9]{7}$", "examples": ["CHEMONTID:0000247"]}
        }
      },
      "Classification": {
        "type": "object",
        "description": "Same fields, in the same order, as the cfy CLI's JSON lines (without id). Levels are null when the lineage is shorter; all are null when nothing matched.",
        "required": ["smiles", "inchikey", "kingdom", "superclass", "class", "subclass", "intermediate_nodes", "direct_parent", "alternative_parents"],
        "properties": {
          "smiles": {"type": "string", "description": "The input SMILES, echoed as sent."},
          "inchikey": {"type": ["string", "null"], "description": "Reserved; always null in this build."},
          "kingdom": {"oneOf": [{"$ref": "#/components/schemas/Node"}, {"type": "null"}]},
          "superclass": {"oneOf": [{"$ref": "#/components/schemas/Node"}, {"type": "null"}]},
          "class": {"oneOf": [{"$ref": "#/components/schemas/Node"}, {"type": "null"}]},
          "subclass": {"oneOf": [{"$ref": "#/components/schemas/Node"}, {"type": "null"}]},
          "intermediate_nodes": {"type": "array", "items": {"$ref": "#/components/schemas/Node"}, "description": "Lineage nodes between subclass and direct_parent."},
          "direct_parent": {"oneOf": [{"$ref": "#/components/schemas/Node"}, {"type": "null"}], "description": "The most specific category chosen."},
          "alternative_parents": {"type": "array", "items": {"$ref": "#/components/schemas/Node"}, "description": "Other most-specific categories the compound matches."}
        }
      },
      "ItemError": {
        "type": "object",
        "required": ["smiles", "error"],
        "properties": {
          "smiles": {"type": "string"},
          "error": {"type": "object", "required": ["code", "message"], "properties": {
            "code": {"type": "string", "enum": ["invalid_smiles", "smiles_too_long", "molecule_too_large", "internal_error"]},
            "message": {"type": "string"}
          }}
        }
      },
      "BatchResult": {
        "type": "object",
        "required": ["count", "results"],
        "properties": {
          "count": {"type": "integer"},
          "results": {"type": "array", "items": {"oneOf": [{"$ref": "#/components/schemas/Classification"}, {"$ref": "#/components/schemas/ItemError"}]}}
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {"error": {"type": "object", "required": ["code", "message"], "properties": {
          "code": {"type": "string", "enum": ["invalid_json", "not_found", "method_not_allowed", "body_too_large", "batch_too_large", "unsupported_media_type", "invalid_request", "invalid_smiles", "smiles_too_long", "molecule_too_large", "internal_error", "overloaded", "request_timeout"], "description": "Stable; match on this."},
          "message": {"type": "string", "description": "Human-readable; may change."}
        }}}
      },
      "Version": {
        "type": "object",
        "properties": {
          "service": {"type": "string"},
          "service_version": {"type": "string"},
          "api_version": {"type": "string"},
          "engine_version": {"type": "string"},
          "engine_git": {"type": "string"},
          "rules_git_sha": {"type": "string", "description": "Classifications can change when this changes; store it with results."},
          "rdkit": {"type": "string"},
          "limits": {"type": "object", "properties": {
            "max_body_bytes": {"type": "integer"},
            "max_batch": {"type": "integer"},
            "max_smiles_length": {"type": "integer"},
            "max_atoms": {"type": "integer"},
            "request_timeout_ms": {"type": "integer"}
          }}
        }
      }
    },
    "responses": {
      "Error": {"description": "Error with a stable code", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    }
  }
}
