openapi: 3.1.0
info:
  title: LoginDetective API
  description: 'LoginDetective public detection API. Authenticate with `X-API-Key` (prefix `ld_`).


    Analyze IP addresses, emails, and user agents; receive a unified suspicious score and raw upstream field data. Free tier:
    100 requests per tenant per UTC day.


    Generated from openapi.yaml by excluding paths, operations, tags, security schemes, and schemas marked `x-internal: true`.'
  version: 1.0.0
  contact:
    name: LoginDetective
    url: https://logindetective.io
servers:
- url: https://api.logindetective.io
  description: Production
- url: http://localhost:8080
  description: Local development
tags:
- name: Health
- name: Detection
- name: Pricing
paths:
  /health:
    get:
      tags:
      - Health
      summary: Health check
      operationId: healthCheck
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /pricing:
    get:
      tags:
      - Pricing
      summary: List pricing plans
      operationId: getPricing
      security: []
      responses:
        '200':
          description: Pricing information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PricingResponse'
  /openai-usage:
    get:
      tags:
      - Usage
      summary: OpenAI token consumption and estimated cost
      description: 'Aggregate OpenAI token usage and estimated USD cost across every

        operation (`holistic`, `ua_ai`, `chat`). Public for now while we build

        out transparency; may be moved behind auth later.

        '
      operationId: getOpenAIUsage
      security: []
      responses:
        '200':
          description: Aggregate usage and cost
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OpenAIUsageResponse'
  /v1/analyze:
    post:
      tags:
      - Detection
      summary: Full login analysis
      description: 'Analyzes IP, email, and/or user-agent. At least one field is required.

        Returns raw upstream data plus unified suspicious_score.

        '
      operationId: analyzeFull
      security:
      - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeRequest'
      responses:
        '200':
          description: Analysis complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Email not verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/analyze/ip:
    post:
      tags:
      - Detection
      summary: IP-only analysis
      operationId: analyzeIp
      security:
      - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeIpRequest'
      responses:
        '200':
          description: IP analysis complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/analyze/email:
    post:
      tags:
      - Detection
      summary: Email-only analysis
      operationId: analyzeEmail
      security:
      - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeEmailRequest'
      responses:
        '200':
          description: Email analysis complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/analyze/useragent:
    post:
      tags:
      - Detection
      summary: AI user-agent analysis
      operationId: analyzeUserAgent
      security:
      - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeUserAgentRequest'
      responses:
        '200':
          description: User-agent analysis complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/analyze/try:
    post:
      tags:
      - Detection
      summary: Anonymous trial analyze (homepage demo)
      description: 'Public, unauthenticated trial endpoint used by the marketing site.

        Rate limited to **5 requests per client IP per UTC day**. Does not

        persist a detection row and does not consume tenant quota.

        '
      operationId: analyzeTry
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeRequest'
      responses:
        '200':
          description: Analysis complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrialDetectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/detections:
    get:
      tags:
      - Detection
      summary: List detection history for the tenant (API key or JWT)
      operationId: listDetections
      security:
      - apiKeyAuth: []
      parameters:
      - $ref: '#/components/parameters/Page'
      - $ref: '#/components/parameters/PerPage'
      - name: suspicious_score
        in: query
        schema:
          $ref: '#/components/schemas/SuspiciousScore'
      - name: from
        in: query
        schema:
          type: string
          format: date-time
      - name: to
        in: query
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Paginated detections
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetectionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/detections/{id}:
    get:
      tags:
      - Detection
      summary: Get single detection by ID
      operationId: getDetection
      security:
      - apiKeyAuth: []
      parameters:
      - $ref: '#/components/parameters/UuidPathId'
      responses:
        '200':
          description: Detection detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetectionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key prefixed with ld_
  parameters:
    UuidPathId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Authentication required or invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: Daily or request rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    SuspiciousScore:
      type: string
      enum:
      - very_low
      - low
      - high
      - very_high
      description: LoginDetective unified risk level (four-band verdict)
    HealthResponse:
      type: object
      required:
      - status
      properties:
        status:
          type: string
          example: ok
        version:
          type: string
          example: 1.0.0
    ErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object
          additionalProperties: true
    AnalyzeRequest:
      type: object
      description: At least one of ip, email, or user_agent must be provided
      properties:
        ip:
          type: string
          description: IPv4 or IPv6 address
          example: 8.8.8.8
        email:
          type: string
          format: email
        user_agent:
          type: string
          description: When provided, static parsing and OpenAI judgment always run (cached)
    AnalyzeIpRequest:
      type: object
      required:
      - ip
      properties:
        ip:
          type: string
        include_info:
          type: boolean
          default: true
          description: Pass info=true to IPDetective for ASN/country/type
    AnalyzeEmailRequest:
      type: object
      required:
      - email
      properties:
        email:
          type: string
          format: email
    AnalyzeUserAgentRequest:
      type: object
      required:
      - user_agent
      properties:
        user_agent:
          type: string
          description: Static parsing and OpenAI judgment always run (cached)
    IpAnalysis:
      type: object
      description: Raw IPDetective response (info=true)
      properties:
        ip:
          type: string
        bot:
          type: boolean
        type:
          type: string
          enum:
          - datacenter
          - bot
          - vpn
          - proxy
          - unknown
          nullable: true
        asn:
          type: integer
          nullable: true
        asn_description:
          type: string
          nullable: true
        country_code:
          type: string
          nullable: true
        country_name:
          type: string
          nullable: true
        cached:
          type: boolean
          description: True if served from LoginDetective cache
    EmailAnalysis:
      type: object
      description: Raw EmailDetective response
      properties:
        email:
          type: string
        user:
          type: string
          nullable: true
        domain:
          type: string
          nullable: true
        did_you_mean:
          type: string
          nullable: true
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        valid_email:
          type: boolean
        valid_spf:
          type: boolean
        valid_dmarc:
          type: boolean
        valid_mx:
          type: boolean
        valid_tld:
          type: boolean
        nonsense:
          type: boolean
        role:
          type: boolean
        free:
          type: boolean
        disposable:
          type: boolean
        score:
          type: integer
          minimum: 0
          maximum: 100
          description: EmailDetective legitimacy score (higher = more legitimate)
        suspicion_rating:
          type: string
          enum:
          - LOW
          - MEDIUM
          - HIGH
          description: EmailDetective's own rating (distinct from suspicious_score)
        cached:
          type: boolean
    UserAgentStaticAnalysis:
      type: object
      properties:
        browser:
          type: string
          nullable: true
        browser_version:
          type: string
          nullable: true
        os:
          type: string
          nullable: true
        os_version:
          type: string
          nullable: true
        device_type:
          type: string
          enum:
          - desktop
          - mobile
          - tablet
          - bot
          - unknown
        is_bot:
          type: boolean
        is_crawler:
          type: boolean
        is_mobile:
          type: boolean
        static_risk_score:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Normalized static UA risk 0-1
    UserAgentAiAnalysis:
      type: object
      description: 'AI-judged user-agent risk. Returns a discrete four-band rating

        (very_low / low / high / very_high) rather than a numeric

        confidence — LLMs are unreliable at calibrated numeric scores

        but reliable at picking from a small enum. The rating is mapped

        to a fixed numeric score server-side when combined with the

        static UA risk.

        '
      properties:
        suspicious_rating:
          $ref: '#/components/schemas/SuspiciousScore'
          description: Four-band UA risk verdict from the AI judge.
        ai_reasoning:
          type: string
        cached:
          type: boolean
    UserAgentAnalysis:
      type: object
      properties:
        static:
          $ref: '#/components/schemas/UserAgentStaticAnalysis'
        ai:
          $ref: '#/components/schemas/UserAgentAiAnalysis'
          nullable: true
    ComponentScores:
      type: object
      description: Normalized 0-1 risk per component (higher = more suspicious)
      properties:
        ip:
          type: number
          format: float
          nullable: true
        email:
          type: number
          format: float
          nullable: true
        user_agent:
          type: number
          format: float
          nullable: true
        combined:
          type: number
          format: float
          description: Final combined score used for thresholding
    DetectionResponse:
      type: object
      required:
      - id
      - suspicious_score
      - created_at
      properties:
        id:
          type: string
          format: uuid
        suspicious_score:
          $ref: '#/components/schemas/SuspiciousScore'
        suspicious_reasoning:
          type: string
          description: Human-readable synthesis of all signals
        component_scores:
          $ref: '#/components/schemas/ComponentScores'
        ip:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        user_agent:
          type: string
          nullable: true
        ip_analysis:
          $ref: '#/components/schemas/IpAnalysis'
          nullable: true
        email_analysis:
          $ref: '#/components/schemas/EmailAnalysis'
          nullable: true
        ua_analysis:
          $ref: '#/components/schemas/UserAgentAnalysis'
          nullable: true
        created_at:
          type: string
          format: date-time
    TrialDetectionResponse:
      type: object
      description: Result of an anonymous trial analyze. Includes remaining daily attempts.
      required:
      - suspicious_score
      - created_at
      - trial_remaining
      - trial_limit
      properties:
        suspicious_score:
          $ref: '#/components/schemas/SuspiciousScore'
        suspicious_reasoning:
          type: string
        component_scores:
          $ref: '#/components/schemas/ComponentScores'
        ip:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        user_agent:
          type: string
          nullable: true
        ip_analysis:
          $ref: '#/components/schemas/IpAnalysis'
          nullable: true
        email_analysis:
          $ref: '#/components/schemas/EmailAnalysis'
          nullable: true
        ua_analysis:
          $ref: '#/components/schemas/UserAgentAnalysis'
          nullable: true
        trial_remaining:
          type: integer
          description: Number of trial attempts remaining for this IP today
        trial_limit:
          type: integer
          description: Daily trial limit per IP
        created_at:
          type: string
          format: date-time
    DetectionSummary:
      type: object
      required:
      - id
      - suspicious_score
      - created_at
      properties:
        id:
          type: string
          format: uuid
        ip:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        suspicious_score:
          $ref: '#/components/schemas/SuspiciousScore'
        created_at:
          type: string
          format: date-time
    DetectionListResponse:
      type: object
      required:
      - detections
      - pagination
      properties:
        detections:
          type: array
          items:
            $ref: '#/components/schemas/DetectionSummary'
        pagination:
          $ref: '#/components/schemas/Pagination'
    Pagination:
      type: object
      required:
      - page
      - per_page
      - total
      - total_pages
      properties:
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
        total_pages:
          type: integer
    PricingResponse:
      type: object
      required:
      - plans
      - current_public_tier
      properties:
        current_public_tier:
          type: string
          example: free
        plans:
          type: array
          items:
            $ref: '#/components/schemas/PricingPlan'
    OpenAIUsageResponse:
      type: object
      required:
      - total_requests
      - prompt_tokens
      - completion_tokens
      - total_tokens
      - cost_usd
      - by_model
      - by_operation
      properties:
        total_requests:
          type: integer
          description: Number of OpenAI API calls recorded
        prompt_tokens:
          type: integer
        completion_tokens:
          type: integer
        total_tokens:
          type: integer
        cost_usd:
          type: number
          format: double
          description: Estimated USD cost (computed at write time from our pricing table)
        by_model:
          type: array
          items:
            $ref: '#/components/schemas/OpenAIUsageBreakdown'
        by_operation:
          type: array
          items:
            $ref: '#/components/schemas/OpenAIUsageBreakdown'
    OpenAIUsageBreakdown:
      type: object
      required:
      - key
      - prompt_tokens
      - completion_tokens
      - total_tokens
      - cost_usd
      - request_count
      properties:
        key:
          type: string
          description: Model id (for by_model) or operation name (for by_operation)
        prompt_tokens:
          type: integer
        completion_tokens:
          type: integer
        total_tokens:
          type: integer
        cost_usd:
          type: number
          format: double
        request_count:
          type: integer
    PricingPlan:
      type: object
      required:
      - id
      - name
      - price_monthly_usd
      - daily_limit
      - coming_soon
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        price_monthly_usd:
          type: number
          nullable: true
        daily_limit:
          type: integer
          nullable: true
        features:
          type: array
          items:
            type: string
        coming_soon:
          type: boolean
