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.preview and Wk-Workspace headers on every request (see Request headers).

  • The grc:read scope for reads, and the grc:write scope 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)

/assuranceProjects, /assuranceProjects/{id}

List and read projects; PATCH a project’s scoping rationale and attached files.

Scoped controls (read)

/assuranceProjects/{id}/scopedControls

A project’s scope entries, in scope and removed, each with its latest rationale.

Add to scope (write)

/assuranceProjects/{id}/scopedControls/addition

Put controls in scope by control id.

Remove from scope (write)

/assuranceProjects/{id}/scopedControls/removal

Take scope entries out of scope, with a rationale.

Restore to scope (write)

/assuranceProjects/{id}/scopedControls/restoration

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:

  • id is the snapshot’s own id. No endpoint returns a snapshot by this id.

  • evergreenControlId is the id of the control, as returned by the Controls API. Use it to fetch the control. It is null when the control was permanently deleted.

  • controlId and name are 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#

  1. Find the project — GET /assuranceProjects and note the id of the project to scope.

  2. Set the scope documentation — PATCH /assuranceProjects/{id} with the scopingRationale and any supporting scopingFiles.

  3. Set the initial scope — pull control ids from the Controls API, then POST …/scopedControls/addition with those ids.

  4. Adjust as decisions change — take an entry’s id from the scoped-controls list and POST …/scopedControls/removal it with a rationale for why it came out; POST …/scopedControls/restoration the scope-entry id later with a rationale if 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).