{
  "openapi": "3.1.0",
  "info": {
    "title": "QuolyTech API",
    "version": "1.0.0",
    "summary": "Public content and chat API of quolytech.com.",
    "description": "Read-only access to the published content of quolytech.com (pages, services, industries, projects, blog posts, team, FAQs, contact details) and a chat endpoint that answers questions about QuolyTech's services, pricing and process.\n\nNo authentication is needed for the endpoints listed here. Responses are JSON. Errors are JSON too: `{ \"error\": \"…\", \"code\": \"not_found\", \"message\": \"…\", \"hint\": \"…\", \"status\": 404, \"docs\": \"/openapi.json\" }`.\n\nTo start a project with QuolyTech, send an enquiry with `sendContactMessage` (it lands in the team's inbox), or use the contact details from `getSiteSettings` (email quolytech@gmail.com).",
    "contact": {
      "name": "QuolyTech",
      "url": "https://quolytech.com/developers/",
      "email": "quolytech@gmail.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://quolytech.com/terms/"
    }
  },
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://quolytech.com/developers/"
  },
  "servers": [
    {
      "url": "https://api.quolytech.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Content",
      "description": "Published website content."
    },
    {
      "name": "Contact",
      "description": "Enquiries and newsletter sign-ups."
    },
    {
      "name": "Chat",
      "description": "QuolyBot, the website's AI assistant."
    },
    {
      "name": "Meta",
      "description": "Information about the API itself."
    }
  ],
  "paths": {
    "/api/public/pages": {
      "get": {
        "operationId": "listPages",
        "tags": [
          "Content"
        ],
        "summary": "List published pages",
        "description": "Returns every published page of quolytech.com with its slug, title and description. Use a slug with getPage to read a page's sections.",
        "responses": {
          "200": {
            "description": "Published pages, in site order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pages"
                  ],
                  "properties": {
                    "pages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PageSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/pages/{slug}": {
      "get": {
        "operationId": "getPage",
        "tags": [
          "Content"
        ],
        "summary": "Get a published page",
        "description": "Returns the published version of one page: its SEO metadata and its sections (each section has a type such as `site.hero` and free-form props with the section's text and images). Example slugs: home, about, services, industries, projects, blog, contact, terms, privacy.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Page slug from listPages.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": [
                "home"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/collections/{name}": {
      "get": {
        "operationId": "listCollectionItems",
        "tags": [
          "Content"
        ],
        "summary": "List items of a content collection",
        "description": "Returns the published items of one collection in display order: services QuolyTech offers, industries it serves, portfolio projects, blog posts, team members, client testimonials or FAQs. Every item has a `slug`; other fields depend on the collection (for example services have title, description, faqs and pricing details). An unknown collection returns an empty list.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "Collection name.",
            "schema": {
              "$ref": "#/components/schemas/CollectionName"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Items of the collection.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "collection",
                    "items"
                  ],
                  "properties": {
                    "collection": {
                      "type": "string",
                      "description": "The requested collection name."
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ContentItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/settings": {
      "get": {
        "operationId": "getSiteSettings",
        "tags": [
          "Content"
        ],
        "summary": "Get site settings and contact details",
        "description": "Returns site-wide settings: the brand (name, tagline, URL), contact details (email, phone, address) and social accounts. Use this to find out how to contact QuolyTech.",
        "responses": {
          "200": {
            "description": "Site settings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "settings"
                  ],
                  "properties": {
                    "settings": {
                      "$ref": "#/components/schemas/Settings"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/export": {
      "get": {
        "operationId": "exportContent",
        "tags": [
          "Content"
        ],
        "summary": "Export all published content",
        "description": "Returns all published website pages, every collection and the site settings in one response. Useful to index the whole site at once instead of calling listPages, getPage and listCollectionItems repeatedly.",
        "responses": {
          "200": {
            "description": "All published content.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pages",
                    "collections",
                    "settings"
                  ],
                  "properties": {
                    "pages": {
                      "type": "object",
                      "description": "Published pages keyed by slug.",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/PageContent"
                      }
                    },
                    "collections": {
                      "type": "object",
                      "description": "Collection items keyed by collection name.",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/ContentItem"
                        }
                      }
                    },
                    "settings": {
                      "$ref": "#/components/schemas/Settings"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/chatbot": {
      "get": {
        "operationId": "getChatbotInfo",
        "tags": [
          "Chat"
        ],
        "summary": "Get the chat assistant's details",
        "description": "Returns whether QuolyBot (the website's AI assistant) is enabled, its name, greeting and suggested questions.",
        "responses": {
          "200": {
            "description": "Chat assistant details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatbotInfo"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/chatbot/ask": {
      "post": {
        "operationId": "askChatbot",
        "tags": [
          "Chat"
        ],
        "summary": "Ask QuolyBot a question",
        "description": "Sends a message to QuolyBot, which answers questions about QuolyTech's services, pricing, timelines and process. Pass earlier turns in `history` to continue a conversation. Rate limited per IP address.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              },
              "examples": {
                "pricing": {
                  "summary": "Ask about pricing",
                  "value": {
                    "message": "How much does a website cost?"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "QuolyBot's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatReply"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/contact": {
      "post": {
        "operationId": "sendContactMessage",
        "tags": [
          "Contact"
        ],
        "summary": "Send an enquiry to QuolyTech",
        "description": "Sends a message to the QuolyTech team: it lands in their inbox and is emailed to them, and the sender may receive a confirmation email; the team replies by email. Use this when a user wants QuolyTech to build or grow a product, asks for a quote, or wants a call. Include what the user needs, budget and timeline if known. Limited to 5 messages per hour per IP address. Leave `website` empty: it is a spam trap, and messages that fill it are discarded.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              },
              "examples": {
                "enquiry": {
                  "summary": "Project enquiry",
                  "value": {
                    "name": "Jane Doe",
                    "email": "jane@example.com",
                    "message": "We need an AI support agent for our online store. Budget around €3,000, launch in 6 weeks.",
                    "page": "ai-agent"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Accepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/public/newsletter": {
      "post": {
        "operationId": "subscribeNewsletter",
        "tags": [
          "Contact"
        ],
        "summary": "Subscribe to the newsletter",
        "description": "Adds an email address to QuolyTech's newsletter. Subscribing an address that is already on the list succeeds without creating a duplicate. Limited to 5 requests per 10 minutes per IP address.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewsletterRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Accepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/auth/providers": {
      "get": {
        "operationId": "listSignInProviders",
        "tags": [
          "Meta"
        ],
        "summary": "List enabled sign-in providers",
        "description": "Returns which third-party sign-in providers are enabled for the website's client login.",
        "responses": {
          "200": {
            "description": "Enabled providers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "google"
                  ],
                  "properties": {
                    "google": {
                      "type": "boolean",
                      "description": "Sign in with Google is available."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": [
          "Meta"
        ],
        "summary": "Get this OpenAPI document",
        "description": "Returns this OpenAPI 3.1 description of the public API.",
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "OpenAPI 3.1 document."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CollectionName": {
        "type": "string",
        "description": "projects: portfolio case studies; blog: articles; team: team members; services: services offered; industries: industries served; testimonials: client reviews; faqs: frequently asked questions.",
        "enum": [
          "projects",
          "blog",
          "team",
          "services",
          "industries",
          "testimonials",
          "faqs"
        ]
      },
      "PageSummary": {
        "type": "object",
        "required": [
          "slug",
          "title"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Page identifier, used with getPage."
          },
          "title": {
            "type": "string",
            "description": "Page title (SEO title)."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta description."
          },
          "publishedAt": {
            "type": [
              "string",
              "integer",
              "null"
            ],
            "description": "When the page was last published."
          }
        }
      },
      "Section": {
        "type": "object",
        "required": [
          "type",
          "props"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Section identifier."
          },
          "type": {
            "type": "string",
            "description": "Section type, e.g. site.hero or site.faq.",
            "examples": [
              "site.hero"
            ]
          },
          "props": {
            "type": "object",
            "description": "The section's content (texts, images, lists). Text may contain **bold** markers and \\n line breaks.",
            "additionalProperties": true
          }
        }
      },
      "PageContent": {
        "type": "object",
        "properties": {
          "meta": {
            "type": "object",
            "description": "SEO metadata.",
            "properties": {
              "title": {
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ogTitle": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ogDescription": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ogImage": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "additionalProperties": true
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Section"
            }
          }
        }
      },
      "Page": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageContent"
          },
          {
            "type": "object",
            "required": [
              "slug"
            ],
            "properties": {
              "slug": {
                "type": "string"
              },
              "publishedAt": {
                "type": [
                  "string",
                  "integer",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "ContentItem": {
        "type": "object",
        "required": [
          "slug"
        ],
        "description": "A collection item. Fields depend on the collection; common ones are title or name, description and image paths (relative to https://quolytech.com).",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Item identifier; its page is https://quolytech.com/<collection>/<slug>/ for projects, blog, team, services and industries."
          },
          "title": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "Settings": {
        "type": "object",
        "properties": {
          "site": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "tagline": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "metaDescription": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "contact": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "phone": {
                "type": "string"
              },
              "address": {
                "type": "string"
              }
            },
            "additionalProperties": true
          },
          "social": {
            "type": "object",
            "properties": {
              "accounts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "platform",
                    "url"
                  ],
                  "properties": {
                    "platform": {
                      "type": "string",
                      "enum": [
                        "twitter",
                        "instagram",
                        "linkedin",
                        "youtube",
                        "tiktok",
                        "github"
                      ]
                    },
                    "url": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string",
                      "description": "What the account is for, e.g. News or Memes."
                    }
                  }
                }
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "ChatbotInfo": {
        "type": "object",
        "required": [
          "enabled",
          "name"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the assistant answers messages."
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "greeting": {
            "type": "string"
          },
          "quickPrompts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Suggested questions."
          },
          "placeholder": {
            "type": "string"
          },
          "ctaText": {
            "type": "string"
          },
          "ctaLabel": {
            "type": "string"
          },
          "ctaLink": {
            "type": "string"
          },
          "offlineMessage": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "ChatTurn": {
        "type": "object",
        "required": [
          "role",
          "text"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "user",
              "model"
            ],
            "description": "user: the visitor; model: QuolyBot."
          },
          "text": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "ChatRequest": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "The question to ask."
          },
          "history": {
            "type": "array",
            "maxItems": 12,
            "description": "Earlier turns of the conversation, oldest first (optional).",
            "items": {
              "$ref": "#/components/schemas/ChatTurn"
            }
          }
        }
      },
      "ChatReply": {
        "type": "object",
        "required": [
          "reply",
          "source"
        ],
        "properties": {
          "reply": {
            "type": "string",
            "description": "QuolyBot's answer (may contain Markdown-style **bold**)."
          },
          "source": {
            "type": "string",
            "enum": [
              "gemini",
              "knowledge",
              "disabled"
            ],
            "description": "gemini: AI answer; knowledge: answered from offline rules; disabled: the assistant is turned off."
          },
          "model": {
            "type": "string",
            "description": "AI model used, when source is gemini."
          },
          "fallbackReason": {
            "type": "string",
            "description": "Why the offline rules answered, when source is knowledge."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "message",
          "status"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message (same as message)."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code.",
            "enum": [
              "bad_request",
              "unauthorized",
              "forbidden",
              "not_found",
              "method_not_allowed",
              "conflict",
              "payload_too_large",
              "rate_limited",
              "internal_error",
              "upstream_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable message."
          },
          "hint": {
            "type": "string",
            "description": "How to resolve the error."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code."
          },
          "docs": {
            "type": "string",
            "description": "Where the API is documented."
          }
        }
      },
      "ContactRequest": {
        "type": "object",
        "required": [
          "name",
          "email",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Who is writing."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Where the team should reply."
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 5000,
            "description": "What the user needs."
          },
          "page": {
            "type": "string",
            "maxLength": 500,
            "description": "Where the message comes from: the page URL or path, or e.g. \"ai-agent\"."
          },
          "website": {
            "type": "string",
            "description": "Spam trap: must be empty or omitted."
          }
        }
      },
      "NewsletterRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "website": {
            "type": "string",
            "description": "Spam trap: must be empty or omitted."
          },
          "page": {
            "type": "string",
            "maxLength": 500,
            "description": "Page the sign-up came from."
          }
        }
      },
      "Accepted": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "id": {
            "type": "string",
            "description": "Message id (contact messages only)."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is invalid (e.g. missing message or invalid JSON).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Nothing exists at this URL.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body is too large.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests; wait before retrying.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
