{
  "openapi": "3.1.0",
  "info": {
    "title": "VisionXLab website API",
    "version": "1.0.0",
    "description": "Public endpoints of www.visionxlab.cz: the contact inquiry API and Markdown access to the pages.",
    "contact": {
      "email": "info@visionxlab.cz",
      "url": "https://www.visionxlab.cz"
    }
  },
  "servers": [
    {
      "url": "https://www.visionxlab.cz"
    }
  ],
  "security": [],
  "paths": {
    "/api/contact": {
      "post": {
        "operationId": "submitInquiry",
        "summary": "Send a contact inquiry to VisionXLab",
        "description": "Send an inquiry only with the consent of the person it is for. The agency answers to the given e-mail. Anti-spam: the inquiry is delivered only when started_at is present and at least 3000 ms in the past and the honeypot field website is empty. Otherwise the API answers 200 {\"ok\":true} but the inquiry is not delivered, so always send started_at. A JSON request gets JSON status codes; a browser form post gets 303 redirects. JSON mode (JSON error bodies, 415) is chosen when Content-Type is application/json or Accept contains application/json; otherwise the endpoint behaves like the browser form (303 redirects, unsupported bodies redirect to ?error=send). Other methods on this path (for example PUT) are answered 405 with Allow: POST.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          },
          "description": "JSON is the primary format. The form-encoded variant is what the website form sends and needs a same-site Origin header."
        },
        "responses": {
          "200": {
            "description": "Accepted (or silently discarded as spam, see the operation description).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "303": {
            "description": "Form mode: redirect to the thank-you page, or back to the form on a validation error.",
            "headers": {
              "Location": {
                "description": "Where to go next.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The body is not a JSON object of string values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Plain text \"Cross-site POST form submissions are forbidden\", sent by the framework origin check before this API runs. It applies to non-GET requests with no body or a form Content-Type (urlencoded, multipart, text/plain) and no same-site Origin header; this includes PUT and POST to unknown /api paths. Send application/json to avoid it.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. Only POST is supported here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Allow": {
                "description": "Always POST.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported Content-Type. Use application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A field is missing or invalid; error.field names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The inquiry could not be delivered. Try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/": {
      "get": {
        "operationId": "getHome",
        "summary": "Home page",
        "description": "Returns a page of the website. Send `Accept: text/markdown` to receive the page as Markdown instead of HTML; without it the response is HTML.",
        "responses": {
          "200": {
            "description": "The page.",
            "headers": {
              "Vary": {
                "description": "Always includes Accept.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Canonical URL of the page, rel=\"canonical\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such page.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/{path}": {
      "get": {
        "operationId": "getPage",
        "summary": "Any page of the website",
        "description": "Returns a page of the website. Send `Accept: text/markdown` to receive the page as Markdown instead of HTML; without it the response is HTML.",
        "responses": {
          "200": {
            "description": "The page.",
            "headers": {
              "Vary": {
                "description": "Always includes Accept.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Canonical URL of the page, rel=\"canonical\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such page.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Page path without the leading slash, for example marketing."
          }
        ]
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "summary": "Index of the site for AI assistants",
        "responses": {
          "200": {
            "description": "Index of the site for AI assistants",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "operationId": "getLlmsFullTxt",
        "summary": "Full text of the site for AI assistants",
        "responses": {
          "200": {
            "description": "Full text of the site for AI assistants",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ContactRequest": {
        "type": "object",
        "required": [
          "name",
          "email",
          "started_at"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 500,
            "description": "Full name of the person the inquiry is for. name needs at least 2 characters.",
            "minLength": 2
          },
          "email": {
            "type": "string",
            "maxLength": 500,
            "description": "E-mail address the agency replies to.",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional phone number. Digits, spaces, +, (, ) and - only."
          },
          "area": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional: which service the inquiry is about."
          },
          "budget": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional: monthly budget."
          },
          "deadline": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional: desired start or deadline."
          },
          "channel": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional: preferred contact channel."
          },
          "message": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional free-text brief."
          },
          "utm_source": {
            "type": "string",
            "maxLength": 500,
            "description": "Campaign attribution (UTM)."
          },
          "utm_medium": {
            "type": "string",
            "maxLength": 500,
            "description": "Campaign attribution (UTM)."
          },
          "utm_campaign": {
            "type": "string",
            "maxLength": 500,
            "description": "Campaign attribution (UTM)."
          },
          "utm_content": {
            "type": "string",
            "maxLength": 500,
            "description": "Campaign attribution (UTM)."
          },
          "utm_term": {
            "type": "string",
            "maxLength": 500,
            "description": "Campaign attribution (UTM)."
          },
          "gclid": {
            "type": "string",
            "maxLength": 500,
            "description": "Google Ads click id, when the visit came from an ad."
          },
          "fbclid": {
            "type": "string",
            "maxLength": 500,
            "description": "Meta ad click id, when the visit came from an ad."
          },
          "page": {
            "type": "string",
            "maxLength": 500,
            "description": "Same-site path the inquiry was sent from, for example /marketing."
          },
          "started_at": {
            "type": [
              "integer",
              "string"
            ],
            "description": "Unix time in milliseconds when the form was opened, at least 3000 ms before the submit. Required: a request whose started_at is missing, unparsable or less than 3000 ms old is treated as spam and answered with 200 {\"ok\":true} but is not delivered. Agents must send it, for example Date.now() minus 4000."
          },
          "website": {
            "type": "string",
            "maxLength": 0,
            "description": "Honeypot, leave empty. A non-empty value is treated as spam: the answer is 200 {\"ok\":true} and the inquiry is not delivered."
          },
          "locale": {
            "type": "string",
            "enum": [
              "cs",
              "sk"
            ],
            "default": "cs",
            "description": "Language of the inquiry."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "hint"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_body",
                  "unsupported_media_type",
                  "invalid_field",
                  "method_not_allowed",
                  "not_found",
                  "upstream_unavailable"
                ]
              },
              "message": {
                "type": "string"
              },
              "hint": {
                "type": "string"
              },
              "field": {
                "type": "string",
                "enum": [
                  "name",
                  "email",
                  "phone"
                ]
              }
            }
          }
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true
          }
        }
      }
    }
  }
}