{
  "openapi": "3.1.0",
  "info": {
    "title": "Husearch API",
    "version": "1.0.0",
    "summary": "Public surface of the Husearch HTTP API.",
    "description": "Husearch is an AI research workspace for scholars. The HTTP API is primarily authenticated and intended for the first-party web client; this document covers the small public surface (health, waitlist, current-user lookup, and the research result stream). Authenticated endpoints exist for projects, chapters, documents, sources, annotations, citations, research orchestration, and export — those require a Clerk session token in the `Authorization: Bearer <jwt>` header and are not documented here.",
    "contact": {
      "name": "Husearch",
      "url": "https://husearch.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://husearch.com",
      "description": "Production"
    }
  ],
  "tags": [
    { "name": "Health", "description": "Liveness checks." },
    { "name": "Waitlist", "description": "Early-access email collection." },
    { "name": "Auth", "description": "Current-user lookup (works unauthenticated)." },
    { "name": "Research streams", "description": "Server-Sent Events for in-progress research sessions." }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": ["Health"],
        "summary": "Liveness check",
        "description": "Returns `{status: 'ok'}` and a server timestamp. Always 200.",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Health" },
                "example": { "status": "ok", "timestamp": "2026-05-15T12:34:56.000Z" }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "curl",
            "source": "curl https://husearch.com/api/health"
          },
          {
            "lang": "JavaScript",
            "label": "fetch",
            "source": "const res = await fetch('https://husearch.com/api/health')\nconst { status, timestamp } = await res.json()"
          }
        ]
      }
    },
    "/api/waitlist": {
      "post": {
        "tags": ["Waitlist"],
        "summary": "Add an email to the early-access waitlist",
        "description": "Validates and persists an email address. Idempotent: re-submitting the same email returns 200 with a friendly message instead of 201.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WaitlistRequest" },
              "example": { "email": "researcher@university.edu" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Email accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Message" },
                "example": { "message": "Welcome to the waitlist!" }
              }
            }
          },
          "200": {
            "description": "Email already on the waitlist.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Message" },
                "example": { "message": "You're already on the list!" }
              }
            }
          },
          "400": {
            "description": "Email missing or not a valid address.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Please enter a valid email address." }
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Something went wrong on our end. Please try again." }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "curl",
            "source": "curl -X POST https://husearch.com/api/waitlist \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"researcher@university.edu\"}'"
          },
          {
            "lang": "JavaScript",
            "label": "fetch",
            "source": "const res = await fetch('https://husearch.com/api/waitlist', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ email: 'researcher@university.edu' }),\n})\nconst { message } = await res.json()"
          }
        ]
      }
    },
    "/api/auth/me": {
      "get": {
        "tags": ["Auth"],
        "summary": "Get the current user, if any",
        "description": "Returns `{authenticated: false}` for anonymous requests, or the current user's profile when a valid Clerk session token is present in the `Authorization` header.",
        "security": [{}, { "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Either the current user or an anonymous response.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MeResponse" },
                "examples": {
                  "anonymous": {
                    "summary": "Unauthenticated caller",
                    "value": { "authenticated": false }
                  },
                  "signedIn": {
                    "summary": "Signed-in user",
                    "value": {
                      "authenticated": true,
                      "user": {
                        "id": 42,
                        "clerk_id": "user_2abc123",
                        "display_name": "Dr. Jane Researcher",
                        "email": "jane@university.edu",
                        "affiliation": "Stanford University",
                        "bio": "Historian of 20th-century economic thought.",
                        "avatar_url": "https://img.clerk.com/.../avatar.png"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "curl (anonymous)",
            "source": "curl https://husearch.com/api/auth/me"
          },
          {
            "lang": "curl",
            "label": "curl (authenticated)",
            "source": "curl https://husearch.com/api/auth/me \\\n  -H 'Authorization: Bearer <clerk_session_jwt>'"
          },
          {
            "lang": "JavaScript",
            "label": "fetch",
            "source": "const res = await fetch('https://husearch.com/api/auth/me', {\n  headers: { Authorization: `Bearer ${clerkToken}` },\n})\nconst body = await res.json()"
          }
        ]
      }
    },
    "/api/research/{id}/stream": {
      "get": {
        "tags": ["Research streams"],
        "summary": "Subscribe to a research session's status (SSE)",
        "description": "Server-Sent Events stream broadcasting the live status of an in-progress research session: orchestration, per-agent state (data, theory, history, critic), synthesis, and granular tool-call activity. Closes when the session reaches a terminal state (`completed`, `failed`, or `partial`). The HMAC-signed `token` query parameter is required because `EventSource` cannot send `Authorization` headers; it is issued in the response body of the authenticated `POST /api/research` call.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "integer" },
            "description": "Research session ID."
          },
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "HMAC-signed stream token. Format: `sessionId:expiresAtMs:signature`. Issued by `POST /api/research`."
          }
        ],
        "responses": {
          "200": {
            "description": "SSE stream of status events. Each event is a JSON object with at minimum `sessionId` and `status`.",
            "content": {
              "text/event-stream": {
                "schema": { "$ref": "#/components/schemas/ResearchStatusEvent" },
                "example": "data: {\"sessionId\":123,\"status\":\"orchestrating\"}\n\ndata: {\"sessionId\":123,\"status\":\"agents_running\",\"agents\":{\"data\":\"running\",\"theory\":\"running\",\"history\":\"waiting\",\"critic\":\"waiting\"}}\n\ndata: {\"sessionId\":123,\"status\":\"synthesizing\",\"synthesizer\":\"running\"}\n\ndata: {\"sessionId\":123,\"status\":\"completed\"}\n\n"
              }
            }
          },
          "403": {
            "description": "Stream token missing, invalid, or expired.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Stream token is invalid or has expired." }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "JavaScript",
            "label": "EventSource",
            "source": "// `token` is returned in the body of the authenticated POST /api/research call.\nconst es = new EventSource(`/api/research/${sessionId}/stream?token=${token}`)\nes.onmessage = (e) => {\n  const event = JSON.parse(e.data)\n  console.log(event.status, event.agents)\n  if (['completed', 'failed', 'partial'].includes(event.status)) es.close()\n}"
          }
        ]
      }
    },
    "/api/extraction-stream/{id}": {
      "get": {
        "tags": ["Research streams"],
        "summary": "Subscribe to a document extraction's progress (SSE)",
        "description": "Server-Sent Events stream for Extend.ai-driven document extraction progress. Like the research stream, this is auth-tokenized via a signed query parameter because `EventSource` cannot send headers.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "token", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "SSE stream of extraction events.",
            "content": { "text/event-stream": { "schema": { "type": "object" } } }
          },
          "403": { "description": "Token missing or invalid." }
        },
        "x-codeSamples": [
          {
            "lang": "JavaScript",
            "label": "EventSource",
            "source": "const es = new EventSource(`/api/extraction-stream/${extractionId}?token=${token}`)\nes.onmessage = (e) => console.log(JSON.parse(e.data))"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Clerk-issued session token. Authenticated endpoints outside this public document expect `Authorization: Bearer <token>`."
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "required": ["status", "timestamp"],
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "WaitlistRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address to add to the waitlist. Case-insensitive; trimmed and lowercased server-side."
          }
        }
      },
      "Message": {
        "type": "object",
        "required": ["message"],
        "properties": {
          "message": { "type": "string" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message." }
        }
      },
      "MeResponse": {
        "oneOf": [
          {
            "type": "object",
            "required": ["authenticated"],
            "properties": {
              "authenticated": { "type": "boolean", "enum": [false] }
            }
          },
          {
            "type": "object",
            "required": ["authenticated", "user"],
            "properties": {
              "authenticated": { "type": "boolean", "enum": [true] },
              "user": { "$ref": "#/components/schemas/User" }
            }
          }
        ]
      },
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "clerk_id": { "type": "string" },
          "display_name": { "type": "string", "nullable": true },
          "email": { "type": "string", "format": "email" },
          "affiliation": { "type": "string", "nullable": true },
          "bio": { "type": "string", "nullable": true },
          "avatar_url": { "type": "string", "format": "uri", "nullable": true }
        }
      },
      "ResearchStatusEvent": {
        "type": "object",
        "description": "Shape of an SSE `data:` payload. Discriminated loosely by presence of fields.",
        "properties": {
          "sessionId": { "type": "integer" },
          "status": {
            "type": "string",
            "enum": ["orchestrating", "agents_running", "synthesizing", "completed", "partial", "failed", "batch_processing", "unknown"]
          },
          "agents": {
            "type": "object",
            "description": "Per-agent status. Keys are the agent names; values are state strings.",
            "additionalProperties": {
              "type": "string",
              "enum": ["waiting", "running", "completed", "failed"]
            },
            "example": { "data": "running", "theory": "waiting", "history": "waiting", "critic": "waiting" }
          },
          "synthesizer": {
            "type": "string",
            "enum": ["waiting", "running", "completed", "failed"]
          },
          "type": { "type": "string", "description": "Set to `agent_activity` for granular tool-call events." },
          "error": { "type": "string", "description": "Present on `failed`/`partial`." }
        }
      }
    }
  }
}
