{
  "openapi": "3.1.0",
  "info": {
    "title": "GRC Platforms public API",
    "version": "1.0.0",
    "summary": "Search and dump the live catalog of GRC software platforms.",
    "description": "Public JSON for grcplatforms.com. Search filters the live catalog. The dump returns every live listing plus categories. 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/platforms.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. Wrong methods return HTTP 405 with Allow: GET, HEAD, OPTIONS. Catalog GET responses send RateLimit, RateLimit-Policy, and X-RateLimit-* headers. A 429, if issued, includes Retry-After.",
    "contact": {
      "name": "GRC Platforms",
      "url": "https://grcplatforms.com/about"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://grcplatforms.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "catalog",
      "description": "Live directory listings. No auth."
    }
  ],
  "security": [],
  "paths": {
    "/api/platforms.json": {
      "get": {
        "operationId": "listPlatforms",
        "tags": [
          "catalog"
        ],
        "summary": "Dump the live catalog",
        "description": "Returns every live listing. Envelope field is `platforms`. 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/PlatformsDump"
                }
              }
            },
            "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "searchPlatforms",
        "tags": [
          "catalog"
        ],
        "summary": "Search live listings",
        "description": "Filter the live catalog. Dimensions AND together. Repeated `category` and `size` OR within that dimension (any value may match). `deployment` is single-valued exact match. `q` is a case-insensitive substring over name, summary, description, category slug, category name, hq, frameworks, and tags. `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": "size",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Company sizes. OR within the dimension."
          },
          {
            "name": "deployment",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Single-valued exact match on deployment."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring over name, summary, description, category, category name, hq, frameworks, tags."
          },
          {
            "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/PlatformsSearch"
                }
              }
            },
            "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "searchPlatformsV1",
        "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 `size` OR within that dimension (any value may match). `deployment` is single-valued exact match. `q` is a case-insensitive substring over name, summary, description, category slug, category name, hq, frameworks, and tags. `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": "size",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Company sizes. OR within the dimension."
          },
          {
            "name": "deployment",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Single-valued exact match on deployment."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring over name, summary, description, category, category name, hq, frameworks, tags."
          },
          {
            "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/PlatformsSearch"
                }
              }
            },
            "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/v1/platforms.json": {
      "get": {
        "operationId": "listPlatformsV1",
        "tags": [
          "catalog"
        ],
        "summary": "Dump the live catalog (v1)",
        "description": "Versioned alias of GET /api/platforms.json. Same envelope. New integrations should call this path. Returns every live listing. Envelope field is `platforms`. 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/PlatformsDump"
                }
              }
            },
            "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PlatformsDump": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "directory",
          "url",
          "lastUpdated",
          "dataAsOf",
          "count",
          "categoryCount",
          "categories",
          "platforms"
        ],
        "properties": {
          "directory": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "lastUpdated": {
            "type": "string",
            "description": "Response date YYYY-MM-DD."
          },
          "dataAsOf": {
            "type": "string",
            "description": "Latest catalog date_added YYYY-MM-DD."
          },
          "count": {
            "type": "integer"
          },
          "categoryCount": {
            "type": "integer"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "platforms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicPlatform"
            }
          }
        }
      },
      "PlatformsSearch": {
        "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/PlatformSearchHit"
            }
          }
        }
      },
      "PlatformSearchHit": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "slug",
          "brief_summary",
          "category",
          "hq",
          "company_size",
          "deployment",
          "frameworks",
          "url",
          "details"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "brief_summary": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "hq": {
            "type": "string"
          },
          "company_size": {
            "type": "string"
          },
          "deployment": {
            "type": "string"
          },
          "frameworks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "url": {
            "type": "string"
          },
          "details": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "PublicPlatform": {
        "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://grcplatforms.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"
        }
      }
    },
    "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"
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Wrong HTTP method. Catalog GET-only endpoints accept GET, HEAD, OPTIONS.",
        "headers": {
          "Allow": {
            "description": "GET, HEAD, OPTIONS",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      }
    }
  }
}
