Start an asynchronous export of issues#

Description#

Starts a long-running operation that exports remediation issues to a CSV or JSONL file, following the Workiva Long Running Operations standard. The response is 202 with a Location header pointing at the gateway’s shared GET /operations/{operationId} endpoint; poll it until completed, then download the file from its resourceUrl. Requires the grc:read scope and the same permission as the in-app issue export; the operation can only be read by the caller that started it, and the permission is checked again on every poll.

Request. filter takes the filter grammar of GET /issues. updatedAt bounds the export to issues created or changed in a window, half-open [from, to) in UTC, applied together with filter. The default window is 365 days (P365D) and the maximum is 366 days (P366D); a longer window returns 400. With neither bound the last 365 days apply; with only from, to is the earlier of now and 365 days after from; with only to, from is 365 days before to. The effective upper bound is the earlier of to and 30 seconds before now, so recent writes are not missed; a from in the future, not before the effective to, or (with no to) within the last 30 seconds returns 400. The effective window is computed once, when the request is received, and persisted on the job; the 202 body echoes it. Sync clients should start the next window at the echoed updatedAt.to, overlap windows by about one minute, and deduplicate by id and modified.dateTime (updatedAt in csv). Any past window may be exported, so backfill works in chunks of up to a year. Deleted issues do not appear in any window. Incremental sync should export without a filter, because an issue that stops matching the filter never reappears in a later window. Deleting a linked control, process or risk does not yet change an issue’s updatedAt, so controlIds, processIds and riskIds in an export can be out of date until the issue itself is next changed.

Formats. UTF-8 without a BOM, ISO-8601 UTC dates, never localized. Action tasks and survey answers are not included; use GET /issues/{id}/actionTasks.

  • jsonl (recommended for sync and import): one JSON object per line, each exactly the public Issue object returned by GET /issues/{id}, created and modified and custom fields included, with no escaping.

  • csv: a header row and one row per issue. Fixed columns, in this order: id, name, description, rootCause, supplementalInformation, type, status, severity, soxDeficiency, isOverdue, reportedDate, targetRemediationDate, resolutionType, resolutionNote, resolutionDate, auditTeamOwnerId, businessOwnerId, collaboratorIds, createdById, resolvedById, controlIds, processIds, riskIds, createdAt, updatedAt (CSV column names; jsonl uses created and modified). Then one column per non-archived custom field, in schema order, headed by the field’s display name. Enums are lowerCamelCase strings, users and linked controls, processes and risks appear as ids only, and multi-valued cells (id lists and multi-select custom field values) are ;-joined. Text cells starting with =, +, -, @, tab or carriage return are prefixed with ' to neutralize spreadsheet formulas (OWASP CSV injection list). Because that prefix cannot be reversed, CSV values may carry an escape prefix, and the custom field columns vary by workspace and can be renamed: sync clients should use jsonl. CSV columns are append-only.

Consistency. The issues are selected when the job starts, and each row reflects its issue when it is written, not when it was selected. An issue edited in between appears as of its later state, with that state’s modified (updatedAt in csv), and the edit also falls in your next window; an issue deleted in between is left out.

Limits. More than 50,000 matching rows returns 400 when the request is received if the count is already above the cap, and fails the operation otherwise (narrow the filter or window); the file is never truncated. A row that cannot be mapped fails the operation. A window with no matching issues yields a header-only file (csv) or an empty file (jsonl).

Duplicates and concurrency. A caller may have one export in flight per workspace. A request that matches the caller’s in-flight export (same workspace, format, filter and requested updatedAt bounds) returns 202 for that same export, so a retry after a lost response is safe. A request that does not match returns 429 with Retry-After.

Download. resourceUrl is a pre-signed blob URL minted on each poll; its lifetime is fixed by blob storage. Treat it as a credential and do not log it. The operation stays available for 7 days.

POST /issues/export

Required OAuth Scopes

grc:read

Parameters#

Parameter

In

Type

Required

Description

X-Version

header

string

true

Version of the API (2026-09-01.grc.preview)

body

body

IssueExportRequest

true

Wk-Workspace

header

string

true

The id of the Workiva workspace to operate in. Authorization is enforced against the authenticated user’s access within this workspace.

Body parameter example#

{
  "filter": "status eq 'open'",
  "format": "csv",
  "updatedAt": {
    "from": "2025-10-01T00:00:00Z",
    "to": "2026-10-01T00:00:00Z"
  }
}

Code Samples#

curl -X POST 'https://api.app.wdesk.com/issues/export' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'Wk-Workspace: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d' \
    -H 'Authorization: Bearer {access-token}' \
    -H 'X-Version: 2026-09-01.grc.preview' \
    --data-raw '{"filter":"status eq '"'"'open'"'"'","format":"csv","updatedAt":{"from":"2025-10-01T00:00:00Z","to":"2026-10-01T00:00:00Z"}}'
echo '{"filter":"status eq '"'"'open'"'"'","format":"csv","updatedAt":{"from":"2025-10-01T00:00:00Z","to":"2026-10-01T00:00:00Z"}}' | http POST https://api.app.wdesk.com/issues/export \
    X-Version:2026-09-01.grc.preview \
    Content-Type:application/json \
    Accept:application/json \
    Wk-Workspace:1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d \
    Authorization:"Bearer {access-token}"
wget --method=POST "https://api.app.wdesk.com/issues/export" \
    --output-document - \
    --header 'Content-Type: application/json' \
    --header 'Accept: application/json' \
    --header 'Wk-Workspace: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d' \
    --header 'Authorization: Bearer {access-token}' \
    --header 'X-Version: 2026-09-01.grc.preview' \
    --body-data='{"filter":"status eq '"'"'open'"'"'","format":"csv","updatedAt":{"from":"2025-10-01T00:00:00Z","to":"2026-10-01T00:00:00Z"}}'
import requests

headers = {
  'X-Version': '2026-09-01.grc.preview',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Wk-Workspace': '1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
  'Authorization': 'Bearer {access-token}'
}

body = {
  "filter": "status eq 'open'",
  "format": "csv",
  "updatedAt": {
    "from": "2025-10-01T00:00:00Z",
    "to": "2026-10-01T00:00:00Z"
  }
}

r = requests.post('https://api.app.wdesk.com/issues/export', headers = headers, json=body)

print(r.headers['Location'])

Returns#

202 - The export was accepted; poll the operation named in Location. The body echoes the effective updatedAt window, computed once at request time and persisted on the job. A repeat request that matches the caller’s in-flight export (same workspace, format, filter and requested updatedAt bounds) also returns 202 for that same export.#

The body of the 202 response to an export request. The operation to poll is named in the Location header.

Header

Description

Location

The location to poll for the operation result.

Retry-After

The number of seconds to wait before polling for a result and between polling attempts.

400 - The request was invalid. invalidFilter with target /filter for an invalid filter; invalidRequest with target /updatedAt for a window longer than 366 days, a from not before the effective to or, with no to, within the last 30 seconds, or a count above 50,000 rows (“narrow your filter or window”), and with target /format for an unsupported format.#

Error response indicating the service could not process the request.

401 - Authentication is missing or invalid.#

Error response indicating the service could not process the request.

403 - The caller is not permitted to access this resource.#

Error response indicating the service could not process the request.

429 - The caller already has an export in flight in this workspace that does not match this request (different format, filter or requested updatedAt bounds). Retry after the number of seconds in Retry-After, or when that export completes. A request that matches the in-flight export returns 202 instead.#

Error response indicating the service could not process the request.

500 - An unexpected error occurred.#

Error response indicating the service could not process the request.

503 - Service Unavailable#

Error response indicating the service could not process the request.

Example Responses#

{
  "updatedAt": {
    "from": "2025-10-01T00:00:00Z",
    "to": "2026-10-01T00:00:00Z"
  }
}
{
  "code": "invalidFilter",
  "message": "The filter could not be parsed.",
  "target": "/filter"
}
{
  "code": "invalidRequest",
  "message": "The updatedAt window must not be longer than 366 days.",
  "target": "/updatedAt"
}
{
  "code": "unauthenticated",
  "message": "Authentication credentials were missing or invalid."
}
{
  "code": "permissionDenied",
  "message": "You do not have permission to access this resource."
}
{
  "code": "tooManyRequests",
  "message": "An export is already in progress. Retry after the period in the Retry-After header."
}
{
  "code": "internalError",
  "message": "An unexpected error occurred. Please try again later."
}
{
  "code": "503ServiceUnavailable",
  "message": "The server cannot handle the request due to a temporary overload or scheduled maintenance"
}