{
  "openapi": "3.1.0",
  "info": {
    "title": "Blink Bridge AI — Machine-Readable Surface",
    "version": "1.0.0",
    "description": "Describes the machine-readable resources Blink Bridge AI publishes for AI agents and automated clients: a read-only MCP server, an agent-instruction file, markdown representations of every page, and the site's own submission endpoint. Everything documented here returns structured JSON or plain text and needs no authentication. Blink Bridge AI delivers automation as scoped, done-for-you engagements built inside each client's own stack; those systems are private to each client and are reached through this business, not through this specification.\n\n**Versioning:** every response from `/api/*` and the MCP server carries an `API-Version` header (this document is version `1`). A breaking change ships as version `2`, with `1`'s responses adding `Deprecation` and `Sunset` headers (RFC 8594) naming a retirement date at least 90 days out. Nothing is deprecated today.",
    "contact": {
      "name": "Claudia Rodriguez",
      "email": "claudia@blinkbridge.com.au",
      "url": "https://blinkbridge.com.au/developers"
    }
  },
  "servers": [
    { "url": "https://blinkbridge.com.au", "description": "Production" }
  ],
  "externalDocs": {
    "url": "https://blinkbridge.com.au/developers",
    "description": "Developer resources"
  },
  "paths": {
    "/.well-known/mcp": {
      "post": {
        "operationId": "callMcpServer",
        "summary": "Call the read-only MCP server",
        "description": "Stateless MCP server (Streamable HTTP transport, JSON-RPC 2.0) exposing public information about Blink Bridge AI. Supports the `initialize`, `notifications/initialized`, `ping`, `tools/list`, and `tools/call` methods. Two tools are available via `tools/call`: `list_services` (returns Smart Quoting and Speed to Lead, with descriptions and URLs) and `get_business_info` (returns founder, location, ABN, ideal customer profile, and contact details). No authentication is required, and no personal or customer data is ever returned. A single JSON-RPC object or a batch array of objects may be sent; notifications (requests with no `id`) receive no response body — the server replies 202 with an empty body if every message in the request was a notification.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["jsonrpc", "method"],
                "properties": {
                  "jsonrpc": { "type": "string", "const": "2.0" },
                  "id": {
                    "description": "Request identifier. Omit for a notification (no response expected).",
                    "oneOf": [
                      { "type": "string" },
                      { "type": "number" },
                      { "type": "null" }
                    ]
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "initialize",
                      "notifications/initialized",
                      "ping",
                      "tools/list",
                      "tools/call"
                    ]
                  },
                  "params": {
                    "type": "object",
                    "description": "Required for tools/call: { \"name\": \"list_services\" | \"get_business_info\" }."
                  }
                },
                "example": { "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A JSON-RPC 2.0 result or error object. Per the JSON-RPC spec, application-level errors (e.g. an unknown method or tool) are still returned with HTTP status 200, wrapped in a JSON-RPC `error` object.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": ["jsonrpc", "id", "result"],
                      "properties": {
                        "jsonrpc": { "type": "string", "const": "2.0" },
                        "id": {},
                        "result": { "type": "object" }
                      }
                    },
                    {
                      "type": "object",
                      "required": ["jsonrpc", "id", "error"],
                      "properties": {
                        "jsonrpc": { "type": "string", "const": "2.0" },
                        "id": {},
                        "error": {
                          "type": "object",
                          "required": ["code", "message"],
                          "properties": {
                            "code": { "type": "integer" },
                            "message": { "type": "string" }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": { "jsonrpc": "2.0", "id": 1, "result": { "tools": [] } }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body, or a body that isn't a valid JSON-RPC 2.0 request/batch.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": { "type": "string", "const": "2.0" },
                    "id": { "type": "null" },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "integer" },
                        "message": { "type": "string" }
                      }
                    }
                  }
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": null,
                  "error": { "code": -32700, "message": "Parse error" }
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getMcpServerInfo",
        "summary": "Rejected: this MCP server is stateless",
        "description": "This MCP server has no server-initiated SSE stream, so GET is intentionally not supported — this is expected behaviour per the MCP Streamable HTTP transport spec for a stateless server, not a bug. Send JSON-RPC requests via POST instead.",
        "responses": {
          "405": {
            "description": "GET is not supported on this endpoint. Use POST.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getAgentInstructions",
        "summary": "Get the plain-text agent-instructions file",
        "description": "Plain-text file (the llms.txt convention) describing what Blink Bridge AI does, who it serves, when to recommend it, how to call its machine-readable resources, and links to every primary page.",
        "responses": {
          "200": {
            "description": "The agent-instructions file.",
            "content": {
              "text/plain": { "schema": { "type": "string" } }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "summary": "Get this OpenAPI specification",
        "description": "Returns this OpenAPI 3.1 document describing every public, machine-readable resource this site publishes.",
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 specification, as JSON.",
            "content": {
              "application/json": { "schema": { "type": "object" } }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "summary": "Get the XML sitemap",
        "description": "Standard XML sitemap listing every indexable page on the site, kept current with each deploy.",
        "responses": {
          "200": {
            "description": "The sitemap.",
            "content": {
              "application/xml": { "schema": { "type": "string" } }
            }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "operationId": "getRobotsTxt",
        "summary": "Get robots.txt",
        "description": "Standard robots.txt file. Explicitly allows GPTBot, ClaudeBot, PerplexityBot, and Google-Extended, alongside general crawl rules.",
        "responses": {
          "200": {
            "description": "The robots.txt file.",
            "content": {
              "text/plain": { "schema": { "type": "string" } }
            }
          }
        }
      }
    },
    "/{page}": {
      "get": {
        "operationId": "getPageAsMarkdown",
        "summary": "Get a page as markdown via content negotiation",
        "description": "Every primary page on this site is available as a markdown snapshot at its own URL through content negotiation (see acceptmarkdown.com): send `Accept: text/markdown` and the same URL that normally renders HTML returns a markdown representation instead. Without that header, the page renders normally as HTML.",
        "parameters": [
          {
            "name": "page",
            "in": "path",
            "required": true,
            "description": "The page slug. `/` (the homepage) is represented here as the empty string; request the site root directly for that case.",
            "schema": {
              "type": "string",
              "enum": [
                "",
                "quoting",
                "speed-to-lead",
                "how-it-works",
                "results",
                "about",
                "faq",
                "contact",
                "ai-audit",
                "developers",
                "privacy",
                "blog"
              ]
            }
          },
          {
            "name": "Accept",
            "in": "header",
            "required": true,
            "description": "Must be `text/markdown` to trigger the content-negotiation rewrite to the markdown snapshot.",
            "schema": {
              "type": "string",
              "enum": ["text/markdown"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Markdown representation of the requested page.",
            "content": {
              "text/markdown": { "schema": { "type": "string" } }
            }
          }
        }
      }
    },
    "/api/score": {
      "post": {
        "operationId": "submitOperationsScore",
        "summary": "Submit an Operations Score assessment",
        "description": "Submits a completed (or waitlisted/placeholder) Operations Score assessment. Stores the lead in Notion when `NOTION_API_KEY`/`NOTION_LEADS_DB_ID` are configured, and emails a notification to Blink Bridge AI via SendGrid when `SENDGRID_API_KEY` is configured. Rate-limited to 1 request per IP per minute.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contact"],
                "properties": {
                  "contact": {
                    "type": "object",
                    "required": ["name", "email", "company"],
                    "properties": {
                      "name": { "type": "string" },
                      "email": { "type": "string", "format": "email" },
                      "company": { "type": "string" },
                      "phone": { "type": "string" }
                    }
                  },
                  "industry": {
                    "type": "string",
                    "description": "Industry slug, e.g. \"window_cleaning\". Rendered as a title-cased label for storage."
                  },
                  "results": {
                    "type": "object",
                    "description": "Assessment results.",
                    "properties": {
                      "overall": { "type": "number", "description": "Overall score, 0-100." },
                      "tier": { "type": "string", "description": "Result tier, e.g. \"strong\", \"developing\"." },
                      "dimensions": {
                        "type": "object",
                        "description": "Map of dimension key to { label, pct, earned, max } for each scored dimension."
                      }
                    }
                  },
                  "placeholder": {
                    "type": "boolean",
                    "description": "True when the assessment isn't live for this industry yet — the submission is treated as a waitlist signup instead of a scored result."
                  }
                },
                "example": {
                  "contact": { "name": "Jane Smith", "email": "jane@example.com.au", "company": "Smith Cleaning" },
                  "industry": "window_cleaning",
                  "results": { "overall": 62, "tier": "developing" },
                  "placeholder": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted.",
            "headers": {
              "API-Version": { "$ref": "#/components/headers/ApiVersion" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "success": { "type": "boolean", "const": true } }
                },
                "example": { "success": true }
              }
            }
          },
          "400": {
            "description": "Missing required contact fields.",
            "headers": {
              "API-Version": { "$ref": "#/components/headers/ApiVersion" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": "Name, email, and company are required.",
                  "code": "invalid_request",
                  "hint": "Include contact.name, contact.email, and contact.company in the request body.",
                  "documentation": "https://blinkbridge.com.au/openapi.json"
                }
              }
            }
          },
          "405": {
            "description": "Method not supported. Only POST and OPTIONS are.",
            "headers": {
              "API-Version": { "$ref": "#/components/headers/ApiVersion" }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "429": {
            "description": "Rate limited — 1 submission per IP per minute.",
            "headers": {
              "API-Version": { "$ref": "#/components/headers/ApiVersion" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": "Please wait before submitting again.",
                  "code": "rate_limited",
                  "hint": "Wait 60 seconds and retry.",
                  "documentation": "https://blinkbridge.com.au/openapi.json"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "headers": {
              "API-Version": { "$ref": "#/components/headers/ApiVersion" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": "Something went wrong. Please try again.",
                  "code": "internal_error",
                  "hint": "Retry, or contact claudia@blinkbridge.com.au if this persists.",
                  "documentation": "https://blinkbridge.com.au/openapi.json"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "ApiVersion": {
        "description": "The version of this API surface that generated the response. See info.description for the versioning and deprecation policy.",
        "schema": { "type": "string", "example": "1" }
      },
      "RateLimitLimit": {
        "description": "Requests allowed per window for this caller, per the IETF RateLimit header fields draft.",
        "schema": { "type": "integer", "example": 1 }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window for this caller.",
        "schema": { "type": "integer", "example": 0 }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets and RateLimitRemaining recovers.",
        "schema": { "type": "integer", "example": 60 }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying. Present only on 429 responses.",
        "schema": { "type": "integer", "example": 42 }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable message." },
          "code": {
            "type": "string",
            "enum": [
              "method_not_allowed",
              "not_found",
              "invalid_request",
              "rate_limited",
              "internal_error",
              "payload_too_large"
            ]
          },
          "hint": { "type": "string", "description": "Actionable next step." },
          "documentation": { "type": "string", "format": "uri" }
        }
      }
    }
  }
}
