{
  "openapi": "3.1.0",
  "info": {
    "title": "AEONiti Agency API",
    "version": "1.0.0",
    "description": "JSON API for pulling full AEO (Answer Engine Optimization) reports for domains. Auth via bearer token from /signup/api. Sync responses ≤256KB; larger reports via async POST + signed webhook delivery.",
    "contact": {
      "name": "AEONiti Support",
      "url": "https://api.aeoniti.com/api",
      "email": "support@aeoniti.com"
    }
  },
  "servers": [
    {"url": "https://api.aeoniti.com", "description": "Production"}
  ],
  "security": [{"bearerAuth": []}],
  "paths": {
    "/v1/health": {
      "get": {
        "summary": "Health probe",
        "description": "Open endpoint (no auth). Returns 200 + service identifier.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Health"}}}
          }
        }
      }
    },
    "/v1/reports/{domain_id}": {
      "get": {
        "summary": "Pull a full domain report (sync, ≤256KB)",
        "description": "Returns the cached report if one exists within the last 24h. Otherwise generates fresh, persists, and returns. Repeated cached fetches cost zero LLM tokens. Reports >256KB return 413 with a hint to use the async endpoint.",
        "parameters": [
          {"name": "domain_id", "in": "path", "required": true,
            "schema": {"type": "integer", "minimum": 1},
            "description": "Numeric domain id you onboarded via the dashboard or onboard API."},
          {"name": "sections", "in": "query", "required": false,
            "schema": {"type": "string"},
            "description": "Comma-separated section keys. Default: all. Valid keys: overview, citations, readiness, signals, recommendations. Unknown keys are silently dropped.",
            "example": "overview,readiness,signals"}
        ],
        "responses": {
          "200": {
            "description": "Report (cached or freshly built)",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Report"}}}
          },
          "400": {"description": "No valid sections requested", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "401": {"description": "Missing or invalid bearer token", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "404": {"description": "Domain not found in your scope", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "413": {"description": "Report exceeds 256KB; use POST /v1/reports/{id}/refresh with callback_url instead", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "429": {"description": "Rate limit, monthly quota, or monthly fresh-refresh quota exceeded", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    },
    "/v1/reports/{domain_id}/refresh": {
      "post": {
        "summary": "Request async report generation with webhook delivery",
        "description": "Spawns a background job that builds the report and POSTs the signed payload to your callback_url. Returns 202 + job_id immediately. The webhook follows the Standard Webhooks spec (standardwebhooks.com) — any standard-webhooks library will verify our signature.",
        "parameters": [
          {"name": "domain_id", "in": "path", "required": true, "schema": {"type": "integer", "minimum": 1}}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/RefreshRequest"}
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RefreshAck"}}}
          },
          "400": {"description": "callback_url must be https; callback_secret must be ≥16 chars; sections invalid"},
          "401": {"description": "Auth"},
          "429": {"description": "Rate limit or fresh-quota exhausted"}
        }
      }
    },
    "/v1/reports/{domain_id}/refresh/{job_id}": {
      "get": {
        "summary": "Poll the status of an async refresh job",
        "description": "Returns 404 while the job is in flight (no attempt recorded yet). Once the webhook has been attempted (success or failure), returns the delivery outcome.",
        "parameters": [
          {"name": "domain_id", "in": "path", "required": true, "schema": {"type": "integer"}},
          {"name": "job_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}
        ],
        "responses": {
          "200": {"description": "Job outcome", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/JobStatus"}}}},
          "404": {"description": "Job not found or still pending"}
        }
      }
    },
    "/signup/api": {
      "post": {
        "summary": "Create a new agency + free-tier API key (public, no auth)",
        "description": "Returns the plaintext bearer token in the response body. The token is ALSO emailed to the provided address as backup. Save it — it cannot be retrieved later.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SignupRequest"}}}
        },
        "responses": {
          "200": {"description": "Account + key created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SignupResponse"}}}},
          "400": {"description": "Invalid email or agency_name"}
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "aeo_<28-byte-hex>",
        "description": "Bearer token from /signup/api. Format: aeo_<56 hex chars>."
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "properties": {
          "status": {"type": "string", "example": "ok"},
          "service": {"type": "string", "example": "aeoniti.wholesale"},
          "version": {"type": "string", "example": "0.1"}
        }
      },
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": {"type": "string", "description": "Human-readable explanation"},
          "code": {"type": "string", "description": "Stable machine-readable error code", "example": "rate_limited"}
        }
      },
      "Report": {
        "type": "object",
        "required": ["meta", "sections"],
        "properties": {
          "meta": {"$ref": "#/components/schemas/ReportMeta"},
          "sections": {"$ref": "#/components/schemas/ReportSections"}
        }
      },
      "ReportMeta": {
        "type": "object",
        "properties": {
          "domain_id": {"type": "integer", "example": 170},
          "domain": {"type": "string", "example": "networkershome.com"},
          "version_id": {"type": "string", "format": "uuid", "description": "Stable identifier for this report snapshot. Stays constant across cached reads of the same snapshot."},
          "generated_at": {"type": "string", "format": "date-time", "description": "When the snapshot was built. Cache TTL is 24h."},
          "cached": {"type": "boolean", "description": "True if served from cache without re-generating."},
          "source": {"type": "string", "example": "aeoniti.v1"}
        }
      },
      "ReportSections": {
        "type": "object",
        "description": "Each section is optional. Sections the agency didn't request via ?sections= are omitted entirely from the response (not null'd).",
        "properties": {
          "overview": {"$ref": "#/components/schemas/OverviewSection"},
          "citations": {"$ref": "#/components/schemas/CitationsSection"},
          "readiness": {"$ref": "#/components/schemas/ReadinessSection"},
          "signals": {"$ref": "#/components/schemas/SignalsSection"},
          "recommendations": {"$ref": "#/components/schemas/RecommendationsSection"}
        }
      },
      "OverviewSection": {
        "type": "object",
        "properties": {
          "has_snapshot": {"type": "boolean"},
          "total_probes": {"type": "integer"},
          "mentioned_count": {"type": "integer"},
          "visibility_pct": {"type": "number", "format": "double"},
          "avg_position": {"type": ["number", "null"], "format": "double"},
          "citation_pct": {"type": "number", "format": "double"},
          "engines_used": {"type": "integer"},
          "per_engine": {"type": "array", "items": {"$ref": "#/components/schemas/OverviewEngineRow"}},
          "completed_at": {"type": ["string", "null"], "format": "date-time"}
        }
      },
      "OverviewEngineRow": {
        "type": "object",
        "properties": {
          "engine": {"type": "string", "example": "sonar"},
          "total": {"type": "integer"},
          "mentioned": {"type": "integer"},
          "visibility_pct": {"type": "number", "format": "double"}
        }
      },
      "CitationsSection": {
        "type": "object",
        "properties": {
          "total_probes": {"type": "integer"},
          "engines_total": {"type": "integer"},
          "engines_cited": {"type": "integer"},
          "body_pct": {"type": "integer"},
          "url_pct": {"type": "integer"},
          "gap_pp": {"type": "integer", "description": "Signed gap in percentage points. Positive = Attribution Loss; negative = Click Loss."},
          "gap_direction": {"type": "string", "enum": ["attribution_loss", "click_loss", "balanced"]},
          "headline_engine": {"type": "string"},
          "per_engine": {"type": "array", "items": {"$ref": "#/components/schemas/CitationsEngineRow"}},
          "top_competitors": {"type": "array", "items": {"$ref": "#/components/schemas/CitationsCompetitorRow"}}
        }
      },
      "CitationsEngineRow": {
        "type": "object",
        "properties": {
          "engine": {"type": "string"},
          "body_pct": {"type": "integer"},
          "url_pct": {"type": "integer"},
          "gap_pp": {"type": "integer"}
        }
      },
      "CitationsCompetitorRow": {
        "type": "object",
        "properties": {
          "brand": {"type": "string"},
          "mention_count": {"type": "integer"}
        }
      },
      "ReadinessSection": {
        "type": "object",
        "properties": {
          "headline_url": {"type": "string"},
          "headline_score": {"type": "integer", "minimum": 0, "maximum": 100},
          "headline_grade": {"type": "string", "enum": ["A", "B", "C", "D", "F", "—"]},
          "per_url": {"type": "array", "items": {"$ref": "#/components/schemas/ReadinessUrlRow"}},
          "breakdown": {"type": "array", "items": {"$ref": "#/components/schemas/ReadinessBreakdownRow"}},
          "urls_total": {"type": "integer"},
          "fetched_at": {"type": ["string", "null"], "format": "date-time"}
        }
      },
      "ReadinessUrlRow": {
        "type": "object",
        "properties": {
          "url": {"type": "string"},
          "score": {"type": ["integer", "null"], "minimum": 0, "maximum": 100},
          "stale": {"type": "boolean", "description": "True if last refresh attempt errored. The score is the last-known-good value."}
        }
      },
      "ReadinessBreakdownRow": {
        "type": "object",
        "properties": {
          "dimension": {"type": "string", "enum": ["numerical_specifics", "named_entities", "authoritative_outbound_links", "author_byline", "last_updated", "content_length"]},
          "scored": {"type": "integer"},
          "max": {"type": "integer"}
        }
      },
      "SignalsSection": {
        "type": "object",
        "properties": {
          "homepage_url": {"type": "string"},
          "cards": {"type": "array", "items": {"$ref": "#/components/schemas/SignalCardRow"}}
        }
      },
      "SignalCardRow": {
        "type": "object",
        "properties": {
          "endpoint": {"type": "string", "enum": ["heading_hierarchy", "snippet_format", "freshness_signal", "snippet_candidates", "headers_intel", "uptime"]},
          "label": {"type": "string"},
          "score": {"type": ["integer", "null"]},
          "stale": {"type": "boolean"},
          "fetched_at": {"type": ["string", "null"], "format": "date-time"}
        }
      },
      "RecommendationsSection": {
        "type": "object",
        "properties": {
          "items": {"type": "array", "items": {"$ref": "#/components/schemas/RecommendationRow"}}
        }
      },
      "RecommendationRow": {
        "type": "object",
        "properties": {
          "priority": {"type": "integer", "description": "1 = highest"},
          "kind": {"type": "string"},
          "title": {"type": "string"},
          "body": {"type": "string"},
          "fix": {"type": "string"},
          "evidence_prompt": {"type": "string"},
          "evidence_engine": {"type": "string"},
          "evidence_quote": {"type": "string"},
          "competitors_in_evidence": {"type": "array", "items": {"type": "string"}}
        }
      },
      "RefreshRequest": {
        "type": "object",
        "required": ["callback_url", "callback_secret"],
        "properties": {
          "callback_url": {"type": "string", "format": "uri", "description": "Must be https://"},
          "callback_secret": {"type": "string", "description": "Pre-shared bytes used to verify our HMAC signature. Minimum 16 chars; 32+ recommended."},
          "sections": {"type": "string", "description": "Optional comma-separated section keys"}
        }
      },
      "RefreshAck": {
        "type": "object",
        "properties": {
          "job_id": {"type": "string", "format": "uuid"},
          "status": {"type": "string", "example": "queued"},
          "callback_url": {"type": "string"}
        }
      },
      "JobStatus": {
        "type": "object",
        "properties": {
          "job_id": {"type": "string", "format": "uuid"},
          "domain_id": {"type": "integer"},
          "succeeded": {"type": "boolean"},
          "attempt_n": {"type": "integer"},
          "response_status": {"type": ["integer", "null"]},
          "error_kind": {"type": ["string", "null"], "enum": ["connect", "timeout", "non_2xx", "build", "client_build", "other", null]},
          "sent_at": {"type": ["string", "null"], "format": "date-time"},
          "report_version": {"type": ["string", "null"], "format": "uuid"}
        }
      },
      "SignupRequest": {
        "type": "object",
        "required": ["email", "agency_name"],
        "properties": {
          "email": {"type": "string", "format": "email"},
          "agency_name": {"type": "string", "maxLength": 80}
        }
      },
      "SignupResponse": {
        "type": "object",
        "properties": {
          "agency_id": {"type": "integer"},
          "api_key": {"type": "string", "description": "Plaintext bearer token. Save this — cannot be retrieved later."},
          "token_prefix": {"type": "string"},
          "tier": {"type": "string", "example": "free"},
          "quotas": {
            "type": "object",
            "properties": {
              "rate_limit_per_min": {"type": "integer"},
              "monthly_quota": {"type": "integer"},
              "monthly_fresh_quota": {"type": "integer"}
            }
          },
          "next": {
            "type": "object",
            "properties": {
              "example": {"type": "string"},
              "docs_url": {"type": "string"}
            }
          }
        }
      }
    }
  }
}
