{
  "openapi": "3.1.0",
  "info": {
    "title": "Serge API",
    "version": "2.0.0",
    "description": "Serge tells e-commerce sites whether users behind AI agents can actually find their products and buy them. Scan any e-commerce domain and get a score out of 100. Each scan returns findings and specific fixes. The scoring methodology is currently being rebuilt from the ground up — see https://www.serge.ai/methodology for the latest status.\n\nRate limits: 10 scans per hour per IP, 5 scans per hour per domain. All responses include X-RateLimit-* headers.",
    "contact": {
      "name": "Serge API Support",
      "url": "https://www.serge.ai/docs",
      "email": "api@serge.ai"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://www.serge.ai/terms"
    }
  },
  "servers": [
    {
      "url": "https://www.serge.ai",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/scan": {
      "post": {
        "operationId": "initiateScan",
        "summary": "Initiate a domain scan",
        "description": "Queues a Serge scan for the given e-commerce domain. The scan crawls the site deterministically (no LLM at scan time) and measures whether AI agents can find products and add them to the cart. Returns a Server-Sent Events stream with real-time progress updates and the final score.",
        "tags": ["Scans"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanRequest"
              },
              "example": {
                "domain": "stripe.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Server-Sent Events stream with scan progress",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE stream. Events: 'status' (scan phase update), 'crawl' (URL crawl progress), 'layer' (layer result with checks), 'complete' (scan ID, scores, and domain), 'error' (failure message)."
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Invalid request — domain is missing, malformed, or fails validation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidDomain": {
                    "summary": "Invalid domain format",
                    "value": {
                      "error": "Please enter a valid domain (e.g., stripe.com)"
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed JSON body",
                    "value": {
                      "error": "Invalid JSON body"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Too many scans. Please try again later."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Scan failed. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/scan/{id}": {
      "get": {
        "operationId": "getScanResults",
        "summary": "Get scan results",
        "description": "Returns the full scan results for a completed scan, including the overall score, per-layer scores, and all 41 individual check results with pass/fail/warn status, messages, and fix-it suggestions.",
        "tags": ["Scans"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The UUID of the scan to retrieve",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          }
        ],
        "responses": {
          "200": {
            "description": "Scan results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanResult"
                },
                "example": {
                  "id": "550e8400-e29b-41d4-a716-446655440000",
                  "domain": "stripe.com",
                  "overallScore": 72,
                  "layerScores": {
                    "access": 83,
                    "identity": 90,
                    "interaction": 58,
                    "content": 65,
                    "integration": 60
                  },
                  "checkResults": [
                    {
                      "layer": 1,
                      "key": "llms_txt",
                      "status": "pass",
                      "message": "llms.txt found with 15 lines and API documentation references",
                      "fix": null,
                      "evidence": {
                        "lineCount": 15,
                        "hasApiMentions": true
                      }
                    }
                  ],
                  "createdAt": "2026-03-24T10:00:00.000Z",
                  "lastSeenAt": "2026-03-24T10:00:00.000Z",
                  "seenCount": 1
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Caching directive",
                "example": "public, max-age=3600, stale-while-revalidate=600"
              }
            }
          },
          "400": {
            "description": "Invalid scan ID format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid scan ID"
                }
              }
            }
          },
          "404": {
            "description": "Scan not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Scan not found"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ScanRequest": {
        "type": "object",
        "required": ["domain"],
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 1,
            "maxLength": 253,
            "description": "The e-commerce domain to scan (e.g., 'yourstore.ch'). No protocol prefix — just the hostname.",
            "example": "yourstore.ch"
          }
        }
      },
      "ScanResult": {
        "type": "object",
        "required": ["id", "domain", "overallScore", "layerScores", "checkResults", "createdAt", "lastSeenAt", "seenCount"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique scan identifier"
          },
          "domain": {
            "type": "string",
            "description": "The scanned domain"
          },
          "overallScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Overall Serge score (0-100). Measures how easy it is for an AI agent to find a product on the site and add it to the cart."
          },
          "layerScores": {
            "$ref": "#/components/schemas/LayerScores"
          },
          "checkResults": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CheckResult"
            },
            "description": "All 41 individual check results"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the scan was first created"
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time",
            "description": "When these exact results were last seen (updated on dedup)"
          },
          "seenCount": {
            "type": "integer",
            "minimum": 1,
            "description": "How many times this exact result set has been seen"
          }
        }
      },
      "LayerScores": {
        "type": "object",
        "description": "Per-dimension scores, each 0-100",
        "required": ["access", "identity", "interaction", "content", "integration"],
        "properties": {
          "access": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Dimension 1: Can agents reach you?"
          },
          "identity": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Dimension 2: Can agents understand what you are?"
          },
          "interaction": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Dimension 3: Can browser agents use your site?"
          },
          "content": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Dimension 4: Can agents parse your content?"
          },
          "integration": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Dimension 5: Can agents programmatically use your service?"
          }
        }
      },
      "CheckResult": {
        "type": "object",
        "required": ["layer", "key", "status", "message"],
        "properties": {
          "layer": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "description": "Which dimension this check belongs to (1-5)"
          },
          "key": {
            "type": "string",
            "description": "Unique check identifier (e.g., 'llms_txt', 'openapi_exists')"
          },
          "status": {
            "type": "string",
            "enum": ["pass", "fail", "warn"],
            "description": "Check result: pass (100%), warn (50%), or fail (0%)"
          },
          "message": {
            "type": "string",
            "description": "Human-readable result description"
          },
          "fix": {
            "type": "string",
            "nullable": true,
            "description": "Suggested fix for failing checks. Null when status is 'pass'."
          },
          "evidence": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Supporting data for the check result"
          },
          "blocked": {
            "type": "boolean",
            "description": "True when the check could not run due to bot protection (WAF challenge) or robots.txt blocking. Blocked checks score as 0 but indicate an access issue, not a missing resource."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message"
          }
        }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Maximum number of requests allowed in the current window",
        "schema": {
          "type": "integer"
        },
        "example": 10
      },
      "X-RateLimit-Remaining": {
        "description": "Number of requests remaining in the current window",
        "schema": {
          "type": "integer"
        },
        "example": 7
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp when the rate limit window resets",
        "schema": {
          "type": "integer"
        },
        "example": 1711324800
      },
      "Retry-After": {
        "description": "Seconds until the rate limit resets (only on 429 responses)",
        "schema": {
          "type": "integer"
        },
        "example": 3600
      }
    }
  },
  "tags": [
    {
      "name": "Scans",
      "description": "Domain scanning and results retrieval"
    }
  ],
  "webhooks": {
    "scoreChanged": {
      "post": {
        "operationId": "onScoreChanged",
        "summary": "Score changed",
        "description": "Fires when a monitored domain's Serge score changes after a rescan.",
        "tags": ["Webhooks"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["event", "domain", "oldScore", "newScore", "timestamp"],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": ["score.changed"],
                    "description": "Event type"
                  },
                  "domain": {
                    "type": "string",
                    "description": "The domain whose score changed"
                  },
                  "oldScore": {
                    "type": "integer",
                    "description": "Previous score"
                  },
                  "newScore": {
                    "type": "integer",
                    "description": "New score"
                  },
                  "timestamp": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the change was detected"
                  }
                }
              },
              "example": {
                "event": "score.changed",
                "domain": "example.com",
                "oldScore": 31,
                "newScore": 45,
                "timestamp": "2026-03-24T06:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook received"
          }
        }
      }
    }
  }
}
