{
  "openapi": "3.1.0",
  "info": {
    "title": "Security Startups public API",
    "version": "1.0.0",
    "summary": "Search and dump the live catalog of young security companies.",
    "description": "Public JSON for securitystartups.com. Search filters the live catalog. The dump returns every live listing. No authentication. Checkout, webhooks, and submission endpoints are not part of this document. Listing order is not a ranking and not paid placement. Stable REST uses a major version in the path, starting at `/api/v1/`. Unversioned `/api/search` and `/api/startups.json` are aliases of the v1 handlers and stay. Backward-compatible fields may be added inside v1. A breaking change requires `/api/v2/`. If a version is retired, responses will carry RFC 8594 `Sunset` and RFC 9745 `Deprecation` headers, and this document will name the retirement date at least 90 days ahead. Unknown `/api/*` paths return HTTP 404 application/json `{error:{code,message,docs}}`. GET `/api/search` and GET `/api/v1/search` return HTTP 400 in the same shape when `limit` is present and is not an integer in 1..100. Catalog GET responses send RateLimit, RateLimit-Policy, and X-RateLimit-* headers. A 429, if issued, includes Retry-After.",
    "contact": {
      "name": "Security Startups",
      "url": "https://securitystartups.com/for-agents"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://securitystartups.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "catalog",
      "description": "Live directory listings. No auth."
    }
  ],
  "security": [],
  "paths": {
    "/api/startups.json": {
      "get": {
        "operationId": "listStartups",
        "tags": ["catalog"],
        "summary": "Dump the live catalog",
        "description": "Returns every live listing. Envelope field is `startups`. Also includes `categories`, `categoryCount`, response-date `lastUpdated`, and catalog freshness `dataAsOf`. No authentication. Revalidated about every 60 seconds.",
        "responses": {
          "200": {
            "description": "Catalog dump",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartupsDump"
                }
              }
            },
            "headers": {
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "searchStartups",
        "tags": ["catalog"],
        "summary": "Search live listings",
        "description": "Filter the live catalog. Dimensions AND together. Repeated `category` and `funding_stage` OR within that dimension (any value may match). `q` is a case-insensitive substring over name, summary, description, category slug, category name, HQ, tags, and investors. `limit` defaults to 50. If `limit` is present and is not an integer from 1 to 100, the response is HTTP 400. Response `count` is the page size returned; `total` is matches before limit; `limit` is the applied cap.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "string" } },
            "description": "Category slugs. Repeat or comma-separate. OR within the dimension."
          },
          {
            "name": "funding_stage",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "string" } },
            "description": "Funding stages. OR within the dimension."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Case-insensitive substring over name, summary, description, category, HQ, tags, investors."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 },
            "description": "Page size. Defaults to 50. If present, must be an integer from 1 to 100; otherwise HTTP 400."
          }
        ],
        "responses": {
          "200": {
            "description": "Search page",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/StartupsSearch" }
              }
            },
            "headers": {
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "searchStartupsV1",
        "tags": ["catalog"],
        "summary": "Search live listings (v1)",
        "description": "Versioned alias of GET /api/search. Same query params, envelope, and errors. New integrations should call this path. Filter the live catalog. Dimensions AND together. Repeated `category` and `funding_stage` OR within that dimension. `q` is a case-insensitive substring. `limit` defaults to 50. If `limit` is present and is not an integer from 1 to 100, the response is HTTP 400. Response `count` is the page size returned; `total` is matches before limit; `limit` is the applied cap.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "string" } },
            "description": "Category slugs. Repeat or comma-separate. OR within the dimension."
          },
          {
            "name": "funding_stage",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "string" } },
            "description": "Funding stages. OR within the dimension."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Case-insensitive substring over name, summary, description, category, HQ, tags, investors."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 },
            "description": "Page size. Defaults to 50. If present, must be an integer from 1 to 100; otherwise HTTP 400."
          }
        ],
        "responses": {
          "200": {
            "description": "Search page",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/StartupsSearch" }
              }
            },
            "headers": {
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/v1/startups.json": {
      "get": {
        "operationId": "listStartupsV1",
        "tags": ["catalog"],
        "summary": "Dump the live catalog (v1)",
        "description": "Versioned alias of GET /api/startups.json. Same envelope and errors. New integrations should call this path. Returns every live listing. Envelope field is `startups`.",
        "responses": {
          "200": {
            "description": "Catalog dump",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/StartupsDump" }
              }
            },
            "headers": {
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "StartupsDump": {
        "type": "object",
        "additionalProperties": false,
        "required": ["directory", "url", "lastUpdated", "dataAsOf", "count", "categoryCount", "categories", "startups"],
        "properties": {
          "directory": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "lastUpdated": { "type": "string" },
          "dataAsOf": { "type": "string" },
          "count": { "type": "integer" },
          "categoryCount": { "type": "integer" },
          "categories": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "startups": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PublicStartup" }
          }
        }
      },
      "StartupsSearch": {
        "type": "object",
        "additionalProperties": false,
        "required": ["count", "total", "limit", "normalized", "results"],
        "properties": {
          "count": { "type": "integer", "description": "Length of `results` (page size)." },
          "total": { "type": "integer", "description": "Matches before limit." },
          "limit": { "type": "integer" },
          "normalized": { "type": "object", "additionalProperties": true },
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/StartupSearchHit" }
          }
        }
      },
      "StartupSearchHit": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "slug", "brief_summary", "category", "hq", "country", "founded", "funding_stage", "total_funding", "investors", "signal", "url", "details"],
        "properties": {
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "brief_summary": { "type": "string" },
          "category": { "type": "string" },
          "hq": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"] },
          "founded": { "type": ["string", "null"] },
          "funding_stage": { "type": ["string", "null"] },
          "total_funding": { "type": ["string", "null"] },
          "investors": { "type": "array", "items": { "type": "string" } },
          "signal": { "type": ["string", "null"] },
          "url": { "type": "string" },
          "details": { "type": "string", "format": "uri" }
        }
      },
      "PublicStartup": {
        "type": "object",
        "additionalProperties": true,
        "required": ["id", "name", "slug", "brief_summary", "url"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "brief_summary": { "type": "string" },
          "url": { "type": "string" }
        }
      },
      "ApiError": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": ["code", "message", "docs"],
            "properties": {
              "code": {
                "type": "string",
                "enum": ["not_found", "invalid_input", "method_not_allowed", "rate_limited", "internal"]
              },
              "message": { "type": "string" },
              "docs": {
                "type": "string",
                "format": "uri",
                "description": "Developer resources. https://securitystartups.com/for-agents"
              }
            }
          }
        }
      }
    },
    "headers": {
      "RateLimit": {
        "description": "RFC RateLimit remaining. Example: \"catalog\";r=120;t=60",
        "schema": { "type": "string" }
      },
      "RateLimitPolicy": {
        "description": "RFC RateLimit-Policy. Example: \"catalog\";q=120;w=60",
        "schema": { "type": "string" }
      },
      "XRateLimitLimit": {
        "description": "Advertised catalog quota per window.",
        "schema": { "type": "integer" }
      },
      "XRateLimitRemaining": {
        "description": "Advertised remaining requests in the current window.",
        "schema": { "type": "integer" }
      },
      "XRateLimitReset": {
        "description": "Unix seconds when the advertised window resets.",
        "schema": { "type": "integer" }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying after HTTP 429.",
        "schema": { "type": "integer" }
      },
      "Allow": {
        "description": "Allowed methods on 405.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid query. GET /api/search returns this when limit is present and is not an integer in 1..100.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "NotFound": {
        "description": "Unknown /api path. Catch-all JSON 404; catalog routes themselves are not 404 for empty results.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Catalog dump and search only accept GET, HEAD, and OPTIONS.",
        "headers": {
          "Allow": { "$ref": "#/components/headers/Allow" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Stay under the advertised catalog quota of 120 per 60 seconds.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "RateLimit": { "$ref": "#/components/headers/RateLimit" },
          "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "Internal": {
        "description": "Unexpected server error; safe to retry with backoff.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      }
    }
  }
}
