{
  "openapi": "3.0.3",
  "info": {
    "title": "NatSecPulse API",
    "version": "1.0.0",
    "description": "Read-only REST API for NatSecPulse — the signal layer for defense technology, modernization & acquisition. Endpoints expose the same clusters, entity graph, taxonomy, and feed the site renders. All responses are JSON unless noted (CSV / RSS). Exports mirror the live feed: summarized, in-scope clusters within the last 72h.\n\n### Authentication & rate limits\nThe API is open and free — anonymous requests work and are rate-limited per client IP (60 requests/minute). A free **evaluation** API key (either `Authorization: Bearer <key>` or `X-API-Key: <key>`) raises the limit to 600/minute. There is no paid plan. Every `/api/v1/*` response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers; exceeding the limit returns `429` with a `Retry-After` header.",
    "contact": {
      "name": "NatSecPulse",
      "url": "https://natsecpulse.com"
    },
    "license": {
      "name": "See site terms",
      "url": "https://natsecpulse.com/about"
    }
  },
  "servers": [
    {
      "url": "https://natsecpulse.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Export",
      "description": "Versioned, stable export surface (/api/v1)."
    },
    {
      "name": "Feed",
      "description": "The ranked feed and search."
    },
    {
      "name": "Entities",
      "description": "The normalized entity graph."
    },
    {
      "name": "Reference",
      "description": "Taxonomy and quality metrics."
    }
  ],
  "security": [
    {},
    {
      "bearerAuth": []
    },
    {
      "apiKeyAuth": []
    }
  ],
  "paths": {
    "/api/v1/clusters": {
      "get": {
        "tags": [
          "Export"
        ],
        "summary": "List recent clusters (JSON)",
        "description": "Returns recent in-feed story clusters with their summary, tags, topic/program, score, and the underlying source articles.",
        "operationId": "listClusters",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Topic"
          },
          {
            "$ref": "#/components/parameters/Program"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of clusters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClustersResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/clusters.csv": {
      "get": {
        "tags": [
          "Export"
        ],
        "summary": "List recent clusters (CSV)",
        "description": "The same selection as `/api/v1/clusters`, as a downloadable CSV file.",
        "operationId": "listClustersCsv",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Topic"
          },
          {
            "$ref": "#/components/parameters/Program"
          }
        ],
        "responses": {
          "200": {
            "description": "CSV attachment.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/entities": {
      "get": {
        "tags": [
          "Entities"
        ],
        "summary": "List tracked entities",
        "description": "Companies, agencies, programs, and technologies from the entity graph.",
        "operationId": "listEntities",
        "parameters": [
          {
            "$ref": "#/components/parameters/EntityType"
          },
          {
            "$ref": "#/components/parameters/EntityLimit"
          }
        ],
        "responses": {
          "200": {
            "description": "A list of entities.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitiesResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/entity/{type}/{slug}": {
      "get": {
        "tags": [
          "Entities"
        ],
        "summary": "Get one entity with its recent clusters",
        "operationId": "getEntity",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "description": "Entity type.",
            "schema": {
              "type": "string",
              "enum": [
                "company",
                "agency",
                "program",
                "technology"
              ]
            }
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Entity slug (e.g. `lockheed-martin`, `darpa`, `ngad`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The entity and its recent clusters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntityDetail"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/feed": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "The ranked feed (JSON)",
        "description": "The ranked feed as the homepage renders it, with an additive per-cluster explainable-scoring overlay (`signal`). Ordering is `score DESC`.",
        "operationId": "getFeed",
        "responses": {
          "200": {
            "description": "The ranked feed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Full-text search over clusters",
        "operationId": "search",
        "parameters": [
          {
            "$ref": "#/components/parameters/Query"
          },
          {
            "$ref": "#/components/parameters/Topic"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked text matches.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/api/search/semantic": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Semantic (\"Ask NatSecPulse\") search",
        "description": "Natural-language semantic search over recent clusters. Returns `503` when semantic search is not enabled on the deployment (no Vectorize index bound).",
        "operationId": "semanticSearch",
        "parameters": [
          {
            "$ref": "#/components/parameters/Query"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Semantically ranked clusters with citations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "503": {
            "description": "Semantic search not configured on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/entities": {
      "get": {
        "tags": [
          "Entities"
        ],
        "summary": "Active entities with recent clusters (feed payload)",
        "description": "Lightweight payload powering the on-site \"Following\" view.",
        "operationId": "getEntitiesPayload",
        "responses": {
          "200": {
            "description": "Active entities.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/taxonomy": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Standardized taxonomy",
        "description": "Versioned development types, capability areas, and source categories.",
        "operationId": "getTaxonomy",
        "responses": {
          "200": {
            "description": "The taxonomy manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/quality": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Feed quality metrics",
        "description": "Self-audit metrics (precision proxy, coverage, latency, source diversity).",
        "operationId": "getQuality",
        "parameters": [
          {
            "name": "metric",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dimension",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Quality metric series.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/trending": {
      "get": {
        "tags": [
          "Feed"
        ],
        "summary": "Trending movers",
        "description": "Entities/clusters gaining momentum across rolling windows.",
        "operationId": "getTrending",
        "responses": {
          "200": {
            "description": "Top movers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key as `Authorization: Bearer <key>` (optional — raises rate limits)."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key as `X-API-Key: <key>` (optional — raises rate limits)."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Max rows to return (1–100).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Number of rows to skip (for pagination).",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Topic": {
        "name": "topic",
        "in": "query",
        "required": false,
        "description": "Filter to a topic (exact match).",
        "schema": {
          "type": "string"
        }
      },
      "Program": {
        "name": "program",
        "in": "query",
        "required": false,
        "description": "Filter to a program (exact match).",
        "schema": {
          "type": "string"
        }
      },
      "Query": {
        "name": "q",
        "in": "query",
        "required": true,
        "description": "Search text / question.",
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "EntityType": {
        "name": "type",
        "in": "query",
        "required": false,
        "description": "Filter to one entity type.",
        "schema": {
          "type": "string",
          "enum": [
            "company",
            "agency",
            "program",
            "technology"
          ]
        }
      },
      "EntityLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Max entities to return (1–500).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 500,
          "default": 100
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Missing or invalid query parameter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. See the `Retry-After` and `X-RateLimit-*` headers.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          }
        },
        "required": [
          "error"
        ]
      },
      "Article": {
        "type": "object",
        "description": "A source article behind a cluster.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "Publisher name."
          },
          "published_at": {
            "type": "integer",
            "description": "Unix epoch seconds."
          }
        },
        "required": [
          "url",
          "title",
          "source",
          "published_at"
        ]
      },
      "Cluster": {
        "type": "object",
        "description": "A story cluster: related articles summarized into one item.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "headline": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "why_matters": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "topic": {
            "type": "string",
            "nullable": true
          },
          "program": {
            "type": "string",
            "nullable": true
          },
          "score": {
            "type": "number",
            "nullable": true
          },
          "created_at": {
            "type": "integer",
            "description": "Unix epoch seconds."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical story permalink."
          },
          "articles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Article"
            }
          }
        },
        "required": [
          "id",
          "tags",
          "created_at",
          "url",
          "articles"
        ]
      },
      "ClustersResponse": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of clusters in this page."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "clusters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Cluster"
            }
          }
        },
        "required": [
          "count",
          "limit",
          "offset",
          "clusters"
        ]
      },
      "EntityListItem": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "company",
              "agency",
              "program",
              "technology"
            ]
          },
          "canonical_name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "mention_count": {
            "type": "integer"
          },
          "last_seen": {
            "type": "integer",
            "description": "Unix epoch seconds."
          }
        },
        "required": [
          "type",
          "canonical_name",
          "slug",
          "mention_count",
          "last_seen"
        ]
      },
      "EntitiesResponse": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer"
          },
          "type": {
            "type": "string",
            "nullable": true
          },
          "entities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityListItem"
            }
          }
        },
        "required": [
          "count",
          "entities"
        ]
      },
      "EntityDetail": {
        "type": "object",
        "description": "An entity plus its recent clusters.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "company",
              "agency",
              "program",
              "technology"
            ]
          },
          "canonical_name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "mention_count": {
            "type": "integer"
          },
          "clusters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Cluster"
            }
          }
        },
        "required": [
          "type",
          "canonical_name",
          "slug"
        ]
      }
    }
  }
}