{
  "openapi": "3.1.0",
  "info": {
    "title": "Klepha API",
    "version": "1.0.0",
    "summary": "AI search visibility: crawl a site, ask ChatGPT/Gemini/Perplexity about it, and measure who gets named and cited.",
    "description": "The Klepha API powers the Klepha web app. Requests are authenticated with a session cookie (`ksess`) obtained through passwordless email OTP or Google sign-in. Metered operations (engine scans, competitor analysis, Reddit analysis, content drafting) consume account credits and return HTTP 402 when the balance is exhausted.",
    "contact": { "name": "Klepha support", "email": "support@klepha.com", "url": "https://klepha.com" },
    "termsOfService": "https://klepha.com/terms",
    "license": { "name": "Proprietary", "url": "https://klepha.com/terms" }
  },
  "servers": [{ "url": "https://klepha.com", "description": "Production" }],
  "tags": [
    { "name": "Session", "description": "Passwordless sign-in and account state" },
    { "name": "Sites", "description": "Sites you track, their crawl and their prompt set" },
    { "name": "Visibility", "description": "Engine scans, answers and scores" },
    { "name": "Off-site", "description": "Search Console and Reddit integrations" },
    { "name": "Billing", "description": "Plans, credits and checkout" }
  ],
  "security": [{ "sessionCookie": [] }],
  "paths": {
    "/api/me": {
      "get": {
        "tags": ["Session"],
        "summary": "Current account state",
        "description": "Returns the signed-in user, their active site, credit balance, plan and which engines are configured. Returns `user: null` when signed out, so it doubles as a capability probe.",
        "security": [],
        "responses": {
          "200": {
            "description": "Account and capability snapshot",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
          }
        }
      }
    },
    "/api/auth/email": {
      "post": {
        "tags": ["Session"],
        "summary": "Request a one-time sign-in code",
        "description": "Emails a six-digit code valid for 10 minutes. Rate limited to one send per 60 seconds per address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "required": ["email"],
            "properties": { "email": { "type": "string", "format": "email" } }
          } } }
        },
        "responses": {
          "200": { "description": "Code sent" },
          "429": { "description": "Asked again too soon" }
        }
      }
    },
    "/api/auth/verify": {
      "post": {
        "tags": ["Session"],
        "summary": "Exchange the code for a session",
        "description": "Verifies the emailed code and sets the `ksess` session cookie. Five attempts are allowed per code.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "required": ["email", "code"],
            "properties": { "email": { "type": "string", "format": "email" }, "code": { "type": "string", "pattern": "^[0-9]{6}$" } }
          } } }
        },
        "responses": {
          "200": { "description": "Signed in; session cookie set" },
          "400": { "description": "Wrong or expired code" }
        }
      }
    },
    "/api/auth/google": {
      "get": {
        "tags": ["Session"],
        "summary": "Begin Google sign-in",
        "description": "Redirects to Google's consent screen. On return, a session is created and the browser lands back in the app.",
        "security": [],
        "responses": { "302": { "description": "Redirect to Google" } }
      }
    },
    "/api/logout": {
      "post": {
        "tags": ["Session"],
        "summary": "End the session",
        "responses": { "200": { "description": "Signed out" } }
      }
    },
    "/api/sites": {
      "get": {
        "tags": ["Sites"],
        "summary": "List your sites",
        "responses": { "200": { "description": "Sites owned by the session user",
          "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Site" } } } } } }
      },
      "post": {
        "tags": ["Sites"],
        "summary": "Add a site and start its crawl",
        "description": "Registers a domain and immediately begins a crawl. Only public pages are read, and private or internal addresses are refused.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "required": ["domain"],
            "properties": { "domain": { "type": "string", "examples": ["example.com"] } }
          } } }
        },
        "responses": {
          "200": { "description": "Site created; crawl started",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } } },
          "402": { "$ref": "#/components/responses/OutOfCredits" }
        }
      }
    },
    "/api/sites/{siteId}": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "put": {
        "tags": ["Sites"],
        "summary": "Update competitors or keywords",
        "requestBody": {
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "competitors": { "type": "array", "maxItems": 12, "items": { "type": "string" } },
              "keywords": { "type": "array", "maxItems": 10, "items": { "type": "string" } }
            }
          } } }
        },
        "responses": { "200": { "description": "Updated" } }
      }
    },
    "/api/sites/{siteId}/summary": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "get": {
        "tags": ["Visibility"],
        "summary": "Dashboard summary",
        "description": "Everything the overview screen shows: visibility score, mention rate, citations, share of voice and per-engine scores.",
        "responses": { "200": { "description": "Summary",
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Summary" } } } } }
      }
    },
    "/api/sites/{siteId}/prompts": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "get": {
        "tags": ["Visibility"],
        "summary": "List tracked prompts",
        "responses": { "200": { "description": "The exact question set scans will run" } }
      },
      "put": {
        "tags": ["Visibility"],
        "summary": "Replace the tracked prompt set",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "required": ["prompts"],
            "properties": { "prompts": { "type": "array", "items": { "type": "string" } } }
          } } }
        },
        "responses": { "200": { "description": "Prompt set saved" } }
      }
    },
    "/api/sites/{siteId}/scans": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "get": {
        "tags": ["Visibility"],
        "summary": "List scans",
        "responses": { "200": { "description": "Scan history, newest first" } }
      },
      "post": {
        "tags": ["Visibility"],
        "summary": "Run a visibility scan",
        "description": "Sends every tracked prompt to ChatGPT, Gemini and Perplexity, then stores each answer with its citations, brand mentions and competitor mentions. Metered: consumes credits per engine call.",
        "responses": {
          "200": { "description": "Scan queued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Scan" } } } },
          "402": { "$ref": "#/components/responses/OutOfCredits" }
        }
      }
    },
    "/api/scans/{scanId}": {
      "parameters": [{ "name": "scanId", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "get": {
        "tags": ["Visibility"],
        "summary": "Scan status and results",
        "description": "Poll while `status` is `queued` or `running`. When `done`, includes every prompt with the full answer text per engine, the sources each engine cited, and which competitors were named.",
        "responses": { "200": { "description": "Scan with results",
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Scan" } } } } }
      }
    },
    "/api/sites/{siteId}/competitors": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "post": {
        "tags": ["Visibility"],
        "summary": "Analyze a competitor",
        "description": "Crawls a competitor and explains why engines cite them. Metered.",
        "responses": { "200": { "description": "Analysis started" }, "402": { "$ref": "#/components/responses/OutOfCredits" } }
      }
    },
    "/api/sites/{siteId}/gsc": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "get": {
        "tags": ["Off-site"],
        "summary": "Search Console performance",
        "description": "28 days of clicks, impressions, top queries, top pages and sitemap state for the matched property.",
        "responses": { "200": { "description": "Search Console data" } }
      }
    },
    "/api/sites/{siteId}/reddit/feed": {
      "parameters": [{ "$ref": "#/components/parameters/siteId" }],
      "get": {
        "tags": ["Off-site"],
        "summary": "Recent Reddit posts about your keywords",
        "description": "The ten most recent relevant threads, each scored for relevance. Metered.",
        "responses": { "200": { "description": "Scored posts" }, "402": { "$ref": "#/components/responses/OutOfCredits" } }
      }
    },
    "/api/usage": {
      "get": {
        "tags": ["Billing"],
        "summary": "Credit usage history",
        "description": "Every metered operation with its kind, token counts and credit cost.",
        "responses": { "200": { "description": "Usage rows and running balance" } }
      }
    },
    "/api/billing/checkout": {
      "post": {
        "tags": ["Billing"],
        "summary": "Start a subscription checkout",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "required": ["plan"],
            "properties": { "plan": { "type": "string", "enum": ["starter", "growth"] } }
          } } }
        },
        "responses": { "200": { "description": "Hosted checkout URL",
          "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" } } } } } } }
      }
    },
    "/api/billing/portal": {
      "post": {
        "tags": ["Billing"],
        "summary": "Open the billing portal",
        "responses": { "200": { "description": "Portal URL" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey", "in": "cookie", "name": "ksess",
        "description": "Session cookie set by `/api/auth/verify` or Google sign-in. HttpOnly, SameSite=Lax."
      }
    },
    "parameters": {
      "siteId": { "name": "siteId", "in": "path", "required": true, "schema": { "type": "integer" } }
    },
    "responses": {
      "OutOfCredits": {
        "description": "Not enough credits for this metered operation",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Me": {
        "type": "object",
        "properties": {
          "user": { "type": ["object", "null"], "properties": { "email": { "type": "string" } } },
          "site": { "type": ["object", "null"] },
          "credits": { "type": ["integer", "null"], "description": "1 credit = $0.01 of engine spend" },
          "plan": { "type": ["string", "null"], "enum": ["starter", "growth", null] },
          "billing": { "type": "boolean" },
          "googleAuth": { "type": "boolean" },
          "emailOtp": { "type": "boolean" },
          "engines": {
            "type": "object",
            "properties": {
              "chatgpt": { "type": "boolean" }, "gemini": { "type": "boolean" },
              "perplexity": { "type": "boolean" }, "fake": { "type": "boolean" }
            }
          }
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "domain": { "type": "string" },
          "onboarded": { "type": "boolean" },
          "competitors": { "type": "array", "items": { "type": "string" } },
          "keywords": { "type": "array", "items": { "type": "string" } }
        }
      },
      "Scan": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "status": { "type": "string", "enum": ["queued", "running", "done", "failed"] },
          "created": { "type": "integer", "description": "Unix epoch milliseconds" },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "prompt": { "type": "string" },
                "engine": { "type": "string", "enum": ["chatgpt", "gemini", "perplexity"] },
                "answer": { "type": "string", "description": "Full answer text as returned by the engine" },
                "mentioned": { "type": "boolean", "description": "Whether your brand was named" },
                "cited": { "type": "boolean", "description": "Whether your domain was cited as a source" },
                "competitors": { "type": "array", "items": { "type": "string" } },
                "citations": { "type": "array", "items": { "type": "string", "format": "uri" } }
              }
            }
          }
        }
      },
      "Summary": {
        "type": "object",
        "properties": {
          "score": { "type": "number", "description": "Klepha visibility score, 0-100" },
          "mentionRate": { "type": "number" },
          "citations": { "type": "integer" },
          "shareOfVoice": { "type": "number" },
          "byEngine": { "type": "object", "additionalProperties": { "type": "number" } }
        }
      },
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } }
      }
    }
  }
}
