openapi: 3.1.0
info:
  title: Siteline API
  version: 1.0.0
  description: |
    AI agent readiness scanner for public websites. Evaluates Signal, Navigate,
    Absorb, and Perform (SNAP) across any URL.

    Rate limiting conforms to the [Graceful Boundaries](https://github.com/snapsynapse/graceful-boundaries)
    specification at Level 4: structured refusal, proactive discovery, constructive
    guidance, and proactive RateLimit headers on success responses.
  contact:
    name: PAICE
    url: https://paice.work/
  license:
    name: Proprietary
    url: https://siteline.to/terms/

servers:
  - url: https://siteline.to
    description: Production

externalDocs:
  description: AI Tool Watch — structured tracking of AI capabilities across products
  url: https://aitool.watch/

paths:
  /api/scan:
    get:
      operationId: scanUrl
      summary: Scan a public URL for agent readiness
      description: |
        Evaluates a public website across the SNAP rubric and returns a grade
        (A-F), score (0-100), per-pillar breakdown, findings, and remediation
        tier. Results are cached for 24 hours per submitted URL scope.
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
          description: Public URL or domain to scan.
          example: https://example.com
        - name: debug
          in: query
          required: false
          schema:
            type: string
            enum: ["1", "true"]
          description: Include fetch timing and debug details in the response.
      responses:
        "200":
          description: Scan result
          headers:
            X-Siteline-Request-Id:
              $ref: "#/components/headers/X-Siteline-Request-Id"
            X-Siteline-Cache:
              $ref: "#/components/headers/X-Siteline-Cache"
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimit-Policy"
          content:
            application/json:
              schema:
                # Full schema with agenticEnablement inlined at /api/v1/openapi.json.
                # Prefer the JSON spec for complete schema resolution.
                $ref: "schemas/scan-result.schema.json"
        "400":
          description: Invalid or missing URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimit-Policy"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitError"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/result:
    get:
      operationId: getResult
      summary: Look up a stored scan result
      description: |
        Retrieves a previously stored scan result by result ID or domain.
        Homepage result IDs follow `domain-slug-YYYYMMDD`; path/query-scoped
        result IDs follow `domain-slug-path-scope-hash-YYYYMMDD`.
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: Result ID (e.g. example-com-20260322 or example-com-pricing-a1b2c3d4-20260322) or domain (e.g. example.com).
      responses:
        "200":
          description: Stored scan result
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimit-Policy"
          content:
            application/json:
              schema:
                $ref: "schemas/scan-result.schema.json"
        "400":
          description: Missing id parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No result found for the given ID or domain
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      scanUrl:
                        type: string
                        description: URL to trigger a fresh scan for this domain.
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitError"

  /api/limits:
    get:
      operationId: getLimits
      summary: Rate limit and policy discovery
      description: |
        Returns all enforced rate limits, SSRF protection policy, and links
        to relevant endpoints. Conforms to
        [Graceful Boundaries](https://github.com/snapsynapse/graceful-boundaries)
        Level 4.
      responses:
        "200":
          description: Policy document
          content:
            application/json:
              schema:
                type: object
                properties:
                  service:
                    type: string
                    example: Siteline
                  description:
                    type: string
                  conformance:
                    type: string
                    enum: [not-applicable, none, level-1, level-2, level-3, level-4]
                    example: level-4
                  specUrl:
                    type: string
                    format: uri
                    example: https://github.com/snapsynapse/graceful-boundaries
                  limits:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        endpoint:
                          type: string
                        method:
                          type: string
                        limits:
                          type: array
                          items:
                            type: object
                            properties:
                              type:
                                type: string
                              maxRequests:
                                type: integer
                              windowSeconds:
                                type: integer
                              description:
                                type: string
                        note:
                          type: string
                  ssrfProtection:
                    type: object
                    properties:
                      description:
                        type: string
                      blocked:
                        type: array
                        items:
                          type: string
                  responseHeaders:
                    type: object
                    additionalProperties:
                      type: string
                  links:
                    type: object
                    additionalProperties:
                      type: string

  /api/email-capture:
    post:
      operationId: captureEmail
      summary: Submit an email for scan report delivery
      description: |
        Captures an email address and optional scan result snapshot for
        report delivery. Rate limited to 12 submissions per IP per hour
        and 1 report email per IP per 60 seconds for emailed report snapshots.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                source:
                  type: string
                  description: Submission source (e.g. "pdf-export").
                result:
                  type: object
                  description: Sanitized scan result snapshot.
                  properties:
                    domain:
                      type: string
                    grade:
                      type: string
                    score:
                      type: number
                    label:
                      type: string
                    likelyFailureMode:
                      type: string
                    remediationTier:
                      type: string
      responses:
        "200":
          description: Email captured
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                  requestId:
                    type: string
                  forwarded:
                    type: boolean
        "400":
          description: Invalid email
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateLimitError"

components:
  headers:
    X-Siteline-Request-Id:
      description: Unique request identifier for debugging.
      schema:
        type: string
    X-Siteline-Cache:
      description: Cache status — HIT (in-memory), KV_HIT (durable store), or MISS (fresh scan).
      schema:
        type: string
        enum: [HIT, KV_HIT, MISS]
    RateLimit:
      description: |
        Proactive rate limit status per Graceful Boundaries Level 4.
        Format: `limit=N, remaining=X, reset=Y`
      schema:
        type: string
    RateLimit-Policy:
      description: |
        Rate limit policy per draft-ietf-httpapi-ratelimit-headers.
        Format: `N;w=WINDOW_SECONDS`
      schema:
        type: string
    Retry-After:
      description: Seconds until the rate limit resets. Present on 429 responses.
      schema:
        type: integer

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        detail:
          type: string
        requestId:
          type: string
    RateLimitError:
      type: object
      description: |
        Structured refusal per [Graceful Boundaries](https://github.com/snapsynapse/graceful-boundaries)
        Level 1+. Includes machine-parseable fields and human-readable explanation.
      properties:
        error:
          type: string
          example: rate_limit_exceeded
        detail:
          type: string
          example: You can run up to 10 scans per hour. Try again in 42 seconds.
        limit:
          type: string
          example: 10 scans per IP per hour
        retryAfterSeconds:
          type: integer
          example: 42
        why:
          type: string
          description: Security signal explaining the defensive intent behind this limit.
          example: Siteline is a free service. Rate limits keep it available for everyone and prevent abuse.
        humanUrl:
          type: string
          format: uri
          example: https://siteline.to/
        alternativeEndpoint:
          type: string
          description: Constructive guidance — a cached result may already exist.
        requestId:
          type: string
