{
  "openapi": "3.1.0",
  "info": {
    "title": "heyGRC public API",
    "version": "1.0.0",
    "summary": "Configure which frameworks heyGRC reviews against, as code.",
    "description": "heyGRC is a GitHub App that reviews pull requests against the compliance frameworks your company selected. This API is the configuration-as-code plane: read and replace org config (company profile + framework ids). It does not return review findings. Base URL is https://api.heygrc.com. Customer docs: https://docs.heygrc.com/docs/api-reference. heyGRC makes no certification claim.\n\nVersioning: the public surface is URL-versioned (`/v1/...`). Breaking changes ship as a new path prefix (`/v2`), not by silently changing `/v1`. `/v1` is current. If a version is retired, responses will carry RFC 8594 `Sunset` and `Deprecation` headers, and this document will name the retirement date at least 90 days ahead. There is no other public API version today.",
    "contact": {
      "name": "heyGRC support",
      "email": "support@ismscopilot.com",
      "url": "https://heygrc.com/about"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.heygrc.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "config",
      "description": "Per-org review configuration. Authenticated with a heyGRC API key."
    },
    {
      "name": "catalog",
      "description": "Public framework catalog. No auth."
    }
  ],
  "security": [],
  "paths": {
    "/v1/frameworks": {
      "get": {
        "operationId": "listFrameworks",
        "tags": ["catalog"],
        "summary": "List valid framework ids",
        "description": "Returns the full machine-readable catalog. Count grows over time; do not hardcode ids. Pass the canonical `id` in PUT /v1/config `frameworks`. No authentication.",
        "responses": {
          "200": {
            "description": "Catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrameworkCatalog"
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/v1/config": {
      "get": {
        "operationId": "getOrgConfig",
        "tags": ["config"],
        "summary": "Read org configuration",
        "description": "Returns this key's org configuration. Scope: `config:read`. The org is always derived from the key.",
        "security": [{ "heygrcApiKey": [] }],
        "responses": {
          "200": {
            "description": "Current config",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrgConfig"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      },
      "put": {
        "operationId": "putOrgConfig",
        "tags": ["config"],
        "summary": "Replace org configuration",
        "description": "Full-replace `profile` and `frameworks`. Idempotent: those two fields are the complete desired state (anything you omit from them is cleared). `eu_inference` and `review_language` are optional and sticky; omitting either leaves the current value. Unknown framework ids are rejected, not dropped. Unknown review_language values are 422 invalid_review_language. A 200 is returned only after the change and its audit record are committed together. Scope: `config:write`.",
        "security": [{ "heygrcApiKey": [] }],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string" },
            "description": "Optional. Logged for safe retries."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrgConfigWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated config",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrgConfigWriteResult"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "422": { "$ref": "#/components/responses/Unprocessable" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "heygrcApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "hgrc_",
        "description": "Per-org API key as `Authorization: Bearer hgrc_...`. Keys in the URL are rejected (400). Scopes: `config:read`, `config:write`."
      }
    },
    "schemas": {
      "Framework": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "name", "region"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Canonical id to send in PUT /v1/config `frameworks` (for example ISO_27001, SOC_2, GDPR).",
            "examples": ["ISO_27001"]
          },
          "name": {
            "type": "string",
            "examples": ["ISO/IEC 27001:2022"]
          },
          "region": {
            "type": "string",
            "examples": ["International"]
          }
        }
      },
      "FrameworkCatalog": {
        "type": "object",
        "additionalProperties": false,
        "required": ["frameworks"],
        "properties": {
          "frameworks": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Framework" }
          }
        }
      },
      "Profile": {
        "type": "object",
        "description": "Company context for reviews. JSON object, max 16 KB, max nesting depth 4. Common keys below are optional; extra string/number/boolean fields are allowed.",
        "additionalProperties": {
          "type": ["string", "number", "boolean"]
        },
        "properties": {
          "company": { "type": "string" },
          "product": { "type": "string" },
          "data_handled": { "type": "string" },
          "hosting": { "type": "string" },
          "compliance_posture": { "type": "string" }
        }
      },
      "OrgConfig": {
        "type": "object",
        "required": ["profile", "frameworks", "eu_inference", "review_language"],
        "properties": {
          "profile": { "$ref": "#/components/schemas/Profile" },
          "frameworks": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Canonical framework ids from GET /v1/frameworks."
          },
          "eu_inference": {
            "type": "boolean",
            "description": "Whether review inference for this org is pinned to the EU. Sticky on PUT when omitted."
          },
          "review_language": {
            "type": "string",
            "enum": ["en", "de", "es", "fr", "it", "nl", "pl"],
            "description": "Language of review comments. Control IDs, the heygrc check name, and the console stay English. Sticky on PUT when omitted."
          }
        }
      },
      "OrgConfigWrite": {
        "type": "object",
        "required": ["profile", "frameworks"],
        "properties": {
          "profile": { "$ref": "#/components/schemas/Profile" },
          "frameworks": {
            "type": "array",
            "items": { "type": "string" }
          },
          "eu_inference": {
            "type": "boolean",
            "description": "Optional. Omitted = keep current value. Non-booleans are rejected."
          },
          "review_language": {
            "type": "string",
            "enum": ["en", "de", "es", "fr", "it", "nl", "pl"],
            "description": "Optional. Omitted = keep current value. Unknown values are 422 invalid_review_language."
          }
        }
      },
      "OrgConfigWriteResult": {
        "type": "object",
        "required": ["ok", "profile", "frameworks"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "profile": { "$ref": "#/components/schemas/Profile" },
          "frameworks": {
            "type": "array",
            "items": { "type": "string" }
          },
          "eu_inference": { "type": "boolean" },
          "review_language": {
            "type": "string",
            "enum": ["en", "de", "es", "fr", "it", "nl", "pl"]
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "unauthorized",
                  "forbidden",
                  "invalid_profile",
                  "unknown_frameworks",
                  "invalid_review_language",
                  "rate_limited",
                  "method_not_allowed",
                  "internal"
                ]
              },
              "message": { "type": "string" },
              "unknown": {
                "type": "array",
                "items": { "type": "string" },
                "description": "Present on unknown_frameworks: the rejected ids."
              }
            }
          }
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Non-JSON body, wrong body shape, or a key was passed in the URL.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, invalid, expired, or revoked key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "Forbidden": {
        "description": "The key lacks the required scope.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "Unprocessable": {
        "description": "invalid_profile, unknown_frameworks, or invalid_review_language.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Stay under about 1 request/second.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "HTTP method not supported on this endpoint.",
        "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" }
          }
        }
      }
    }
  }
}
