{
  "openapi": "3.1.0",
  "info": {
    "title": "FMOps Public API",
    "summary": "The small read-only API behind fmops.ie, plus the free AI visibility check.",
    "description": "FMOps is a one-person growth consultancy in Waterford, Ireland, run by Fionn Murphy: AI search visibility, websites, business automation, and Claude for Small Business installs for Irish small businesses.\n\nThis document describes only the endpoints that are open to the public. Administrative endpoints exist but require an `X-Admin-Token` header and are not described here.\n\nEvery page on fmops.ie is also available as markdown from its own URL by sending `Accept: text/markdown`, per https://acceptmarkdown.com. A plain-language index of the whole site is at https://fmops.ie/llms.txt.\n\nNo authentication is needed for anything in this document. All endpoints send `Access-Control-Allow-Origin: *`. Write endpoints are rate limited per IP address.",
    "version": "1.0.0",
    "termsOfService": "https://fmops.ie/terms",
    "contact": {
      "name": "Fionn Murphy, FMOps",
      "email": "fionn@fmops.ie",
      "url": "https://fmops.ie/contact"
    },
    "license": {
      "name": "Free to use, no warranty",
      "url": "https://fmops.ie/terms"
    }
  },
  "servers": [
    { "url": "https://fmops.ie", "description": "Production" }
  ],
  "externalDocs": {
    "description": "FMOps developer resources",
    "url": "https://fmops.ie/developers"
  },
  "tags": [
    { "name": "Public data", "description": "Read-only JSON used by the site itself." },
    { "name": "AI visibility check", "description": "The free check at https://fmops.ie/ai-check." }
  ],
  "paths": {
    "/api/slots": {
      "get": {
        "tags": ["Public data"],
        "operationId": "getSlots",
        "summary": "Remaining founding pilot slots",
        "description": "How many founding AI-visibility pilot slots are left. Cached for 60 seconds at the edge.",
        "responses": {
          "200": {
            "description": "A map of slot key to availability.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": { "$ref": "#/components/schemas/Slot" }
                },
                "example": {
                  "ai_pilot": { "total": 3, "remaining": 2, "label": "Founding AI-visibility pilots" }
                }
              }
            }
          }
        }
      }
    },
    "/api/testimonials": {
      "get": {
        "tags": ["Public data"],
        "operationId": "getTestimonials",
        "summary": "Published client testimonials",
        "description": "Testimonials that the client has agreed to publish. Newest first, at most 50. Cached for 5 minutes at the edge. No star ratings are collected or returned.",
        "responses": {
          "200": {
            "description": "A list of published testimonials.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Testimonial" }
                }
              }
            }
          }
        }
      }
    },
    "/api/ai-check": {
      "get": {
        "tags": ["AI visibility check"],
        "operationId": "getAiCheckReport",
        "summary": "Fetch a finished AI visibility report",
        "description": "Returns a report by its id. The id is handed back by the POST below and emailed to the address that requested the check. Reports are not listable: you need the id.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The report id, a UUID.",
            "schema": { "type": "string", "format": "uuid" }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Set to `html` for the rendered report instead of JSON.",
            "schema": { "type": "string", "enum": ["html"] }
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/AiCheckReport" } },
              "text/html": { "schema": { "type": "string" } }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "503": { "$ref": "#/components/responses/Error" }
        }
      },
      "post": {
        "tags": ["AI visibility check"],
        "operationId": "createAiCheck",
        "summary": "Run a free AI visibility check",
        "description": "Asks several AI assistants a set of buying questions for a trade in a town, and reports whether the named business is mentioned. Rate limited to 5 checks per IP address per hour. The report is emailed to the address given, and the response carries the id you need for the GET above.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AiCheckRequest" },
              "example": {
                "email": "owner@example.ie",
                "business": "Riverside Physiotherapy",
                "trade": "physiotherapist",
                "town": "Waterford",
                "website": "https://example.ie"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted. `status` is `done` when the report finished inside the request, `queued` when it is still running.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "id": { "type": ["string", "null"], "format": "uuid" },
                    "url": { "type": ["string", "null"], "format": "uri" },
                    "status": { "type": "string", "enum": ["done", "queued"] },
                    "mention_rate": { "type": "number", "minimum": 0, "maximum": 1 }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "429": {
            "description": "Rate limit exceeded. Check the `Retry-After` header.",
            "headers": {
              "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait." }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "Error": {
        "description": "Something was wrong with the request.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string", "description": "A message written for a person to read." }
        },
        "required": ["ok", "error"]
      },
      "Slot": {
        "type": "object",
        "properties": {
          "total": { "type": "integer", "description": "How many slots there are in all." },
          "remaining": { "type": "integer", "description": "How many are still open." },
          "label": { "type": "string" },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "required": ["total", "remaining", "label"]
      },
      "Testimonial": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "business": { "type": "string" },
          "town": { "type": "string" },
          "service": { "type": "string", "description": "Which of the four services the work was." },
          "quote": { "type": "string" },
          "result": { "type": ["string", "null"], "description": "The outcome in the client's own words." },
          "date": { "type": "string", "format": "date-time" }
        }
      },
      "AiCheckRequest": {
        "type": "object",
        "properties": {
          "email": { "type": "string", "format": "email", "description": "Where the report is sent." },
          "business": { "type": "string", "maxLength": 160 },
          "trade": { "type": "string", "maxLength": 80, "description": "For example: physiotherapist, solicitor, garage." },
          "town": { "type": "string", "maxLength": 80, "description": "The town the business works in." },
          "website": { "type": "string", "format": "uri" },
          "name": { "type": "string", "maxLength": 120 },
          "phone": { "type": "string", "maxLength": 40 }
        },
        "required": ["email", "business", "trade", "town"]
      },
      "AiCheckReport": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "id": { "type": "string", "format": "uuid" },
          "status": { "type": "string" },
          "business": { "type": "string" },
          "trade": { "type": "string" },
          "town": { "type": "string" },
          "website": { "type": ["string", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "summary": { "type": "object", "description": "Per-assistant mention counts and the overall mention rate." },
          "report_html": { "type": "string" }
        }
      }
    }
  }
}
