heyGRC Docs

API reference

heyGRC public API: endpoints for frameworks and review configuration, with request and response shapes.

Base URL: https://api.heygrc.com

All requests authenticate with a per-org API key as a Bearer token:

Authorization: Bearer hgrc_...

A key belongs to one heyGRC org (one per GitHub App installation) and carries scopes (config:read, config:write). Send the key in the header only - keys passed in the URL are rejected (400), to avoid leaking them into logs and referrers. The org a request acts on is always derived from the key, so a key can only ever read or write its own org's config.


GET /v1/config

Returns your org's current configuration. Scope: config:read.

200

{
  "profile": { "company": "Acme Inc", "...": "..." },
  "frameworks": ["ISO_27001", "SOC_2", "GDPR"],
  "eu_inference": false,
  "review_language": "en",
  "commitments": [
    { "area": "logging", "implementation": "never log email or full IP", "source": "Logging Policy 4.2" }
  ],
  "commitments_updated_at": "2026-08-26T22:00:00Z"
}

PUT /v1/config

Send what you change. Every field is optional: an omitted field keeps its current value, a present field is replaced. This makes agent workflows safe: PUT just the field you want to change, no read-modify-write of the whole config. Unknown top-level fields are rejected with 400, so a typo never silently no-ops. Scope: config:write.

Headers: Content-Type: application/json; optional Idempotency-Key: <string> (logged for safe retries).

Body

FieldTypeNotes
profileobject (optional)Free-form company context. Must be a JSON object, ≤ 16 KB, max nesting depth 4, values are strings / numbers / booleans / arrays of those. Present = replaced; omitted = unchanged.
frameworksstring[] (optional)The framework ids you must comply with. Every id must be in the catalog - unknown ids are rejected, not silently dropped. Present = replaced; omitted = unchanged.
commitmentsarray (optional)Your implementation commitments: up to 5 rows, one per area (logging, access_mfa, encryption, pii_retention, subprocessors); each row is { "area", "implementation" (1-300 chars, single line), "source"? (1-120 chars, single line) }. Present = replaced (send [] to clear the map); omitted = unchanged; null is rejected.
eu_inferenceboolean (optional)Sticky: omitted = current value kept; boolean = set. Non-booleans rejected. Customer how-to: EU inference.
review_languagestring (optional)Sticky: omitted = current value kept. Allowlist: en, de, es, fr, it, nl, pl. Unknown values are 422 invalid_review_language. Customer how-to: Review language.

200

{ "ok": true, "commitments": [ { "area": "logging", "implementation": "never log email or full IP" } ] }

Only the fields you sent are echoed back. A 200 is only returned after the change and its audit record are committed together, so a successful write is always auditable.

Errors

Stable JSON shape on every error:

{ "error": { "code": "unknown_frameworks", "message": "unknown framework ids: FOO", "unknown": ["FOO"] } }
StatuscodeMeaning
400invalid_requestNon-JSON body, wrong body shape, or a key was passed in the URL.
401unauthorizedMissing, malformed, invalid, expired, or revoked key.
403forbiddenThe key lacks the required scope (config:read / config:write).
422invalid_profileprofile isn't an object, is too large, or is too deeply nested.
422unknown_frameworksOne or more framework ids aren't in the catalog (see unknown).
422invalid_review_languagereview_language is not in the allowlist.
400invalid_requestUnknown top-level field(s), or a body with none of the known fields (empty update).
422invalid_commitmentscommitments isn't an array of valid rows (unknown or duplicate area, over-length text, control characters). null is a 400: use [] to clear.
429rate_limitedToo many requests; slow down.
405method_not_allowedHTTP method not supported on this endpoint.
500internalUnexpected server error; safe to retry with backoff.

Rate limits

Per-key and per-IP request caps protect the service. Stay under ~1 request/second and you'll never see a 429. Config is a control plane - you set it occasionally, not in a hot loop.


GET /v1/frameworks

The full, machine-readable catalog (no auth - point your agent here to discover valid ids):

{ "frameworks": [ { "id": "ISO_27001", "name": "ISO 27001:2022", "region": "International" },  ] }

Framework catalog

heyGRC's framework catalog is returned by GET /v1/frameworks (count grows over time; do not hardcode). Pass the canonical id in frameworks. The most common ids:

ISO_27001, ISO_42001, ISO_27701, ISO_22301, ISO_9001, SOC_2, GDPR, UK_GDPR, HIPAA, CCPA, EU_AI_ACT, NIST_CSF, NIST_800_53, NIST_800_171, NIST_AI_RMF, DORA, TISAX, CMMC, FEDRAMP, CYFUN, AU_ESSENTIAL_EIGHT, and NIS 2 as NIS_2 plus per-country variants (NIS_2_DE, NIS_2_FR, NIS_2_IT, …).

A PUT with an id outside the catalog is rejected with 422 unknown_frameworks and the offending ids echoed back, so your agent can self-correct.

On this page