{
  "openapi": "3.1.0",
  "info": {
    "title": "Haddon Institute HTTP API",
    "version": "1.1.0",
    "description": "Public HTTP APIs exposed by the Haddon Institute website (Next.js route handlers). Conference checkout, staff admin, and webhooks are omitted; see /developers and /docs/api. Rate limits: POST routes return X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix ms), and Retry-After (seconds) on HTTP 429."
  },
  "servers": [{ "url": "/", "description": "Current origin" }],
  "components": {
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string", "example": "NOT_FOUND" },
              "message": { "type": "string" },
              "resolution": {
                "type": "string",
                "description": "Hint for agents and integrators"
              }
            }
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": { "type": "string", "enum": ["ok"] }
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "schema": { "type": "integer" },
        "description": "Maximum requests in the current window"
      },
      "RateLimitRemaining": {
        "schema": { "type": "integer" },
        "description": "Requests remaining in the current window"
      },
      "RateLimitReset": {
        "schema": { "type": "integer" },
        "description": "Unix timestamp (ms) when the window resets"
      },
      "RetryAfter": {
        "schema": { "type": "integer" },
        "description": "Seconds to wait before retrying (HTTP 429)"
      }
    },
    "responses": {
      "NotFound": {
        "description": "Unknown route",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      }
    }
  },
  "paths": {
    "/.well-known/api-catalog": {
      "get": {
        "summary": "API catalog (RFC 9727)",
        "description": "Returns application/linkset+json listing discoverable API entry points, OpenAPI service-desc, and health status links.",
        "operationId": "getApiCatalog",
        "responses": {
          "200": {
            "description": "Linkset (application/linkset+json)",
            "content": {
              "application/linkset+json": {
                "schema": { "type": "object" }
              }
            }
          }
        }
      },
      "head": {
        "summary": "API catalog metadata",
        "description": "HEAD includes Link: rel=api-catalog per RFC 9727.",
        "operationId": "headApiCatalog",
        "responses": {
          "200": { "description": "Includes Link: rel=api-catalog" }
        }
      }
    },
    "/api/health": {
      "get": {
        "summary": "Service health",
        "description": "Lightweight JSON probe for uptime monitoring and RFC 9727 status links.",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthStatus" }
              }
            }
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "summary": "Site search",
        "description": "Search courses, programs, handbook pages, and public marketing content.",
        "operationId": "getSearch",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query string",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "JSON search results",
            "content": {
              "application/json": { "schema": { "type": "object" } }
            }
          },
          "400": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/chat": {
      "post": {
        "summary": "Workers AI streaming chat with RAG",
        "description": "Streaming UI message response (AI SDK). Rate limit: 20 requests per 10 minutes per client IP.",
        "operationId": "postChat",
        "responses": {
          "200": { "description": "UI message stream (AI SDK)" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": {
            "description": "Workers AI binding unavailable in this environment"
          }
        }
      }
    },
    "/api/email-marketing-signup": {
      "post": {
        "summary": "Email marketing signup",
        "description": "JSON body with email address. Requires Turnstile when configured. Rate limit: 3 per 10 minutes per IP.",
        "operationId": "postEmailMarketingSignup",
        "responses": {
          "200": { "description": "Accepted" },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/course-registration": {
      "post": {
        "summary": "Course registration for returning students",
        "description": "JSON registration payload with selected courses. Rate limit: 3 per 10 minutes per IP.",
        "operationId": "postCourseRegistration",
        "responses": {
          "200": { "description": "Accepted" },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/turnstile/site-key": {
      "get": {
        "summary": "Cloudflare Turnstile site key",
        "description": "Runtime publishable site key for client form widgets.",
        "operationId": "getTurnstileSiteKey",
        "responses": {
          "200": { "description": "JSON with site key" },
          "503": { "description": "Turnstile not configured" }
        }
      }
    },
    "/api/turnstile/required": {
      "get": {
        "summary": "Turnstile required flag",
        "description": "Whether donation and form APIs require a Turnstile token.",
        "operationId": "getTurnstileRequired",
        "responses": {
          "200": { "description": "JSON boolean flag" }
        }
      }
    },
    "/api/auth": {
      "get": {
        "summary": "Better Auth GET handler",
        "description": "Session and other GET handlers; additional path segments are supported by the deployment.",
        "operationId": "authGet",
        "responses": { "200": { "description": "Auth handler response" } }
      },
      "post": {
        "summary": "Better Auth POST handler",
        "description": "Sign-in and other POST handlers; additional path segments are supported by the deployment.",
        "operationId": "authPost",
        "responses": { "200": { "description": "Auth handler response" } }
      }
    },
    "/api/{unknown}": {
      "get": {
        "summary": "Unknown API route",
        "description": "Catch-all behaviour for unadvertised paths returns JSON errors.",
        "operationId": "getUnknownApiRoute",
        "parameters": [
          {
            "name": "unknown",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  }
}
