Assurance Projects & Scoping Guide#
This guide walks through reading assurance projects and scoping their controls 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 for reads, and thegrc:writescope for setting scope.
Important
Projects are read-only through this API — you cannot create, rename, or delete a project.
Setting the scoping rationale and changing scope are supported only for Independent Controls
Testing projects (type = independentControlsTesting). A scope write against any other
project type returns 409 with code unsupportedProjectType.
Note
Scope reasoning is captured in two independent places: the project’s overall
scopingRationale (one narrative per project, set with PATCH /assuranceProjects/{id}) and a
control’s scopeRationale (the reason a control was removed or restored, required on every
removal and restoration). See
Set the scoping rationale and attached files
and Remove controls from scope for each in full.
Resources#
Resource |
Path |
Description |
|---|---|---|
Assurance projects (read + PATCH) |
|
List and read projects; |
Scoped controls (read) |
|
A project’s scope entries, in scope and removed, each with its latest rationale. |
Add to scope (write) |
|
Put controls in scope by control id. |
Remove from scope (write) |
|
Take scope entries out of scope, with a rationale. |
Restore to scope (write) |
|
Restore removed scope entries, with a rationale. |
List projects#
GET /assuranceProjects returns a page of projects of every type. Filter by status, year,
or name.
curl -X GET "https://api.app.wdesk.com/assuranceProjects?\$maxpagesize=100" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}"
A listed project returns the fields common to every project type — id, type, name,
status, year, startDate, endDate, budgetedHours, managers, and contributors — so
the shape is stable as more project types are added. The free-text objective and
scopingRationale are returned when you fetch a single project by id (below), not in the list.
Note
type and status are open strings. New project types and statuses may be introduced over
time, so tolerate values beyond those documented here.
Get a single project#
curl -X GET "https://api.app.wdesk.com/assuranceProjects/{id}" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}"
Working with an existing project#
This API does not create assurance projects — a project is created in Workiva by a user with
the appropriate role. Use GET /assuranceProjects to find the project you want to scope and
note its id; every operation below works against that id.
Read a project’s scope#
GET /assuranceProjects/{id}/scopedControls lists a project’s scope entries. Each entry has its
own id (the scope-entry id you pass to the removal and restoration operations), a
controlSnapshot, a scopeStatus, and an optional scopeRationale.
The controlSnapshot is the control as it was when it was put in scope:
idis the snapshot’s own id. No endpoint returns a snapshot by this id.evergreenControlIdis the id of the control, as returned by the Controls API. Use it to fetch the control. It isnullwhen the control was permanently deleted.controlIdandnameare the control’s identifier and name when it was put in scope.
The entry is still returned when its control is deleted, and can be removed or restored by its
id. The scopeRationale is the rationale from the entry’s most recent removal or restoration;
it is null if the entry has never been removed, and is independent of the project-level
scopingRationale.
Both in-scope and removed entries are returned by default. Narrow the list by status, or to
specific entries by id, with a filter:
$filter=scopeStatus eq 'inScope'
$filter=scopeStatus eq 'removedFromScope'
$filter=id in ('019cb0a1-2f3e-7c22-9a10-4d5e6f7a8b90', '019cb0a1-2f3e-7c22-9a10-4d5e6f7a8b92')
Only id (eq, in) and scopeStatus (eq) can be filtered, and only scopeStatus can be
ordered. Anything else returns 400 with code invalidFilter or invalidOrderBy and the
parameter as target.
curl -X GET "https://api.app.wdesk.com/assuranceProjects/{id}/scopedControls" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}"
Set the scoping rationale and attached files#
PATCH /assuranceProjects/{id} updates a project’s scope documentation: its project-level
scopingRationale (the whole-engagement narrative of how the overall scope was determined) and
the set of Workiva files attached as supporting evidence. The body is a JSON Patch document
(RFC 6902, media type application/json-patch+json): an array of operations. The supported
operations are replace on /scopingRationale and replace on /scopingFiles; include an
operation only for the field you want to change, and omit it to leave that field unchanged.
/scopingFiles is replace-set: the array you supply becomes the complete attached set,
referenced by each file’s Workiva id. A currently-attached file you leave out is detached, and
[] detaches all. Read the current set back from the project’s scopingFiles (whose id is the
attachment id, not the file id). Files must be a document, spreadsheet, or presentation.
curl -X PATCH "https://api.app.wdesk.com/assuranceProjects/{id}" \
-H "Content-Type: application/json-patch+json" \
-H "Accept: application/json" \
-H "Authorization: Bearer {access-token}" \
-H "X-Version: 2026-09-01.grc.preview" \
-H "Wk-Workspace: {workspace-id}" \
-d '[
{ "op": "replace", "path": "/scopingRationale", "value": "FY26 scope limited to key controls over financial reporting per materiality analysis." },
{ "op": "replace", "path": "/scopingFiles", "value": [ { "id": "019f2a3b-4c5d-7e6f-8a9b-0c1d2e3f4a5b" } ] }
]'
Add controls to scope#
POST /assuranceProjects/{id}/scopedControls/addition adds up to 250 controls to scope, identified by
their control id (as returned by the Controls API). It takes no rationale; the overall reason
for the scope lives in the project’s scopingRationale (set with PATCH above). The response
returns only the scope entries this call created, in request order, each with its assigned
ScopedControl.id. Each returned entry’s controlSnapshot.evergreenControlId is the requested
control id. A control that is already in the project, in scope or removed, is left as it
is and is not returned; use the restoration operation to restore one that was removed. An id
that doesn’t match a current control in the workspace, including a deleted one, is skipped. Read
the scoped-controls list to check the state of controls that weren’t returned. Retrying the same
request is safe: it creates nothing further.
curl -X POST "https://api.app.wdesk.com/assuranceProjects/{id}/scopedControls/addition" \
-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": ["019cafc7-4901-7181-a53b-b2263115961d", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"] }'
Remove controls from scope#
POST /assuranceProjects/{id}/scopedControls/removal removes up to 250 controls from scope,
identified by scope-entry id (the id on each entry from the scoped-controls list, not the
control’s own id). Removal is a soft change: removed controls stay visible in the
scoped-controls list with a removedFromScope status. A rationale is required and is
recorded against every entry in the request as its scopeRationale, replacing any earlier one,
and separate from the project’s overall scopingRationale.
Removal is idempotent: an entry that is already removed stays removed and takes the new rationale, so a retried request leaves the same state and returns the same response. An id that doesn’t match a scope entry in the project is skipped. The response returns the matched entries in request order, in their resulting state.
curl -X POST "https://api.app.wdesk.com/assuranceProjects/{id}/scopedControls/removal" \
-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": ["019cb0a1-2f3e-7c22-9a10-4d5e6f7a8b90"],
"rationale": "Below materiality threshold for FY26."
}'
Restore controls to scope#
POST /assuranceProjects/{id}/scopedControls/restoration restores up to 250 previously removed
controls to scope, identified by scope-entry id. A rationale is required and recorded as
each entry’s scopeRationale, explaining the restoration and replacing any earlier one.
Restoration is idempotent: an entry that is already in scope stays in scope and takes the new
rationale. An id that doesn’t match a scope entry in the project is skipped; add a never-scoped
control with the addition operation instead. The response returns the matched entries in
request order, in their resulting state.
curl -X POST "https://api.app.wdesk.com/assuranceProjects/{id}/scopedControls/restoration" \
-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": ["019cb0a1-2f3e-7c22-9a10-4d5e6f7a8b90"],
"rationale": "Business retained; control back in scope for FY26."
}'
Scoping a project end to end#
Find the project —
GET /assuranceProjectsand note theidof the project to scope.Set the scope documentation —
PATCH /assuranceProjects/{id}with thescopingRationaleand any supportingscopingFiles.Set the initial scope — pull control ids from the Controls API, then
POST …/scopedControls/additionwith those ids.Adjust as decisions change — take an entry’s
idfrom the scoped-controls list andPOST …/scopedControls/removalit with arationalefor why it came out;POST …/scopedControls/restorationthe scope-entry id later with arationaleif the decision reverses.
Note
The GRC API stores the scope; your integration decides what is in scope. The project’s
scopingRationale is the overall narrative (set with PATCH); a control’s scopeRationale is
the reason it was removed or restored (required on every removal and restoration).