{
  "openapi": "3.1.0",
  "info": {
    "title": "LoginDetective API",
    "description": "LoginDetective public detection API. Authenticate with `X-API-Key` (prefix `ld_`).\n\nAnalyze 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.\n\nGenerated 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\noperation (`holistic`, `ua_ai`, `chat`). Public for now while we build\nout transparency; may be moved behind auth later.\n",
        "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.\nReturns raw upstream data plus unified suspicious_score.\n",
        "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.\nRate limited to **5 requests per client IP per UTC day**. Does not\npersist a detection row and does not consume tenant quota.\n",
        "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\n(very_low / low / high / very_high) rather than a numeric\nconfidence \u2014 LLMs are unreliable at calibrated numeric scores\nbut reliable at picking from a small enum. The rating is mapped\nto a fixed numeric score server-side when combined with the\nstatic UA risk.\n",
        "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"
          }
        }
      }
    }
  }
}