Sustainability API Guide

Table Of Contents

Sustainability API Guide#

The Sustainability API enables you to programmatically manage sustainability programs, metrics, dimensions, and data points within the Workiva platform. Use it to sync data from external systems like data lakes, automate data collection across reporting periods, manage metric value statuses through review workflows, and integrate sustainability data with tasking for approval processes.

Important: The Sustainability API is only available as a paid add-on to the Workiva platform. If you’re not sure whether your workspace has this add-on enabled, reach out to your Customer Success Manager (CSM) to confirm before you start building.


Table of Contents#


Prerequisites & Authentication#

Before getting started, make sure you have credentials set up.

All requests to the Workiva API require:

  • A valid OAuth 2.0 Bearer token

  • The X-Version: 2026-01-01 header on every request

  • The appropriate OAuth scopes for each operation (noted per endpoint below)

export TOKEN="your_access_token_here"
export PROGRAM_ID="your-program-uuid"

Important: Always include X-Version: 2026-01-01 on all requests. Mixing functionality between different API versions is not supported and may produce unpredictable results.


Pagination#

All list endpoints in the Sustainability API return paginated results. When a response includes an @nextLink field at the top level, there are more results available. You must follow @nextLink to retrieve the full dataset.

If you ignore @nextLink, your results will be silently truncated. For integrations that load hundreds of metrics or thousands of metric values, this can cause data loss without any error.

How to Follow Pagination#

@nextLink is an opaque URL returned by the server. Treat it as a black box; do not construct or modify it. Make a GET request to the exact URL provided, using the same auth headers, until the response no longer includes @nextLink.

You can also control pagination behavior with these query parameters on the initial request:

  • $maxpagesize sets the maximum number of results per page (e.g., $maxpagesize=50).

  • $next is an explicit pagination cursor. In most cases you should follow @nextLink instead of constructing $next values yourself.

NEXT_LINK="https://api.app.wdesk.com/programs?..."

while [ -n "$NEXT_LINK" ]; do
  RESPONSE=$(curl -s -G "${NEXT_LINK}" \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Accept: application/json" \
    -H "X-Version: 2026-01-01")

  echo "$RESPONSE" | jq '.data[]'

  NEXT_LINK=$(echo "$RESPONSE" | jq -r '.["@nextLink"] // empty')
done

Note: @nextLink applies to all list endpoints: GET /programs, GET /programs/{programId}/metrics, GET /programs/{programId}/metrics/{metricId}/values, and others. Always paginate to completion before processing results.


Key Concepts#

Concept

Description

Program

A sustainability program is the top-level container that organizes all sustainability data within a workspace. It holds topics, metrics, and dimensions. Program names must be unique within a workspace.

Topic

An organizational grouping mechanism for metrics. Topics can be nested (a topic can have a parent topic) to create a hierarchical outline, for example “Climate” > “Emissions” > “Scope 1”.

Metric

A trackable data point within a program, such as “Scope 1 GHG Emissions” or “Total Energy Consumption”. Metrics have a dataType (text, number, date, currency, percent), a unit of measurement, and can reference sustainability frameworks like GRI or SASB.

Dimension

A categorization axis that allows collecting multiple values per metric. Common dimensions include facility location, business unit, market region, or demographic breakdowns. A metric can use up to 7 dimensions, and each dimension can have up to 1,500 values.

Metric Value

A single data record belonging to a metric for a specific reporting period and dimension coordinate. For example, the Scope 1 emissions value for “Ames, IA” in Q1 2025.

Reporting Period

Defines the time window for a metric value using year, start month, and end month. Months are integers (1–12). For example, Q1 = start: 1, end: 3; full year = start: 1, end: 12.

Value Status

The workflow state of a metric value: notStarted, notSent, inProgress, sentForApproval, returned, complete, or error. Status transitions are constrained. You can set a value to complete only from notStarted, and reset to notStarted only from complete.

Coordinates

A mapping of dimension IDs to dimension value IDs that identifies which “slice” of a metric a value belongs to. For example, {"facility-dimension-id": "Ames, IA"}. A value can have up to 3 coordinate pairs.

Framework Reference

An association linking a metric to a reporting framework or standard (e.g., GRI 2-6-b-iii, SASB, TCFD), including the framework name, published version, and reference identifier.

Tag

A key-value pair associated with a metric for classification, for example {"name": "Industry", "value": "Agriculture"}.


How Sustainability Programs Are Structured#

A sustainability program follows a hierarchical structure:

Program
├── Dimensions (e.g., Facility, Region)
│   └── Dimension Values (e.g., "Ames, IA", "Portland, OR")
├── Topics (e.g., Climate, Water, Waste)
│   └── Subtopics (nested via parent reference)
└── Metrics (e.g., Scope 1 GHG Emissions)
    ├── Framework References (GRI, SASB, TCFD)
    ├── Tags (Industry: Agriculture)
    └── Metric Values (data points per reporting period + coordinates)
        ├── Reporting Period (year, start month, end month)
        ├── Coordinates (dimension → dimension value mappings)
        ├── Status (notStarted → inProgress → complete)
        └── Task (optional linked task for review/approval)

Metrics are organized under topics, and each metric value is uniquely identified by the combination of its reporting period and coordinates. This means you can have one value per metric per unique (reporting period + coordinate) combination.

Note: A metric value’s identity is determined by its reportingPeriod and coordinates. If you create two values with the same reporting period and coordinates for the same metric, the batch upsert endpoint will update the existing value rather than creating a duplicate.


Managing Programs#

Listing Programs#

Retrieve all sustainability programs in the current workspace.

Required scope: file:read

GET /programs

curl -X GET "https://api.app.wdesk.com/programs" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "@nextLink": "<opaque_url>",
  "data": [
    {
      "id": "ca72b647-8e43-44c3-a4e7-2aa3294c37ad",
      "name": "Sustainability Program"
    }
  ]
}

You can filter programs by name using the $filter query parameter.

curl -G "https://api.app.wdesk.com/programs" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=name eq "ESG Reporting 2025"'

Tip: Use $filter=name eq "..." to find a specific program by name when you don’t have its ID. This is particularly useful when mapping programs from an external system.

Retrieving a Single Program#

Required scope: file:read

GET /programs/{programId}

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "id": "ca72b647-8e43-44c3-a4e7-2aa3294c37ad",
  "name": "Sustainability Program"
}

Creating a Program#

Required scope: file:write

POST /programs

Request Body:

{
  "name": "ESG Reporting 2025"
}
curl -X POST "https://api.app.wdesk.com/programs" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (201 Created):

{
  "id": "ca72b647-8e43-44c3-a4e7-2aa3294c37ad",
  "name": "ESG Reporting 2025"
}

Note: Program names must be unique within a workspace. Attempting to create a program with a duplicate name will return a 409 Conflict error.

Renaming a Program#

Required scope: file:write

PATCH /programs/{programId}

Request Body:

[
  {
    "op": "replace",
    "path": "/name",
    "value": "ESG Reporting 2025 - Draft"
  }
]
curl -X PATCH "https://api.app.wdesk.com/programs/${PROGRAM_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (200 OK):

{
  "id": "ca72b647-8e43-44c3-a4e7-2aa3294c37ad",
  "name": "ESG Reporting 2025 - Draft"
}

Warning: Only the /name property can be updated via PATCH. Only one property may be updated at a time.


Managing Topics#

Topics provide organizational structure for your metrics. They can be nested to create a hierarchical outline.

Creating a Topic#

Required scope: file:write

POST /programs/{programId}/topics

Request Body:

{
  "name": "Climate",
  "index": 0
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/topics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (201 Created):

{
  "id": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
  "name": "Climate",
  "index": 0,
  "parent": null
}

Creating a Subtopic#

To create a subtopic, include the parent field referencing the parent topic’s ID.

Request Body:

{
  "name": "Emissions",
  "index": 0,
  "parent": "cc507098-c403-4b8b-98b4-31a6c5a639f4"
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/topics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (201 Created):

{
  "id": "dd618209-d504-5c9c-a5f8-42b4305d98e5",
  "name": "Emissions",
  "index": 0,
  "parent": "cc507098-c403-4b8b-98b4-31a6c5a639f4"
}

Tip: Set the index property to control the ordering of topics in the program outline. If index is not set, the topic will be ordered last.

Updating a Topic#

Required scope: file:write

PATCH /programs/{programId}/topics/{topicId}

Only one property may be updated at a time. The following properties support PATCH:

Path

Supported Operations

/index

replace, test

/name

replace, test

/parent

replace, test

Request Body:

[
  {
    "op": "replace",
    "path": "/name",
    "value": "Climate & Energy"
  }
]
curl -X PATCH "https://api.app.wdesk.com/programs/${PROGRAM_ID}/topics/${TOPIC_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (200 OK):

{
  "id": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
  "name": "Climate & Energy",
  "index": 0,
  "parent": null
}

Deleting a Topic#

Required scope: file:write

DELETE /programs/{programId}/topics/{topicId}

curl -X DELETE "https://api.app.wdesk.com/programs/${PROGRAM_ID}/topics/${TOPIC_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-01-01"

Response (204 No Content)

Warning: Deleting a topic does not delete the metrics beneath it. Metrics that belonged to the deleted topic will no longer have a topic assignment. Reassign them with a PATCH before deleting the topic if needed.


Managing Dimensions#

Dimensions allow you to collect multiple values per metric across different categories, such as facility locations, business units, or demographic groups.

Listing Dimensions#

Required scope: file:read

GET /programs/{programId}/dimensions

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}/dimensions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "data": [
    {
      "active": true,
      "id": "c38353ce-32bc-4ea7-852a-bf5e12b72d76",
      "name": "Facility"
    }
  ]
}

Note: When listing dimensions, the values array is null. To retrieve the full list of dimension values, fetch the individual dimension by ID.

Retrieving a Dimension with Its Values#

Required scope: file:read

GET /programs/{programId}/dimensions/{dimensionId}

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}/dimensions/${DIMENSION_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "active": true,
  "id": "c38353ce-32bc-4ea7-852a-bf5e12b72d76",
  "name": "Facility",
  "values": [
    {
      "id": "Ames, IA",
      "name": "Ames, Iowa",
      "active": true
    },
    {
      "id": "Portland, OR",
      "name": "Portland, Oregon",
      "active": true
    }
  ]
}

Key fields:

  • id: The system-generated UUID for the dimension.

  • values: The list of dimension values. Each value has a user-entered id (up to 20 characters), an optional name (up to 300 characters, defaults to id if not provided), and an active boolean indicating whether the value is available for use in new metric values.

  • active: Whether the dimension is available for use in new metrics.

Important: Dimension value id fields are user-entered identifiers (up to 20 characters), not system-generated UUIDs. These IDs are what you use in the coordinates mapping when creating metric values.

Creating a Dimension#

Required scope: file:write

POST /programs/{programId}/dimensions

Request Body:

{
  "active": true,
  "name": "Facility",
  "values": [
    {
      "id": "Ames, IA",
      "name": "Ames, Iowa",
      "active": true
    },
    {
      "id": "Portland, OR",
      "name": "Portland, Oregon",
      "active": true
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/dimensions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (201 Created):

{
  "active": true,
  "id": "c38353ce-32bc-4ea7-852a-bf5e12b72d76",
  "name": "Facility",
  "values": [
    {
      "id": "Ames, IA",
      "name": "Ames, Iowa",
      "active": true
    },
    {
      "id": "Portland, OR",
      "name": "Portland, Oregon",
      "active": true
    }
  ]
}

Note: A dimension must have at least 1 value and can have up to 1,500 values. Plan your dimension values carefully, as the id field on each value is what you will use in metric value coordinates.

Updating a Dimension#

Required scope: file:write

PATCH /programs/{programId}/dimensions/{dimensionId}

Only one property may be updated at a time. The following properties support PATCH:

Path

Supported Operations

/active

replace, test

/name

replace, test

/values

replace, test

Request Body:

[
  {
    "op": "replace",
    "path": "/name",
    "value": "Office Location"
  }
]
curl -X PATCH "https://api.app.wdesk.com/programs/${PROGRAM_ID}/dimensions/${DIMENSION_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (200 OK):

{
  "active": true,
  "id": "c38353ce-32bc-4ea7-852a-bf5e12b72d76",
  "name": "Office Location",
  "values": null
}

Tip: Use PATCH with /values to add or update dimension values after the dimension has been created. Use /active to deactivate a dimension without deleting it.


Managing Metrics#

Metrics are the trackable data points in your sustainability program. Each metric has a data type, unit, and belongs to a topic.

Listing Metrics#

Required scope: file:read

GET /programs/{programId}/metrics

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "data": [
    {
      "code": 12,
      "dataType": "number",
      "description": "Covers all direct greenhouse gas emissions from sources owned or controlled by the reporting entity.",
      "id": "ae82b647-8e43-44c3-a4e7-2aa3294c87ac",
      "index": 0,
      "name": "Scope 1 Consolidated GHG Emissions",
      "requireNotes": false,
      "requireSupportingAttachments": false,
      "strCode": "M12",
      "topic": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
      "unit": "Metric Ton"
    }
  ]
}

Filtering Metrics#

You can filter metrics by many properties including name, dataType, topic, code, and unit.

curl -G "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=name contains "GHG"'

Filter Field

Supported Predicates

name

eq, ne, in, contains

dataType

eq, ne, in

topic

eq, ne, in

code

eq, ne, in, gt, ge, lt, le

unit

eq, ne, in, contains

id

eq, ne, in

Tip: Use $filter=name contains "..." to search for metrics by keyword when mapping from an external system where you know the metric name but not the Workiva ID.

Creating a Metric#

Required scope: file:write

POST /programs/{programId}/metrics

Request Body:

{
  "name": "Scope 1 Consolidated GHG Emissions",
  "dataType": "number",
  "unit": "Metric Ton",
  "description": "Covers all direct greenhouse gas emissions from sources owned or controlled by the reporting entity.",
  "topic": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
  "allowedDimensions": ["c38353ce-32bc-4ea7-852a-bf5e12b72d76"],
  "requireNotes": false,
  "requireSupportingAttachments": false,
  "frameworkReferences": [
    {
      "framework": "GRI",
      "publishedVersion": "2016.2",
      "referenceName": "305-1"
    }
  ],
  "tags": [
    {
      "name": "Category",
      "value": "Environment"
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (201 Created):

{
  "code": 14,
  "dataType": "number",
  "description": "Covers all direct greenhouse gas emissions from sources owned or controlled by the reporting entity.",
  "id": "ae82b647-8e43-44c3-a4e7-2aa3294c87ac",
  "index": 0,
  "name": "Scope 1 Consolidated GHG Emissions",
  "requireNotes": false,
  "requireSupportingAttachments": false,
  "strCode": "M14",
  "topic": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
  "unit": "Metric Ton"
}

Key fields:

  • dataType: One of text, number, date, currency, percent.

  • allowedDimensions: Up to 7 dimension IDs that can be used when creating values for this metric.

  • topic: The UUID of the topic this metric belongs to.

  • code: An optional integer identifier. If not provided, the system assigns the next available integer.

  • strCode: The system-generated string representation (e.g., M14), read-only.

  • frameworkReferences: Up to 20 framework associations (e.g., GRI, SASB, TCFD).

  • tags: Up to 20 key-value tag pairs for classification.

Updating a Metric#

Required scope: file:write

PATCH /programs/{programId}/metrics/{metricId}

Only one property may be updated at a time. The following properties support PATCH:

Path

Supported Operations

/name

replace, test

/datatype

replace

/description

replace, test

/unit

replace, test

/topic

replace, test

/index

replace, test

/requireNotes

replace, test

/requireSupportingAttachments

replace, test

Request Body:

[
  {
    "op": "replace",
    "path": "/unit",
    "value": "Metric Tons CO2e"
  }
]
curl -X PATCH "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (200 OK):

{
  "code": 14,
  "dataType": "number",
  "description": "Covers all direct greenhouse gas emissions from sources owned or controlled by the reporting entity.",
  "id": "ae82b647-8e43-44c3-a4e7-2aa3294c87ac",
  "index": 0,
  "name": "Scope 1 Consolidated GHG Emissions",
  "requireNotes": false,
  "requireSupportingAttachments": false,
  "strCode": "M14",
  "topic": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
  "unit": "Metric Tons CO2e"
}

Deleting a Metric#

Required scope: file:write

DELETE /programs/{programId}/metrics/{metricId}

curl -X DELETE "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-01-01"

Response (204 No Content)

Warning: Deleting a metric permanently removes it and all associated metric values. This action is irreversible.


Loading and Updating Metric Values#

Metric values are the actual data points collected for each metric. Each value is identified by its reporting period and coordinates (dimension mappings).

Creating a Single Metric Value#

Required scope: file:write

POST /programs/{programId}/metrics/{metricId}/values

Request Body:

{
  "value": "512.8768743",
  "reportingPeriod": {
    "year": 2025,
    "start": 1,
    "end": 12
  },
  "coordinates": {
    "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
  },
  "status": "notStarted",
  "notes": "Sourced from ERP system export on 2025-01-15"
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (201 Created):

{
  "value": "512.8768743",
  "reportingPeriod": {
    "year": 2025,
    "start": 1,
    "end": 12
  },
  "coordinates": {
    "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
  }
}

Key fields:

  • value: Always a string, even for numeric metrics. Up to 32,767 characters.

  • reportingPeriod: Defines the time window. Use month integers: Q1 = start: 1, end: 3; Q2 = start: 4, end: 6; full year = start: 1, end: 12.

  • coordinates: Maps dimension IDs (UUIDs) to dimension value IDs (the user-entered short IDs). Up to 3 coordinate pairs. Omit if the metric has no dimensions.

  • status: The initial workflow status. See Managing Value Status for valid transitions.

  • notes: Optional notes (up to 32,767 characters).

  • task: Optional task ID to link this value to a task for review/approval workflows.

Important: The coordinates mapping uses the dimension’s UUID as the key and the dimension value’s user-entered id as the value, not the dimension value’s name.

Listing Metric Values#

Required scope: file:read

GET /programs/{programId}/metrics/{metricId}/values

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "data": [
    {
      "id": "bf9aa2c3-f278-4c77-8acb-97584e843dcd",
      "value": "512.8768743",
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
      },
      "reportingPeriod": {
        "year": 2025,
        "start": 1,
        "end": 12
      },
      "status": "notStarted",
      "notes": "Sourced from ERP system export on 2025-01-15",
      "task": "VGFMDU1MmJiOGEwZDVjZWYzNDlVhZDWU5Y2jYzY3zax5iMg"
    }
  ]
}

Deleting a Single Metric Value#

Required scope: file:write

DELETE /programs/{programId}/metrics/{metricId}/values/{valueId}

curl -X DELETE "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/${VALUE_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-01-01"

Response (204 No Content)

Use this endpoint when you need to delete one specific value and you have its id. For deleting multiple values at once, use the Batch Deleting Metric Values endpoint instead.

Updating a Single Metric Value#

Required scope: file:write

PATCH /programs/{programId}/metrics/{metricId}/values/{metricValueId}

Only one property may be updated at a time. The following properties support PATCH:

Path

Supported Operations

/notes

replace, test

/value

replace, test

Request Body:

[
  {
    "op": "replace",
    "path": "/notes",
    "value": "Updated notes after review"
  }
]
curl -X PATCH "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/${VALUE_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (200 OK):

{
  "coordinates": {},
  "id": "bf9aa2c3-f278-4c77-8acb-97584e843dcd",
  "notes": "Updated notes after review",
  "reportingPeriod": {
    "end": 12,
    "start": 1,
    "year": 2025
  },
  "value": "512.8768743"
}

Tip: Use PATCH when you need to update a single value’s notes or value field without going through the batch upsert flow.


Filtering Metric Values by Reporting Period#

Use the $filter query parameter to retrieve values for specific reporting periods.

Full Year Values#

curl -G "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=reportingPeriod.year eq 2025'

Quarterly Values#

Retrieve only Q1 2025 values:

curl -G "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=reportingPeriod.year eq 2025 and reportingPeriod.start eq 1 and reportingPeriod.end eq 3'

Available Filter Fields#

Field

Supported Predicates

reportingPeriod.year

eq, ne, in, gt, ge, lt, le

reportingPeriod.start

eq, ne, in, gt, ge, lt, le

reportingPeriod.end

eq, ne, in, gt, ge, lt, le

id

eq, ne, in

notes

eq, ne, in, contains

Tip: Use reportingPeriod.year ge 2024 and reportingPeriod.year le 2025 to retrieve values across multiple years for year-over-year comparison.


Managing Value Status#

Metric values have a workflow status that tracks their progress through the data collection and review process.

Status Values#

Status

Description

notStarted

No data has been entered yet. This is the starting state.

notSent

Data has been entered but the task has not been sent to assignees.

inProgress

The assignee is actively working on the data point.

sentForApproval

Data has been submitted and is awaiting approval.

returned

The approver has returned the data for revision.

complete

Data has been finalized and approved.

error

An error occurred during processing.

Status Transition Rules#

The status field on a metric value can only be changed directly via the API for two transitions. All other transitions are driven by the task approval workflow and cannot be set via the API directly.

Transition

Trigger

How

completenotStarted

Reset a value for re-entry

Set status: "notStarted" via batch upsert

notStartedcomplete

Mark as final without a task

Set status: "complete" via batch upsert

notStartednotSent

Task created but not sent

Automatic (task workflow)

notSentinProgress

Task sent to assignee

Automatic (task workflow)

inProgresssentForApproval

Assignee submits (SUBMIT action)

Task action submission

sentForApprovalcomplete

Approver approves (APPROVE action)

Task action submission

sentForApprovalreturned

Approver rejects (REJECT action)

Task action submission

returnedinProgress

Assignee revises and resubmits

Automatic (task workflow)

Any status → error

Processing failure

Automatic (system-set; cannot be set via API)

Important: Attempting to set status to complete from any state other than notStarted will return a validation error. If a value is in inProgress, sentForApproval, or returned, its status must advance through the task approval workflow.

To mark a value as complete (from notStarted):

Request Body:

{
  "data": [
    {
      "reportingPeriod": {
        "year": 2025,
        "start": 1,
        "end": 12
      },
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
      },
      "status": "complete",
      "value": "512.8768743"
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/batchUpsertion" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Tip: When loading data from a data lake where the values are already considered final, set status to complete during the upsert. If the data still requires human review, leave the status as notStarted or omit it, and use the task approval workflow to manage the review process.


Batch Upserting Metric Values#

The batch upsert endpoint is the most efficient way to load or update multiple metric values at once. It handles both creating new values and updating existing ones in a single request.

Required scope: file:write

POST /programs/{programId}/metrics/{metricId}/values/batchUpsertion

Request Body:

{
  "data": [
    {
      "value": "512.87",
      "reportingPeriod": {
        "year": 2025,
        "start": 1,
        "end": 3
      },
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
      },
      "status": "notStarted",
      "notes": "Q1 2025 - imported from data lake"
    },
    {
      "value": "489.23",
      "reportingPeriod": {
        "year": 2025,
        "start": 4,
        "end": 6
      },
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
      },
      "status": "notStarted",
      "notes": "Q2 2025 - imported from data lake"
    },
    {
      "value": "601.44",
      "reportingPeriod": {
        "year": 2025,
        "start": 1,
        "end": 3
      },
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Portland, OR"
      },
      "status": "notStarted"
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/batchUpsertion" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (202 Accepted):

{
  "operationLocation": "https://api.app.wdesk.com/operations/128f274395254cf17eda6b3eb3d021b9"
}

Response Headers:

Location: https://api.app.wdesk.com/operations/128f274395254cf17eda6b3eb3d021b9
Retry-After: 5

This is an asynchronous operation. Poll the operationLocation URL to check the status.

Polling for Completion#

GET /operations/{operationId}

curl -X GET "https://api.app.wdesk.com/operations/${OPERATION_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Version: 2026-01-01"

Response (in progress):

{
  "id": "128f274395254cf17eda6b3eb3d021b9",
  "status": "started",
  "created": { "dateTime": "2026-01-15T10:00:00Z" },
  "updated": { "dateTime": "2026-01-15T10:00:05Z" }
}

Response (completed):

{
  "id": "128f274395254cf17eda6b3eb3d021b9",
  "status": "completed",
  "resourceUrl": "https://api.app.wdesk.com/operations/128f274395254cf17eda6b3eb3d021b9/results",
  "created": { "dateTime": "2026-01-15T10:00:00Z" },
  "updated": { "dateTime": "2026-01-15T10:00:30Z" }
}

Important: Always respect the Retry-After header when polling. The operations endpoint is rate-limited. Polling too frequently may result in 429 Too Many Requests errors.

How Upsertion Identifies Existing Values#

For each value in the batch, provide either:

  1. The value’s id (if you know it from a previous GET), or

  2. Both the reportingPeriod and coordinates

If both are provided, the id takes precedence. When matching by reporting period and coordinates, an existing value with the same combination will be updated; otherwise, a new value is created.

Warning: The payload is limited to 10 MB. Break larger payloads into multiple requests. If any value in the batch fails validation, no changes are stored and the entire batch is rolled back.

Updating Existing Values#

To update an existing data point’s value, use the batch upsert endpoint with the same reporting period and coordinates. The new value and any other provided fields will overwrite the existing record.

Request Body:

{
  "data": [
    {
      "value": "525.10",
      "reportingPeriod": {
        "year": 2025,
        "start": 1,
        "end": 3
      },
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
      },
      "notes": "Q1 2025 - corrected value after audit"
    }
  ]
}

Clearing Fields on Update#

Use the fieldsToClear array to explicitly clear optional fields during an upsert. This is the only way to remove a value’s notes or other nullable fields.

Request Body:

{
  "data": [
    {
      "id": "bf9aa2c3-f278-4c77-8acb-97584e843dcd",
      "value": "525.10",
      "fieldsToClear": ["notes"]
    }
  ]
}

Batch Deleting Metric Values#

Required scope: file:write

POST /programs/{programId}/metrics/{metricId}/values/batchDeletion

For each value to delete, provide either the id or both the reportingPeriod and coordinates.

Request Body:

{
  "data": [
    {
      "reportingPeriod": {
        "year": 2025,
        "start": 1,
        "end": 3
      },
      "coordinates": {
        "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA"
      }
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/batchDeletion" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (204 No Content)

Warning: Batch deletion is immediate and irreversible. There is no undo. Verify your reporting period and coordinates carefully before executing.


Linking Metric Values to Workiva Documents#

Metric values can be connected to live data in a Workiva spreadsheet or document using the dataSource field. When a dataSource is set, the metric value reflects data from the linked cell or region, connecting sustainability data directly to your financial reporting documents.

Connection Types#

connectionType

Required Fields

Description

spreadsheetCell

spreadsheet, sheet, cell

Links to a specific cell in a Workiva spreadsheet

tableCell

document, section, table, cell

Links to a cell within a table in a Workiva document

textRegion

document, section

Links to a text region within a Workiva document

Example: Linking to a Spreadsheet Cell#

Include dataSource in your batch upsert request body alongside the value:

{
  "data": [
    {
      "value": "512.87",
      "reportingPeriod": { "year": 2025, "start": 1, "end": 3 },
      "coordinates": { "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA" },
      "dataSource": {
        "connectionType": "spreadsheetCell",
        "spreadsheetCellConnection": {
          "spreadsheet": "7a5e271acf1d49d480a6fbabc394a0fa",
          "sheet": "576696e0f7a143b4a0bc7c20a34480ab",
          "cell": "A1"
        }
      }
    }
  ]
}

Example: Linking to a Document Table Cell#

{
  "dataSource": {
    "connectionType": "tableCell",
    "tableCellConnection": {
      "document": "3f2a891bcf1d49d480a6fbabc394a012",
      "section": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "table": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5",
      "cell": "B3"
    }
  }
}

Tip: The dataSource field is available on both POST (single value create) and batchUpsertion. Use it when your sustainability program pulls data that also lives in a Workiva spreadsheet, so the two stay in sync without manual re-entry.


Supporting Attachments#

The supportingAttachments field on a metric value holds evidence or source documentation linked to that data point. It is read-only via the API (populated through the Workiva UI or task workflow) and is included in GET responses when attachments are present.

Schema#

Each attachment is a MetricAttachment object with these fields:

Field

Type

Description

name

string (required)

Display name of the attachment

type

string (required)

One of: url, file, externalFile

A metric value can have up to 50 supporting attachments.

Example GET Response with Attachments#

{
  "id": "bf9aa2c3-f278-4c77-8acb-97584e843dcd",
  "value": "512.8768743",
  "status": "sentForApproval",
  "supportingAttachments": [
    {
      "name": "Q1 2025 ERP Export",
      "type": "file"
    },
    {
      "name": "https://internal.example.com/reports/q1-ghg",
      "type": "url"
    }
  ]
}

Note: You cannot add or remove supportingAttachments via the API. Attachments are managed in the Workiva UI or through the task workflow. When an agent uses attachments as part of an approval decision (see Use Case: Agent-Based Data Analysis and Auto-Review), check whether supportingAttachments is non-empty rather than inspecting specific attachment content.


Working with Tasks for Sustainability#

Each metric value can be linked to a task for review and approval workflows. The task field on a metric value contains the task ID, which you can use with the Tasks API to manage the approval process.

Listing Tasks for a Sustainability Program#

Use the Tasks API with a location.resource filter to find tasks associated with your program.

Required scope: task:read

GET /tasks

curl -G "https://api.app.wdesk.com/tasks" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=location.resource eq "ca72b647-8e43-44c3-a4e7-2aa3294c37ad"'

Response (200 OK):

{
  "data": [
    {
      "id": "129g274495354cf18edb6b3ea3d023b2",
      "title": "Review Q1 2025 Scope 1 Emissions - Ames, IA",
      "description": "Please review and validate the Q1 2025 Scope 1 GHG emissions value for Ames, IA.",
      "status": "Created",
      "assignees": [
        {
          "id": "V1ZVd2VyFzU3NiQ1NDA4NjIzNzk2MjD",
          "type": "USER"
        }
      ],
      "dueAt": "2026-02-15T00:00:00Z",
      "location": {
        "resource": "ca72b647-8e43-44c3-a4e7-2aa3294c37ad",
        "segment": "ae82b647-8e43-44c3-a4e7-2aa3294c87ac"
      },
      "approvalSteps": [
        {
          "completionMode": "ONE",
          "dueAt": null,
          "participants": [
            {
              "id": "V0ZVc2VyHzY0MTI5NzM0MzkxODg5OTI",
              "type": "USER"
            }
          ],
          "responses": []
        }
      ],
      "modified": {
        "dateTime": "2026-01-16T08:00:00Z"
      },
      "created": {
        "dateTime": "2026-01-15T10:00:00Z",
        "user": {
          "displayName": "System Admin",
          "email": "admin@example.com",
          "id": "V3ZVc2VyFzV3NiQ5NDA2NjIzNxk2njH"
        }
      }
    }
  ]
}

Note: The Tasks API returns both general tasks and sustainability-linked tasks. Use the location.resource filter shown above to scope results to a specific program. The location and approvalSteps fields appear on tasks that have approval workflows configured.

Task Status and Approval Flow#

Sustainability tasks follow a multi-step approval process:

  1. Assignee receives the task and enters/validates the metric value

  2. Assignee submits the task for approval (SUBMIT action)

  3. Approver(s) review and either approve (APPROVE) or reject (REJECT)

  4. If rejected, the task is returned to the assignee for revision

Use the POST /tasks/{taskId}/actionSubmission endpoint to submit actions programmatically. Available actions: SUBMIT, APPROVE, REJECT, CANCEL, RESTART, SKIP.

Note: The Tasks API returns both general tasks and sustainability-associated tasks. It does not return tasks created as part of a process. Filter by location.resource to scope results to a specific program.


Managing Program Permissions#

Program permissions control which users and groups can access a sustainability program and in what capacity. Permissions are referenced by permission UUID, not by role name. Use GET /permissions to list the available permission definitions (e.g., Editor, Viewer, Owner) and their UUIDs for your workspace.

Retrieving Program Permissions#

Required scope: file:read

GET /programs/{programId}/permissions

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}/permissions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

Response (200 OK):

{
  "@nextLink": "<opaque_url>",
  "data": [
    {
      "permission": "85aa87ee-beb9-4417-8fa0-420e9de63534",
      "principal": "V0ZVc2VyHzU2NDg2NjU2MjQ0NDQ5Mjg",
      "principalType": "user",
      "resource": "014b90fd-0631-422c-b94e-1240c53f1d6d"
    },
    {
      "permission": "85aa87ee-beb9-4417-8fa0-420e9de63534",
      "principal": "V0ZVc2VyHzUQ0NDQ5Mjg2NDg2NjU2Mj",
      "principalType": "group",
      "resource": "014b90fd-b94e-0631-422c-1240c53f1d6d"
    }
  ]
}

Key fields:

  • permission: The UUID of the permission definition (e.g., Editor, Viewer). Look up permission names via GET /permissions.

  • principal: The string ID of the user or group.

  • principalType: Either user or group.

  • resource: The UUID of the program the permission applies to (read-only).

You can filter by permission or principal using the $filter query parameter.

Modifying Program Permissions#

Required scope: file:write

POST /programs/{programId}/permissions/modification

The modification endpoint accepts two arrays: toAssign for granting permissions and toRevoke for removing them. Each entry requires a permission UUID and a principal ID. To change a user’s permission level (e.g., from Viewer to Editor), revoke the old permission and assign the new one in the same request.

Request Body:

{
  "toAssign": [
    {
      "permission": "598e8fa3-3e7c-4fb7-b662-f44522216e2b",
      "principal": "V0ZVc2VyHzU2NDg2NjU2MjQ0NDQ5Mjg"
    }
  ],
  "toRevoke": [
    {
      "permission": "85aa87ee-beb9-4417-8fa0-420e9de63534",
      "principal": "V0ZVc2VyHzU2NDg2NjU2MjQ0NDQ5Mjg"
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/permissions/modification" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Response (204 No Content)

If any modification in the request fails, all modifications in that request fail.

Note: You must have owner-level access to a program to modify its permissions. Users without the appropriate role will receive a 403 Forbidden response.


Use Case: Syncing Data from a Data Lake#

This section walks through a complete workflow for syncing sustainability data from an external system (such as a data warehouse, ERP, or data lake) into a Workiva sustainability program.

Overview#

The sync workflow involves:

  1. Map your external IDs to Workiva program, metric, and dimension IDs

  2. Check if metrics/dimensions exist, and create any that are missing

  3. Load data points using the batch upsert endpoint

  4. Set the appropriate value status based on your data quality workflow

Step 1: Discover or Create the Program#

First, find your target program by name:

curl -G "https://api.app.wdesk.com/programs" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=name eq "ESG Reporting 2025"'

If the program does not exist, create it:

curl -X POST "https://api.app.wdesk.com/programs" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d '{"name": "ESG Reporting 2025"}'

Store the returned id as your PROGRAM_ID.

Step 2: Sync Dimensions#

List existing dimensions and compare against your external system’s categories:

curl -X GET "https://api.app.wdesk.com/programs/${PROGRAM_ID}/dimensions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01"

For each dimension in your external system that does not exist in Workiva, create it:

Request Body:

{
  "active": true,
  "name": "Business Unit",
  "values": [
    { "id": "BU-001", "name": "North America Operations", "active": true },
    { "id": "BU-002", "name": "European Operations", "active": true },
    { "id": "BU-003", "name": "Asia Pacific Operations", "active": true }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/dimensions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Tip: Maintain a mapping table in your integration layer that maps external system IDs to Workiva dimension IDs and dimension value IDs. Dimension value id fields are limited to 20 characters, so use short codes (e.g., BU-001) rather than full names.

Step 3: Sync Metrics#

List existing metrics and check for any that need to be created:

curl -G "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=name contains "Scope 1"'

If the metric does not exist, create it with the appropriate topic and allowed dimensions:

Request Body:

{
  "name": "Scope 1 GHG Emissions",
  "dataType": "number",
  "unit": "Metric Tons CO2e",
  "topic": "cc507098-c403-4b8b-98b4-31a6c5a639f4",
  "allowedDimensions": ["c38353ce-32bc-4ea7-852a-bf5e12b72d76"]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Step 4: Load Data Points#

Use the batch upsert endpoint to load data for each metric. The upsert handles both creating new values and updating existing ones.

Request Body:

{
  "data": [
    {
      "value": "1024.55",
      "reportingPeriod": { "year": 2025, "start": 1, "end": 3 },
      "coordinates": { "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA" },
      "status": "notStarted",
      "notes": "Data lake sync - 2026-01-15T08:00:00Z"
    },
    {
      "value": "987.32",
      "reportingPeriod": { "year": 2025, "start": 4, "end": 6 },
      "coordinates": { "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA" },
      "status": "notStarted",
      "notes": "Data lake sync - 2026-01-15T08:00:00Z"
    },
    {
      "value": "1155.80",
      "reportingPeriod": { "year": 2025, "start": 1, "end": 3 },
      "coordinates": { "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Portland, OR" },
      "status": "notStarted",
      "notes": "Data lake sync - 2026-01-15T08:00:00Z"
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/batchUpsertion" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Poll the returned operationLocation URL until the operation completes.

Step 5: Decide on Value Status#

Your choice of initial status depends on whether the data lake values are considered final:

Scenario

Recommended Status

Rationale

Data lake values are authoritative and pre-validated

complete

No further review is needed. Mark as final immediately.

Data requires human review before finalization

notStarted

Leave in starting state so reviewers can process through the task workflow.

Data needs selective review based on thresholds

notStarted with post-load analysis

Load as notStarted, then use threshold logic to auto-complete low-risk values and flag high-risk ones for review.

Tip: Wdata tables can serve as a staging area for mapping and validating external data before loading into the sustainability program. Use the Wdata Preparation API to import data into a table, perform transformations and mapping, then use the Sustainability API to load the validated data points.


Use Case: Agent-Based Data Analysis and Auto-Review#

This section describes how an automated agent can analyze loaded data and make intelligent decisions about which values need human review versus which can be auto-approved.

Threshold-Based Analysis#

After loading data from a data lake, an automated process can compare current period values against prior year data to determine whether human review is warranted.

Step 1: Retrieve Current and Prior Year Values#

Fetch values for both the current and prior reporting periods:

curl -G "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=reportingPeriod.year eq 2025'
curl -G "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=reportingPeriod.year eq 2024'

Step 2: Apply Threshold Logic#

In your application logic, compare values across periods. Example thresholds:

Condition

Action

Delta is less than 10% and less than $10,000

Auto-complete the value (low risk)

Delta exceeds 10% or exceeds $10,000

Flag for human review, create/update a task

No prior year value exists

Flag for human review (new data point)

Step 3: Auto-Complete Low-Risk Values#

For values that fall within acceptable thresholds, batch upsert with status: "complete":

Request Body:

{
  "data": [
    {
      "reportingPeriod": { "year": 2025, "start": 1, "end": 3 },
      "coordinates": { "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Ames, IA" },
      "value": "512.87",
      "status": "complete",
      "notes": "Auto-approved: delta from prior year is 2.1% ($10.52)"
    }
  ]
}
curl -X POST "https://api.app.wdesk.com/programs/${PROGRAM_ID}/metrics/${METRIC_ID}/values/batchUpsertion" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Step 4: Flag High-Risk Values for Review#

For values that exceed the threshold, leave the status as notStarted and add a note explaining why review is needed:

Request Body:

{
  "data": [
    {
      "reportingPeriod": { "year": 2025, "start": 1, "end": 3 },
      "coordinates": { "c38353ce-32bc-4ea7-852a-bf5e12b72d76": "Portland, OR" },
      "value": "1155.80",
      "status": "notStarted",
      "notes": "REVIEW REQUIRED: delta from prior year is 18.7% ($182.36). Prior year value: 973.44"
    }
  ]
}

Agent as Final Reviewer#

An automated agent can also serve as the third and final reviewer in a multi-step approval process. After human reviewers have approved or entered data, the agent can:

  1. Retrieve tasks in sentForApproval status for the program

  2. Check each value for proper supporting documentation (notes, attachments)

  3. Auto-approve values that have adequate support using the task action submission endpoint

  4. Auto-reject values that lack required notes or supporting attachments

curl -G "https://api.app.wdesk.com/tasks" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Accept: application/json" \
  -H "X-Version: 2026-01-01" \
  --data-urlencode '$filter=location.resource eq "'"${PROGRAM_ID}"'" and status eq "Awaiting Approval"'

For each task that passes the agent’s validation criteria, submit an approval:

Request Body:

{
  "action": "APPROVE",
  "comment": "Auto-approved: value within threshold, notes present, supporting attachment verified."
}
curl -X POST "https://api.app.wdesk.com/tasks/${TASK_ID}/actionSubmission" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

For tasks that fail validation:

Request Body:

{
  "action": "REJECT",
  "comment": "Auto-rejected: missing supporting attachment. Please attach source documentation before resubmitting."
}
curl -X POST "https://api.app.wdesk.com/tasks/${TASK_ID}/actionSubmission" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Version: 2026-01-01" \
  -d @request.json

Tip: When using an agent as a reviewer, always include a descriptive comment explaining the rationale for the approval or rejection. This creates an audit trail that satisfies compliance requirements.


Common Issues#

Issue

Solution

“Duplicate program name (409 Conflict)”

Program names must be unique within a workspace. Use GET /programs with $filter=name eq "..." to check for existing programs before creating.

“Batch upsert failed, all values rolled back”

If any single value in a batch fails validation, the entire batch is rejected. Check that all coordinates reference valid dimension value IDs, all reporting periods are valid, and the payload is under 10 MB.

“Cannot set status to complete”

Status can only transition to complete from notStarted. If the value is in any other status (e.g., inProgress, sentForApproval), the status must be managed through the task approval workflow.

“Dimension values list is null”

When listing dimensions via GET /programs/{programId}/dimensions, the values array is always null. Fetch the individual dimension by ID to retrieve its values.

“Coordinates don’t match, value not found”

Coordinates use the dimension UUID as the key and the dimension value’s user-entered id (not name) as the value. Verify you are using the correct identifiers.

“Metric value id vs. reporting period + coordinates”

When upserting or deleting values, you can identify a value by its id or by the combination of reportingPeriod and coordinates. If both are provided, the id takes precedence.

“Polling too frequently (429 Too Many Requests)”

Respect the Retry-After header when polling async operations. The operations endpoint is rate-limited.

“Dimension value ID too long”

Dimension value id fields are limited to 20 characters. Use short codes (e.g., BU-001) rather than full names.

“PATCH only allows one property at a time”

Both program and metric PATCH endpoints support only one operation per request. Make separate PATCH requests for each property you need to update.

“Can’t find tasks for my program”

Use $filter=location.resource eq "{programId}" on the GET /tasks endpoint to find tasks associated with a specific sustainability program.

Error Responses#

All sustainability endpoints return standard error responses:

Status

Code

Meaning

400

400BadRequest

The request body is malformed or a required parameter is missing.

401

401Unauthorized

The OAuth 2.0 token is missing, expired, or invalid.

403

403Forbidden

The token lacks the required scope or the user lacks permission on the program.

404

404NotFound

The program, metric, dimension, or value ID does not exist.

409

409Conflict

A resource with the same identifying attributes already exists (e.g., duplicate program name).

429

429TooManyRequests

Rate limit exceeded. Back off and retry after the period indicated in the response.

500

500InternalServerError

The server encountered an unexpected condition. Retry the request after a brief delay.

503

503ServiceUnavailable

The server is temporarily unable to handle the request due to overload or maintenance.

Error response bodies include a code and message:

{
  "code": "400BadRequest",
  "message": "The request was unacceptable, often due to a missing or invalid parameter"
}

Summary of API Endpoints#

Operation

Method

Endpoint

Required Scope

List programs

GET

/programs

file:read

Retrieve a program

GET

/programs/{programId}

file:read

Create a program

POST

/programs

file:write

Update a program

PATCH

/programs/{programId}

file:write

List topics

GET

/programs/{programId}/topics

file:read

Create a topic

POST

/programs/{programId}/topics

file:write

Update a topic

PATCH

/programs/{programId}/topics/{topicId}

file:write

Delete a topic

DELETE

/programs/{programId}/topics/{topicId}

file:write

List dimensions

GET

/programs/{programId}/dimensions

file:read

Retrieve a dimension

GET

/programs/{programId}/dimensions/{dimensionId}

file:read

Create a dimension

POST

/programs/{programId}/dimensions

file:write

Update a dimension

PATCH

/programs/{programId}/dimensions/{dimensionId}

file:write

List metrics

GET

/programs/{programId}/metrics

file:read

Retrieve a metric

GET

/programs/{programId}/metrics/{metricId}

file:read

Create a metric

POST

/programs/{programId}/metrics

file:write

Update a metric

PATCH

/programs/{programId}/metrics/{metricId}

file:write

Delete a metric

DELETE

/programs/{programId}/metrics/{metricId}

file:write

List metric values

GET

/programs/{programId}/metrics/{metricId}/values

file:read

Retrieve a metric value

GET

/programs/{programId}/metrics/{metricId}/values/{valueId}

file:read

Create a metric value

POST

/programs/{programId}/metrics/{metricId}/values

file:write

Update a metric value

PATCH

/programs/{programId}/metrics/{metricId}/values/{valueId}

file:write

Delete a metric value

DELETE

/programs/{programId}/metrics/{metricId}/values/{valueId}

file:write

Batch upsert metric values

POST

/programs/{programId}/metrics/{metricId}/values/batchUpsertion

file:write

Batch upsert results

GET

/operations/{operationId}/results

file:read

Batch delete metric values

POST

/programs/{programId}/metrics/{metricId}/values/batchDeletion

file:write

Retrieve program permissions

GET

/programs/{programId}/permissions

file:read

Modify program permissions

POST

/programs/{programId}/permissions/modification

file:write

List tasks

GET

/tasks

task:read

Retrieve a task

GET

/tasks/{taskId}

task:read

Submit task action

POST

/tasks/{taskId}/actionSubmission

task:write