{
  "openapi": "3.0.3",
  "info": {
    "title": "uml.singsk.com PlantUML service API",
    "version": "1.1.0.2",
    "description": "AI-first PlantUML rendering service. Input: PlantUML source text. Output: SVG or PNG diagram. Includes encode/decode for PlantUML URL tokens, a render-verified syntax catalog, health, about, and version endpoints. Discovery document at /api/; this spec at /api/openapi.json; Swagger UI at /api/docs. Copyright (C) SingSK - 2025.",
    "contact": {
      "url": "https://uml.singsk.com/api/"
    },
    "license": {
      "name": "Copyright (C) SingSK - 2025",
      "url": "https://uml.singsk.com/about.html"
    }
  },
  "servers": [
    {
      "url": "https://uml.singsk.com",
      "description": "Unversioned paths (permanent aliases of v1)"
    },
    {
      "url": "https://uml.singsk.com/api/v1",
      "description": "Canonical v1 prefix: /api/v1/render, /api/v1/validate, ... (same operations as /api/*)"
    }
  ],
  "tags": [
    {
      "name": "discovery",
      "description": "Self-description for AI agents"
    },
    {
      "name": "render",
      "description": "PlantUML text -> diagram image"
    },
    {
      "name": "encoding",
      "description": "PlantUML URL token encode/decode"
    },
    {
      "name": "reference",
      "description": "Syntax and help catalogs"
    },
    {
      "name": "ops",
      "description": "Health and version"
    }
  ],
  "paths": {
    "/api/": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getDiscovery",
        "summary": "AI-first discovery document",
        "description": "Machine-readable description of the whole service: endpoints, UI behaviour (left/right panels, zoom), syntax catalog, help links, limits, quick-start for agents.",
        "responses": {
          "200": {
            "description": "Discovery document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/openapi.json": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getOpenapi",
        "summary": "This OpenAPI 3.0 specification",
        "responses": {
          "200": {
            "description": "OpenAPI document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "ops"
        ],
        "operationId": "getHealth",
        "summary": "End-to-end health check",
        "description": "Renders a one-line diagram through the real engine and reports status, render latency, version and uptime. status is \"ok\" or \"degraded\".",
        "responses": {
          "200": {
            "description": "Healthy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Renderer unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/api/render": {
      "post": {
        "tags": [
          "render"
        ],
        "operationId": "render",
        "summary": "Render PlantUML source to SVG or PNG",
        "description": "The core function. Send PlantUML source text; receive the rendered diagram. format defaults to svg. PlantUML syntax errors still return an image (200) containing the error message - call POST /api/validate first (or use /api/render/batch, which validates per item) to get a machine-readable error instead. Responses carry a strong ETag and Cache-Control: public, max-age=86400, immutable; send If-None-Match to receive 304 for a repeated source. Every response carries RateLimit-* headers.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              },
              "example": {
                "text": "@startuml\nAlice -> Bob: hello\n@enduml",
                "format": "svg"
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              },
              "example": "@startuml\nAlice -> Bob: hello\n@enduml"
            }
          }
        },
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "svg",
                "png"
              ]
            },
            "description": "Used when the body is text/plain; JSON body format field wins otherwise."
          }
        ],
        "responses": {
          "200": {
            "description": "Rendered diagram",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Missing/empty text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Renderer unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/validate": {
      "post": {
        "tags": [
          "render"
        ],
        "operationId": "validate",
        "summary": "Validate PlantUML source without rendering an image",
        "description": "Cheap syntax check. 200 {ok:true, diagram_type, warnings[]} when the engine accepts the source; 422 with a machine-readable error (code, message, 1-based line/column relative to the submitted text, source_line, hint, diagram_type_detected, help link) otherwise. Use it in a generate -> validate -> fix loop before rendering.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidateRequest"
              },
              "example": {
                "text": "@startuml\nAlice -> Bob: hello\n@enduml"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Valid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationOk"
                }
              }
            }
          },
          "422": {
            "description": "Invalid - structured error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/render/batch": {
      "post": {
        "tags": [
          "render"
        ],
        "operationId": "renderBatch",
        "summary": "Render up to 20 diagrams in one call",
        "description": "Each item is validated then rendered; item-level failures do not fail the request (partial success). svg items return the SVG text, png items return png_base64. Every item echoes its id and carries the share url and ETag.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              },
              "example": {
                "diagrams": [
                  {
                    "id": "fig-1",
                    "text": "@startuml\nA -> B\n@enduml",
                    "format": "svg"
                  },
                  {
                    "id": "fig-2",
                    "text": "@startmindmap\n* root\n@endmindmap",
                    "format": "png"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or too many items",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats": {
      "get": {
        "tags": [
          "ops"
        ],
        "operationId": "getStats",
        "summary": "Aggregate agent telemetry",
        "description": "Counts by endpoint, status, error code, diagram type, agent family and entry point since process start. No IPs, user agents or sources are stored.",
        "responses": {
          "200": {
            "description": "Telemetry snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/encode": {
      "post": {
        "tags": [
          "encoding"
        ],
        "operationId": "encode",
        "summary": "PlantUML source -> URL token",
        "description": "Token algorithm: UTF-8 -> raw DEFLATE (level 9, no zlib header) -> every 3 bytes mapped to 4 characters of PlantUML's own 64-character alphabet \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_\", no padding. This is PlantUML's custom encoding — NOT base64; a base64 codec will not produce valid tokens. Verified against the reference encoder, the local engine, and the official plantuml.com server. The returned urls render directly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EncodeRequest"
              },
              "example": {
                "text": "@startuml\nA -> B\n@enduml"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token + ready URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EncodeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing text",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "encoding"
        ],
        "operationId": "encodeGet",
        "summary": "Encode via query string (short sources)",
        "parameters": [
          {
            "name": "text",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "URL-encoded PlantUML source"
          }
        ],
        "responses": {
          "200": {
            "description": "Token + ready URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EncodeResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/decode": {
      "post": {
        "tags": [
          "encoding"
        ],
        "operationId": "decode",
        "summary": "URL token -> PlantUML source",
        "description": "Accepts a bare token or a full render URL (the token is extracted). Reverses /api/encode.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DecodeRequest"
              },
              "example": {
                "encoded": "SoWkIImgAStDuNBCoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk42oYde0jM05MDHLLoGdrUS2W00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Original source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DecodeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "encoding"
        ],
        "operationId": "decodeGet",
        "summary": "Decode via query string",
        "parameters": [
          {
            "name": "src",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token or full render URL"
          }
        ],
        "responses": {
          "200": {
            "description": "Original source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DecodeResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/syntax": {
      "get": {
        "tags": [
          "reference"
        ],
        "operationId": "getSyntax",
        "summary": "Diagram-type catalog with verified examples",
        "description": "All 22 diagram types the bundled help documents, each with description, help page URL, and an example extracted from that help page and verified to render on this server.",
        "responses": {
          "200": {
            "description": "Syntax catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SyntaxCatalog"
                }
              }
            }
          }
        }
      }
    },
    "/api/help": {
      "get": {
        "tags": [
          "reference"
        ],
        "operationId": "getHelp",
        "summary": "Help page catalog",
        "responses": {
          "200": {
            "description": "Help links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/about": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "getAbout",
        "summary": "About this project",
        "description": "What the tool is, what it does, links, hashtags, and copyright (Copyright (C) SingSK - 2025). Human versions: /about.html, /about.md; raw JSON also at /about.json.",
        "responses": {
          "200": {
            "description": "About document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/plantuml/{format}/": {
      "get": {
        "tags": [
          "render"
        ],
        "operationId": "renderNative",
        "summary": "Native render endpoint (token form)",
        "description": "The engine endpoint the editor itself uses. Hard transport limit: the src token must be at most 16000 characters (about a 16 KB URL); longer requests answer 414 with {ok:false, error:{code:\"URL_TOO_LONG\"}}. Check url_ok from POST /api/encode; for larger diagrams use POST /api/render (5 MB body) or /api/render/batch.",
        "parameters": [
          {
            "name": "format",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "svg",
                "png",
                "txt",
                "pdf"
              ]
            }
          },
          {
            "name": "src",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 16000
            },
            "description": "Token from /api/encode (max 16000 characters)"
          }
        ],
        "responses": {
          "200": {
            "description": "Rendered diagram"
          },
          "414": {
            "description": "Token longer than 16000 characters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/version.json": {
      "get": {
        "tags": [
          "ops"
        ],
        "operationId": "getVersion",
        "summary": "Version single source of truth",
        "responses": {
          "200": {
            "description": "Version info",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Version"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "RenderRequest": {
        "type": "object",
        "required": [
          "text"
        ],
        "properties": {
          "text": {
            "type": "string",
            "description": "PlantUML source, including @start/@end directives"
          },
          "format": {
            "type": "string",
            "enum": [
              "svg",
              "png"
            ],
            "default": "svg"
          }
        }
      },
      "EncodeRequest": {
        "type": "object",
        "required": [
          "text"
        ],
        "properties": {
          "text": {
            "type": "string"
          }
        }
      },
      "EncodeResponse": {
        "type": "object",
        "properties": {
          "encoded": {
            "type": "string",
            "description": "PlantUML URL token"
          },
          "token_length": {
            "type": "integer"
          },
          "url_ok": {
            "type": "boolean",
            "description": "true when the token fits the GET URL limit (url_limit). When false the urls will answer 414 - use POST /api/render instead."
          },
          "url_limit": {
            "type": "integer",
            "description": "Maximum token length accepted on GET ?src= URLs (16000)"
          },
          "urls": {
            "type": "object",
            "properties": {
              "svg": {
                "type": "string"
              },
              "png": {
                "type": "string"
              }
            }
          },
          "warning": {
            "type": "string",
            "description": "Present only when url_ok is false"
          }
        }
      },
      "DecodeRequest": {
        "type": "object",
        "required": [
          "encoded"
        ],
        "properties": {
          "encoded": {
            "type": "string",
            "description": "Token or full render URL"
          }
        }
      },
      "DecodeResponse": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          }
        }
      },
      "SyntaxCatalog": {
        "type": "object",
        "properties": {
          "verified": {
            "type": "string"
          },
          "types": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "help_url": {
                  "type": "string"
                },
                "example": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "version": {
            "type": "string"
          },
          "renderer": {
            "type": "object",
            "properties": {
              "ok": {
                "type": "boolean"
              },
              "latency_ms": {
                "type": "integer"
              }
            }
          },
          "uptime_s": {
            "type": "integer"
          }
        }
      },
      "Version": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "released": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "ApiError": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "EMPTY_SOURCE",
                  "MISSING_START_TAG",
                  "UNKNOWN_DIAGRAM_TYPE",
                  "SYNTAX_ERROR",
                  "TOO_LARGE",
                  "TIMEOUT",
                  "ENGINE_ERROR",
                  "RATE_LIMITED",
                  "BAD_FORMAT",
                  "URL_TOO_LONG"
                ]
              },
              "message": {
                "type": "string"
              },
              "line": {
                "type": "integer",
                "description": "1-based, relative to the submitted text"
              },
              "column": {
                "type": "integer",
                "description": "1-based, when known"
              },
              "source_line": {
                "type": "string"
              },
              "hint": {
                "type": "string",
                "description": "How to fix it"
              },
              "diagram_type_detected": {
                "type": "string"
              },
              "help": {
                "type": "string",
                "format": "uri"
              },
              "retry_after_s": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ValidateRequest": {
        "type": "object",
        "required": [
          "text"
        ],
        "properties": {
          "text": {
            "type": "string"
          }
        }
      },
      "ValidationOk": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "diagram_type": {
            "type": "string"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "line": {
                  "type": "integer"
                },
                "code": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "diagrams"
        ],
        "properties": {
          "diagrams": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "object",
              "required": [
                "text"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "text": {
                  "type": "string"
                },
                "format": {
                  "type": "string",
                  "enum": [
                    "svg",
                    "png"
                  ],
                  "default": "svg"
                }
              }
            }
          }
        }
      },
      "BatchResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "true only when every item succeeded"
          },
          "count": {
            "type": "integer"
          },
          "succeeded": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                },
                "format": {
                  "type": "string"
                },
                "content_type": {
                  "type": "string"
                },
                "diagram_type": {
                  "type": "string"
                },
                "etag": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "svg": {
                  "type": "string"
                },
                "png_base64": {
                  "type": "string"
                },
                "error": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  }
}