{
  "openapi": "3.1.0",
  "info": {
    "title": "Minh Spark Public API",
    "version": "1.0.0",
    "summary": "Read-only HTTP surface of minhspark.com for agents and developer tooling.",
    "description": "Public, unauthenticated, read-only endpoints of minhspark.com, the portfolio and developer site of Bui Dang Minh (Minh Spark).\n\nEvery operation here is safe to call without credentials and returns JSON, Markdown, plain text or XML. Errors always come back as JSON matching ApiError, never as an HTML page.\n\nWrite operations (checkout, orders, payments, subscriptions, guestbook submissions, contact messages) exist on this host but are deliberately excluded from this document: they create side effects for a human and are not meant for autonomous agents. Point a person at the matching page instead.\n\nOperational endpoints are excluded too. Service health lives at /api/health for this site's own monitoring: it probes the database, cache, payment and mail dependencies on every call, so it is not published for agents to poll.\n\nRate limits are per-IP and generous for discovery traffic. A 429 with code rate_limited means back off and retry later.",
    "contact": {
      "name": "Minh Spark",
      "url": "https://minhspark.com/contact/"
    },
    "license": {
      "name": "Content is published for public reading",
      "url": "https://minhspark.com/chinh-sach/"
    },
    "termsOfService": "https://minhspark.com/chinh-sach/"
  },
  "servers": [
    {
      "url": "https://minhspark.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Minh Spark developer resources",
    "url": "https://minhspark.com/developers/"
  },
  "tags": [
    {
      "name": "Discovery",
      "description": "Machine-readable descriptions of this site and its API."
    },
    {
      "name": "Content",
      "description": "Page content in agent-friendly Markdown."
    },
    {
      "name": "Community",
      "description": "Public, moderated reader content."
    },
    {
      "name": "Analytics",
      "description": "Public page view counters."
    },
    {
      "name": "MCP",
      "description": "Model Context Protocol endpoint for tool-using clients."
    }
  ],
  "paths": {
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "summary": "Get this OpenAPI document",
        "description": "Return the OpenAPI 3.1 description of every public Minh Spark endpoint. Fetch this first to discover the API surface, then call the operations it lists.",
        "tags": [
          "Discovery"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.1 document.",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getAgentGuidance",
        "summary": "Get agent guidance",
        "description": "Return the llms.txt file: when to use this site, which resources are canonical, and which endpoints agents may call.",
        "tags": [
          "Discovery"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Agent guidance in the llms.txt format.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "summary": "List public page URLs",
        "description": "Return the XML sitemap with every public Vietnamese and English page URL, their last modification dates and hreflang pairs. Use it to enumerate readable content.",
        "tags": [
          "Discovery"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "A sitemaps.org urlset.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/developers.md": {
      "get": {
        "operationId": "getDeveloperGuide",
        "summary": "Get the developer guide as Markdown",
        "description": "Return the developer guide: MCP connection steps, request headers, worked tool-call examples and integration boundaries.",
        "tags": [
          "Content"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Page content as Markdown, without navigation chrome or scripts.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document that starts with a single level-one heading."
                }
              }
            },
            "headers": {
              "Link": {
                "description": "Points at the agent guidance file that describes this site.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "</llms.txt>; rel=\"describedby\""
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/index.md": {
      "get": {
        "operationId": "getHomepageMarkdown",
        "summary": "Get the Vietnamese homepage as Markdown",
        "description": "Return the Vietnamese homepage content as Markdown: services, products and portfolio summary.",
        "tags": [
          "Content"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Page content as Markdown, without navigation chrome or scripts.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document that starts with a single level-one heading."
                }
              }
            },
            "headers": {
              "Link": {
                "description": "Points at the agent guidance file that describes this site.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "</llms.txt>; rel=\"describedby\""
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/en/index.md": {
      "get": {
        "operationId": "getEnglishHomepageMarkdown",
        "summary": "Get the English homepage as Markdown",
        "description": "Return the English homepage content as Markdown: services, products and portfolio summary.",
        "tags": [
          "Content"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Page content as Markdown, without navigation chrome or scripts.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document that starts with a single level-one heading."
                }
              }
            },
            "headers": {
              "Link": {
                "description": "Points at the agent guidance file that describes this site.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "</llms.txt>; rel=\"describedby\""
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/{page}.md": {
      "get": {
        "operationId": "getPageMarkdown",
        "summary": "Get any public page as Markdown",
        "description": "Append .md to any public page path to read it as Markdown. Equivalent to requesting the HTML page with Accept: text/markdown. Unknown paths return Markdown pointing back at the sitemap.",
        "tags": [
          "Content"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "path",
            "required": true,
            "description": "Public page path without the .md suffix and without a leading slash.",
            "schema": {
              "type": "string",
              "examples": [
                "about",
                "blog/index",
                "en/portfolio/index"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page content as Markdown, without navigation chrome or scripts.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document that starts with a single level-one heading."
                }
              }
            },
            "headers": {
              "Link": {
                "description": "Points at the agent guidance file that describes this site.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "</llms.txt>; rel=\"describedby\""
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such page. The body links the sitemap and agent guidance in Markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/guestbook": {
      "get": {
        "operationId": "listGuestbookEntries",
        "summary": "List approved guestbook entries",
        "description": "Return up to 100 moderated public guestbook entries, newest first. Only approved entries are visible; pending and rejected ones are never returned.",
        "tags": [
          "Community"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Approved entries, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuestbookEntries"
                }
              }
            }
          },
          "405": {
            "description": "Only GET is documented for agents. Posting an entry is a human action on the guestbook page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "The backend is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "502": {
            "description": "The storage backend rejected the read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/views": {
      "get": {
        "operationId": "getPageViews",
        "summary": "Get public page view counts",
        "description": "Return view counters for the requested page identifiers. Unknown identifiers come back as 0 rather than an error, so a batch read never fails because of one bad id. POST on this path is reserved for the site's own first-party tracking and is not for agents.",
        "tags": [
          "Analytics"
        ],
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": false,
            "description": "Comma-separated page identifiers, at most 50. Omit to receive an empty object.",
            "schema": {
              "type": "string",
              "examples": [
                "blog-post-slug,another-slug"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A counter per requested identifier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ViewCounts"
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "operationId": "callMcpEndpoint",
        "summary": "Call the public MCP endpoint",
        "description": "Model Context Protocol endpoint over Streamable HTTP, JSON responses, stateless, no authentication. Tools: get_site_info and list_developer_resources, both read-only and taking empty arguments. Prefer a real MCP client over hand-rolled JSON-RPC; the developer guide shows the initialize handshake.",
        "tags": [
          "MCP"
        ],
        "parameters": [
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "required": false,
            "description": "Protocol version negotiated during initialize.",
            "schema": {
              "type": "string",
              "examples": [
                "2025-11-25"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "A single JSON-RPC 2.0 request or notification.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A JSON-RPC 2.0 response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "202": {
            "description": "A notification was accepted. The body is empty."
          },
          "400": {
            "description": "Malformed JSON-RPC message or unsupported protocol version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "403": {
            "description": "The Origin header is not an allowed site origin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "405": {
            "description": "Only POST and OPTIONS are supported; there is no standalone SSE stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "406": {
            "description": "Accept must include application/json and text/event-stream.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiError": {
        "type": "object",
        "title": "ApiError",
        "description": "Machine-readable error body returned by every Minh Spark API route. Branch on code, show message, follow hint to recover.",
        "required": [
          "error",
          "code",
          "message",
          "hint",
          "docs"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable summary. Kept as a legacy alias of message for existing clients.",
            "examples": [
              "Method POST Not Allowed"
            ]
          },
          "code": {
            "type": "string",
            "enum": [
              "method_not_allowed",
              "not_found",
              "not_configured",
              "invalid_request",
              "rate_limited",
              "upstream_error",
              "internal_error"
            ],
            "description": "Stable error identifier. Safe to match on; the wording of message may change."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of what failed."
          },
          "hint": {
            "type": "string",
            "description": "Concrete next step that resolves this error.",
            "examples": [
              "Retry with GET. This route is read-only."
            ]
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Documentation covering this route and its error codes."
          },
          "fields": {
            "type": "object",
            "description": "Per-field validation problems, present only when code is invalid_request.",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "GuestbookEntries": {
        "type": "object",
        "title": "GuestbookEntries",
        "description": "A page of approved guestbook entries.",
        "required": [
          "entries"
        ],
        "properties": {
          "entries": {
            "type": "array",
            "description": "Approved entries, newest first, at most 100.",
            "items": {
              "$ref": "#/components/schemas/GuestbookEntry"
            }
          }
        }
      },
      "GuestbookEntry": {
        "type": "object",
        "title": "GuestbookEntry",
        "description": "One approved public guestbook entry.",
        "required": [
          "id",
          "display_name",
          "message",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable entry identifier."
          },
          "display_name": {
            "type": "string",
            "description": "Name the author chose to display."
          },
          "message": {
            "type": "string",
            "description": "Moderated message text."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the entry was submitted."
          }
        }
      },
      "ViewCounts": {
        "type": "object",
        "title": "ViewCounts",
        "description": "View counters keyed by page identifier.",
        "required": [
          "views"
        ],
        "properties": {
          "views": {
            "type": "object",
            "description": "Counter per requested identifier. Unknown identifiers are 0.",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            }
          },
          "disabled": {
            "type": "boolean",
            "description": "Present and true when counting is switched off; views is then empty."
          }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "title": "JsonRpcRequest",
        "description": "A JSON-RPC 2.0 request or notification understood by the MCP endpoint.",
        "required": [
          "jsonrpc",
          "method"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0",
            "description": "Protocol marker."
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ],
            "description": "Request identifier. Omit for notifications."
          },
          "method": {
            "type": "string",
            "description": "MCP method name.",
            "examples": [
              "initialize",
              "tools/list",
              "tools/call",
              "ping"
            ]
          },
          "params": {
            "type": "object",
            "description": "Method parameters.",
            "additionalProperties": true
          }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "title": "JsonRpcResponse",
        "description": "A JSON-RPC 2.0 response from the MCP endpoint.",
        "required": [
          "jsonrpc"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0",
            "description": "Protocol marker."
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Identifier of the request being answered."
          },
          "result": {
            "type": "object",
            "description": "Method result on success.",
            "additionalProperties": true
          },
          "error": {
            "type": "object",
            "description": "JSON-RPC error on failure.",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "description": "JSON-RPC error code."
              },
              "message": {
                "type": "string",
                "description": "Error message."
              },
              "data": {
                "description": "Optional error detail."
              }
            }
          }
        }
      }
    }
  }
}
