openapi: 3.1.0
info:
  title: DocuChat Developer API
  version: "1.0.0"
  description: >
    The DocuChat public developer API (`/v1`). Authenticate with a developer API
    key (`dc_ak_<keyId>_<secret>`) created in the DocuChat app under
    **Settings → API**. The key is shown once at creation — store it securely.


    **v1 scope:** read your chatbots and run metered programmatic queries against
    them. Write-management routes (create/update/delete chatbots + documents) and
    streaming query responses are planned follow-ups. All money/usage is
    server-authoritative; the query endpoint debits credits exactly like the app
    chat path.
  contact:
    name: DocuChat
    url: https://docuchat-marketing.pages.dev
servers:
  - url: https://{apiHost}/v1
    description: DocuChat public API base URL (the `/v1` prefix is permanent for v1).
    variables:
      apiHost:
        default: api.docuchat.in
        description: The DocuChat public API host (set to your deployment's host).

security:
  - ApiKeyAuth: []
  - BearerAuth: []

tags:
  - name: Chatbots
    description: Read your chatbots.
  - name: Query
    description: Run metered questions against a chatbot.

paths:
  /chatbots:
    get:
      tags: [Chatbots]
      summary: List your chatbots
      description: >
        Returns the calling account's chatbots, newest first, keyset-paginated.
        Free (not metered).
      operationId: listChatbots
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of chatbots.
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/XRateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/XRateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/XRateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Chatbot" }
                  nextCursor:
                    type: [string, "null"]
                    description: Opaque cursor for the next page; `null` on the last page.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/Internal" }

  /chatbots/{id}:
    get:
      tags: [Chatbots]
      summary: Get one chatbot
      description: Returns a single chatbot owned by the calling account. Free (not metered).
      operationId: getChatbot
      parameters:
        - $ref: "#/components/parameters/ChatbotId"
      responses:
        "200":
          description: The chatbot.
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/XRateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/XRateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/XRateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chatbot" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/Internal" }

  /chatbots/{id}/query:
    post:
      tags: [Query]
      summary: Query a chatbot (metered)
      description: >
        Runs the same retrieval + LLM generation as the DocuChat app/widget chat
        for a chatbot you own, and **debits credits** for the tokens used
        (`source: "api"`). Guardrails run in order: authenticate → rate limit →
        credit/plan gate → per-chatbot spend cap → generate → debit.


        Streaming (`stream: true`) is not supported yet and returns `400`. Omit
        `stream` or set it to `false` for the buffered JSON response.
      operationId: queryChatbot
      parameters:
        - $ref: "#/components/parameters/ChatbotId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QueryRequest" }
            examples:
              basic:
                value:
                  message: "What is the refund policy?"
      responses:
        "200":
          description: The generated answer plus token/credit usage and optional sources.
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/XRateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/XRateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/XRateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QueryResponse" }
        "400":
          description: >
            Validation error — empty `message`, or `stream: true` (not yet supported).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Insufficient credits / account blocked (when billing enforcement is on).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "USAGE_BLOCKED", message: "Insufficient credits. Top up to continue." }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Chatbot is not ready to answer (still provisioning or inactive).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429":
          description: >
            Rate limited (`RATE_LIMITED`) or the chatbot's spend cap tripped
            (`CHATBOT_RATE_LIMITED`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "500": { $ref: "#/components/responses/Internal" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "`X-API-Key: dc_ak_<keyId>_<secret>`"
    BearerAuth:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer dc_ak_<keyId>_<secret>`"

  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: Page size (1–100).
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque pagination cursor returned as `nextCursor` on the previous page.
      schema: { type: string }
    ChatbotId:
      name: id
      in: path
      required: true
      description: The chatbot id.
      schema: { type: string }

  headers:
    XRateLimitLimit:
      description: Requests allowed per window for this key.
      schema: { type: integer }
    XRateLimitRemaining:
      description: Requests remaining in the current window.
      schema: { type: integer }
    XRateLimitReset:
      description: Unix seconds when the current window resets.
      schema: { type: integer }

  responses:
    Unauthorized:
      description: Missing, malformed, unknown, or revoked API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "UNAUTHORIZED", message: "Invalid or revoked API key." }
    NotFound:
      description: The resource was not found (or is not owned by the calling account).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "NOT_FOUND", message: "Chatbot not found." }
    RateLimited:
      description: Too many requests for this key.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "RATE_LIMITED", message: "Rate limit exceeded. Retry after 12s." }
    Internal:
      description: An unexpected server error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "INTERNAL", message: "An unexpected error occurred." }

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Machine-readable code.
          enum:
            - UNAUTHORIZED
            - FORBIDDEN
            - ACCOUNT_READ_ONLY
            - NOT_FOUND
            - VALIDATION_ERROR
            - RATE_LIMITED
            - USAGE_BLOCKED
            - CHATBOT_RATE_LIMITED
            - INTERNAL
        message:
          type: string
          description: Human-readable, safe to surface to end users.

    Chatbot:
      type: object
      required:
        - chatbotId
        - name
        - description
        - provisioningStatus
        - isActive
        - llmModel
        - answerLength
        - showCitations
        - documentCount
        - webUrlCount
        - createdAt
        - updatedAt
      properties:
        chatbotId: { type: string }
        name: { type: string }
        description: { type: [string, "null"] }
        provisioningStatus:
          type: string
          enum: [pending, active, failed]
        isActive: { type: boolean }
        llmModel: { type: string }
        answerLength:
          type: string
          enum: [short, medium, long]
        showCitations: { type: boolean }
        documentCount: { type: integer }
        webUrlCount: { type: integer }
        createdAt: { type: integer, description: Epoch milliseconds. }
        updatedAt: { type: integer, description: Epoch milliseconds. }

    QueryRequest:
      type: object
      required: [message]
      properties:
        message:
          type: string
          minLength: 1
          description: The question to ask the chatbot.
        sessionId:
          type: string
          description: Optional caller-supplied conversation id (defaults to a per-request id).
        stream:
          type: boolean
          default: false
          description: Reserved. `true` is not supported yet and returns `400`.

    QueryResponse:
      type: object
      required: [answer, chatbotId, sessionId, model, usage, sources]
      properties:
        answer: { type: string }
        chatbotId: { type: string }
        sessionId: { type: string }
        model:
          type: string
          description: The model that produced the answer.
        usage: { $ref: "#/components/schemas/QueryUsage" }
        sources:
          type: array
          items: { $ref: "#/components/schemas/QuerySource" }
          description: Citations (empty unless the chatbot has citations enabled and an answer was found).

    QueryUsage:
      type: object
      required: [inputTokens, outputTokens, creditsDebited]
      properties:
        inputTokens: { type: integer }
        outputTokens: { type: integer }
        creditsDebited:
          type: number
          description: Credits debited for this query (0 while billing enforcement is in shadow mode).

    QuerySource:
      type: object
      required: [title, source, score]
      properties:
        title: { type: string }
        source: { type: string }
        page: { type: integer }
        score: { type: number }
