{
  "openapi": "3.1.0",
  "info": {
    "title": "CalcWidgets API",
    "version": "1.0.0",
    "summary": "Public endpoints for the CalcWidgets embeddable calculators.",
    "description": "CalcWidgets provides embeddable, white-label mortgage and property calculators for mortgage brokers and real estate professionals in Australia, New Zealand and the United States.\n\nThis specification covers the public, unauthenticated surface only. Calculator results are also available to AI agents through the Model Context Protocol server at `/api/mcp/`, which exposes the calculators as read-only tools plus resources describing the catalogue and each region's lending rules.\n\nAll calculator output is an indicative estimate — general information, not financial or credit advice.",
    "contact": {
      "name": "CalcWidgets",
      "url": "https://calcwidgets.com/contact/"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://calcwidgets.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Site guide for LLMs",
    "url": "https://calcwidgets.com/llms.txt"
  },
  "tags": [
    {
      "name": "Discovery",
      "description": "Machine-readable descriptions of the site."
    },
    {
      "name": "Agents",
      "description": "Model Context Protocol server."
    },
    {
      "name": "Clients",
      "description": "Broker account helpers."
    },
    {
      "name": "Contact",
      "description": "Contact form."
    }
  ],
  "paths": {
    "/api/": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "Index of the public API",
        "description": "Lists the public endpoints and points at this specification, llms.txt and the sitemap.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "API index.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "documentation_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "path",
                          "method",
                          "description"
                        ]
                      }
                    }
                  },
                  "required": [
                    "name",
                    "documentation_url",
                    "endpoints"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp/": {
      "post": {
        "operationId": "postMcpMessage",
        "summary": "Model Context Protocol endpoint",
        "description": "MCP server over the Streamable HTTP transport. Send JSON-RPC 2.0 messages (`initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`).\n\nTools: `calculate_loan_repayment`, `calculate_borrowing_capacity`, `calculate_stamp_duty`, `calculate_purchasing_power` — all read-only.\n\nResources: `calcwidgets://calculators`, `calcwidgets://regions`, `calcwidgets://guide`, `calcwidgets://calculator/{slug}`.\n\nThe bare path `/api/mcp` is served directly (no redirect). Responses are `text/event-stream`; clients should accept both `application/json` and `text/event-stream`, though a JSON-only `Accept` header is tolerated.",
        "tags": [
          "Agents"
        ],
        "externalDocs": {
          "description": "MCP specification",
          "url": "https://modelcontextprotocol.io/specification"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A JSON-RPC 2.0 request object.",
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "const": "2.0"
                  },
                  "id": {
                    "type": [
                      "string",
                      "number"
                    ]
                  },
                  "method": {
                    "type": "string",
                    "examples": [
                      "initialize",
                      "resources/list"
                    ]
                  },
                  "params": {
                    "type": "object"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response, streamed as Server-Sent Events.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed — SSE (GET) is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "406": {
            "description": "Client did not accept a supported media type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/clients/check-subdomain": {
      "get": {
        "operationId": "checkSubdomainAvailability",
        "summary": "Check whether a client subdomain is available",
        "description": "Each broker gets a subdomain such as `yourbrand.calcwidgets.com`. Subdomains are 3–30 characters, lowercase letters and digits only.",
        "tags": [
          "Clients"
        ],
        "parameters": [
          {
            "name": "subdomain",
            "in": "query",
            "required": true,
            "description": "The subdomain to check, without the domain suffix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{3,30}$",
              "examples": [
                "ratereset"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability result. `available` is false with an `error` string when the subdomain is taken or malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "available"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The `subdomain` parameter is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Availability could not be determined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "operationId": "submitContactForm",
        "summary": "Submit the contact form",
        "tags": [
          "Contact"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "phone": {
                    "type": "string"
                  },
                  "businessName": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string",
                    "description": "Optional free-text message."
                  }
                },
                "required": [
                  "name",
                  "email",
                  "phone",
                  "businessName"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Required fields are missing or invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The message could not be delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "summary": "Site guide for large language models",
        "description": "Plain-text overview of the calculators, regions and embedding docs.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "The site guide.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "summary": "XML sitemap",
        "description": "Every indexable URL on the site.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "The sitemap.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "operationId": "getRobotsTxt",
        "summary": "Crawler directives",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "The robots.txt file.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "summary": "This specification",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "title": "Error",
        "description": "Structured error. `error` and `message` carry the same text; `error` is retained as a string for backward compatibility with existing clients.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "message": {
            "type": "string",
            "description": "Same text as `error`."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code.",
            "enum": [
              "bad_request",
              "not_found",
              "unauthorized",
              "method_not_allowed",
              "internal_error"
            ]
          },
          "resolution": {
            "type": "string",
            "description": "What to do next."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code."
          }
        },
        "required": [
          "error",
          "message",
          "code",
          "resolution",
          "documentation_url",
          "status"
        ],
        "examples": [
          {
            "error": "No API endpoint exists at /api/does-not-exist/.",
            "message": "No API endpoint exists at /api/does-not-exist/.",
            "code": "not_found",
            "resolution": "Check the path against the OpenAPI specification at /openapi.json, or GET /api/ for an index of the public endpoints.",
            "documentation_url": "https://calcwidgets.com/openapi.json",
            "status": 404
          }
        ]
      }
    }
  }
}