openapi: 3.1.0
info:
  title: RetailIntel API
  version: "1.0.0"
  description: |
    Anthropic-shaped APIs for Indian retail intelligence. JSON-only
    bodies; SSE for streaming endpoints. Anonymous traffic accepted on
    /v1/ask and /v1/sources/*; Clerk Bearer required for /v1/research
    and /v1/follow writes.
  contact:
    name: RetailIntel desk
    email: developers@theclarity.us
servers:
  - url: https://retailintel.in
paths:
  /api/v1/ask:
    post:
      summary: Cited answer over India retail signals
      operationId: askQuery
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  items:
                    type: object
                    required: [role, content]
                    properties:
                      role: { type: string, enum: [user, assistant] }
                      content: { type: string }
                stream: { type: boolean, default: false }
                idempotency_key: { type: string }
                session_id: { type: integer }
      responses:
        "200":
          description: JSON answer (stream=false) or text/event-stream (stream=true)
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /api/v1/research:
    post:
      summary: Compose a research briefing on a topic
      operationId: createResearch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [topic]
              properties:
                topic: { type: string, maxLength: 240 }
                preset: { type: string, enum: [quick, standard, deep], default: standard }
                seed_signals: { type: array, items: { type: string } }
                seed_entities: { type: array, items: { type: string } }
                access_scope: { type: string, enum: [public, private], default: public }
      responses:
        "200":
          description: Research completed (synchronous v1)
          content:
            application/json:
              schema:
                type: object
                properties:
                  investigation_id: { type: string, format: uuid }
                  status: { type: string, enum: [completed] }
                  report_id: { type: string, format: uuid }
                  report_slug: { type: string }
                  report_url: { type: string }
                  citation_count: { type: integer }
                  body_chars: { type: integer }
                  cost_usd: { type: number }
        "422":
          description: insufficient_corpus — no signals matched the topic
        "429": { $ref: "#/components/responses/RateLimited" }
  /api/v1/research/{id}:
    get:
      summary: Retrieve an investigation status + report
      operationId: getResearch
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Investigation snapshot
        "404":
          description: not_found
  /api/v1/follow:
    post:
      summary: Track a brand, theme, signal, or source
      operationId: createFollow
      security: [{ ClerkBearer: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [follow_type, label]
              properties:
                follow_type: { type: string, enum: [brand, theme, signal, source, topic] }
                ref_id: { type: string, format: uuid }
                ref_slug: { type: string }
                label: { type: string, maxLength: 120 }
                channels:
                  type: array
                  items: { type: string, enum: [inbox, email, webhook, sms] }
                frequency: { type: string, enum: [realtime, daily, weekly] }
                sensitivity: { type: string, enum: [high, normal, low] }
      responses:
        "201": { description: Follow created }
        "200": { description: Follow already exists (idempotent) }
        "401":
          description: auth_required
        "400":
          description: validation error
    get:
      summary: List the authenticated user's follows + unread inbox count
      operationId: listFollows
      security: [{ ClerkBearer: [] }]
      responses:
        "200": { description: follows + unread_inbox_count }
        "401": { description: auth_required }
  /api/v1/follow/{id}:
    delete:
      summary: Remove a follow
      operationId: deleteFollow
      security: [{ ClerkBearer: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: removed }
        "404": { description: not_found }
        "401": { description: auth_required }
  /api/v1/sources/health:
    get:
      summary: Source roster + Oracle SLO snapshot
      operationId: getSourcesHealth
      responses:
        "200":
          description: stats + oracle + sources[]
          headers:
            Cache-Control: { schema: { type: string }, description: "public, max-age=60, stale-while-revalidate=300" }
  /api/v1/sources/propose:
    post:
      summary: Suggest a new source for the catalogue
      operationId: proposeSource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [proposed_name, sample_url]
              properties:
                proposed_name: { type: string, maxLength: 120 }
                sample_url: { type: string, format: uri, maxLength: 500 }
                notes: { type: string, maxLength: 1000 }
      responses:
        "201": { description: queued }
        "200": { description: duplicate (already proposed) }
        "429": { $ref: "#/components/responses/RateLimited" }
components:
  securitySchemes:
    ClerkBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Clerk-issued user JWT. Get one at https://retailintel.in/login.
  responses:
    InvalidRequest:
      description: Malformed request
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
              message: { type: string }
    RateLimited:
      description: Too many requests; back off
      headers:
        Retry-After: { schema: { type: integer }, description: "Seconds to wait before retrying" }
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string, enum: [rate_limited] }
              retry_after: { type: integer }
