Risk & Control Matrix Guide#
This guide walks through reading a Risk & Control Matrix (RCM) with the GRC API. Before you start you need:
A valid OAuth 2.0 Bearer token (see Authentication).
The
X-Version: 2026-09-01.grc.previewandWk-Workspaceheaders on every request (see Request headers).The
grc:readscope.
Resources#
This preview exposes two RCM resources, each with list, get by id, and batchRetrieval
operations:
Resource |
Path |
Description |
|---|---|---|
Controls |
|
Safeguards that mitigate risks. |
Risks |
|
Potential events controls mitigate. |
Full resources and digests#
An endpoint returns the full representation of its own resource. Related resources are
embedded as digests — an id plus a few display fields (for example, a control’s
risks are returned as risk digests). Use the control or risk endpoint with a digest id to
retrieve that resource’s full representation. Processes, subprocesses, systems, and locations
are included as context digests but do not have their own endpoints in this preview.
This keeps payloads bounded and avoids cycles (a control embeds its risks; each risk embeds its mitigating controls). See Digests for the general pattern.
List controls#
A workspace is in either standalone mode or multi-instance mode. Unfiltered
GET /controls results contain standalone controls in standalone mode, or definition and
instance controls in multi-instance mode. Use $filter=controlType to request a subset of the
types available in that workspace mode.
curl -X GET "https://api.app.wdesk.com/controls?\$maxpagesize=100" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}"
See Control types and response shapes below for the three control shapes and for paging instances of a definition.
Get a single control#
curl -X GET "https://api.app.wdesk.com/controls/{id}" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}"
A control includes its controlType (standalone, definition, or instance),
significance, frequency, automatedManual, preventiveDetective, its process and
subProcess digests, its risks, systems, and locations digests, and any
customFields configured for controls in the workspace.
See Control types and response shapes below for what each controlType returns.
Control types and response shapes#
Every control has a controlType of standalone, definition, or instance, and that type
determines which type-specific sub-object the response carries. All three share the same
top-level fields (id, controlId, name, significance, frequency, owner, process,
risks, systems, locations, customFields, and the rest); they differ only in
whether they carry a definition or an instance sub-object.
Standalone#
A classic, independent control that is not part of the definition/instance model. It carries
neither sub-object, and its systems and locations may each list several dimensions.
{
"id": "d9c09501-214c-432e-85c9-8cd1009b9660",
"controlId": "BEN.001",
"controlType": "standalone",
"name": "General Ledger Reconciliation",
"owner": { "id": "u_4cc9f9128a684f45a7a37b08b70b54c1" },
"systems": [{ "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "name": "Workday" }],
"locations": [{ "id": "7f6e5d4c-3b2a-4c1d-9e8f-0a1b2c3d4e5f", "name": "North America" }]
}
Definition#
A template that spawns instances across location and system dimensions. It carries a
definition object with instanceCount (how many instances exist). A template has no dimension
of its own, so its systems and locations are empty. Definitions appear in unfiltered lists
only in multi-instance-mode workspaces.
{
"id": "4a3b2c1d-0e9f-4a8b-7c6d-5e4f3a2b1c0d",
"controlId": "BEN.002",
"controlType": "definition",
"name": "AP Invoice Approval",
"systems": [],
"locations": [],
"definition": { "instanceCount": 3 }
}
Instance#
A concrete control derived from a definition for a specific location or system. It carries an
instance object with definition (a digest of the parent it came from) and overriddenFields
(the fields whose values differ from that parent, listed as a public standard field name or a
stable custom-field schema id; empty when the instance is fully aligned with its definition).
Its systems and locations hold the single dimension it applies to.
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"controlId": "BEN.002",
"controlType": "instance",
"name": "AP Invoice Approval",
"frequency": "quarterly",
"systems": [{ "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "name": "Workday" }],
"locations": [{ "id": "7f6e5d4c-3b2a-4c1d-9e8f-0a1b2c3d4e5f", "name": "North America" }],
"instance": {
"definition": {
"id": "4a3b2c1d-0e9f-4a8b-7c6d-5e4f3a2b1c0d",
"controlId": "BEN.002",
"name": "AP Invoice Approval"
},
"overriddenFields": ["frequency", "8f3c1a2b-4d5e-4f6a-8b7c-9d0e1f2a3b4c"]
}
}
Here the instance runs quarterly while its definition runs monthly, so frequency appears
in overriddenFields, and the custom field with schema id 8f3c1a2b-4d5e-4f6a-8b7c-9d0e1f2a3b4c
has likewise been overridden on this instance.
Working with definitions in lists#
controlType filters request a subset of the types available in the workspace mode. In a
multi-instance-mode workspace, unfiltered results already include definitions and instances:
$filter=controlType eq 'definition'
$filter=controlType in ('definition', 'instance')
To page through the instances derived from one definition, filter by that definition’s
controlId (its human-readable id, not its UUID):
$filter=definitionId eq 'BEN.002'
Custom fields#
Controls and risks carry workspace-defined customFields. Each entry has a stable schema id
(names can change), a name, a type, and a single value field determined by the type:
|
Value field |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"customFields": [
{ "id": "8f3c1a2b-4d5e-4f6a-8b7c-9d0e1f2a3b4c", "name": "Risk Rating", "type": "singleSelectEnum", "enumValue": "High" },
{ "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Frameworks", "type": "multiSelectEnum", "enumValues": ["SOX", "COSO"] },
{ "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e", "name": "Last Reviewed", "type": "date", "dateValue": "2026-01-15" },
{ "id": "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a", "name": "Legacy Date", "type": "invalid", "invalidValue": "not-a-date" }
]
Note
type is an open string — new custom-field types may be introduced over time, so tolerate
values beyond those listed here.
Note
A 404 means the requested control was not found.
Fetch a known set of ids (batch)#
When you already hold a set of ids, use batchRetrieval to retrieve up to 250 in one request. The
response data array preserves the order of the requested ids. Batch get is all-or-nothing: if
any id is missing or not accessible, the whole request fails with 400 and a per-id error list
(in the error details), rather than returning a partial page.
curl -X POST "https://api.app.wdesk.com/controls/batchRetrieval" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}" \
-d '{ "data": ["d9c09501-214c-432e-85c9-8cd1009b9660", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"] }'
Reconstructing the matrix#
The heart of the RCM is the control↔risk linkage. Each control exposes risks
(risk digests) and each risk exposes mitigatingControls (control digests). Walk either
side and follow the digest ids to build the full matrix, grouping by process / subProcess
and tagging by systems / locations as needed. To find controls for a specific system or
location, filter GET /controls with systems.id or locations.id.