---
title: "Upload Demand Forecast"
method: POST
path: "/v1/forecasting/demand"
---

# Upload Demand Forecast

`POST /v1/forecasting/demand`

## Request

- Base URL: `https://app.untetherlabs.com/api`
- URL: `https://app.untetherlabs.com/api/v1/forecasting/demand`
- Auth: HTTP bearer

Upload demand: the number of staff each role needs over a time range.

Each row has a [role](/reference/v1/roles/get-role) id, a startDate, an
endDate and a demand, the number of staff needed throughout the range.
Times are ISO-8601 with an explicit offset, on a quarter hour (:00, :15,
:30 or :45). A row covers its start up to, but not including, its end.
Rows may come in any order.

For each role in the request, everything stored between the role's
earliest start and latest end is replaced by the rows sent, for every
skill value, including stretches no row covers. Demand outside that span
is unchanged.

A role broken down by [skills](/guides/skills) accepts one extra column
per skill. A cell holds one of the skill's values; an empty cell, or in
JSON a missing key, null or "", means any value. Column names and values
are matched exactly.
[`GET /v1/forecasting/demand-config/{teamId}`](/reference/v1/forecasting/get-demand-config)
lists the columns and values each role accepts. Rows for the same role may
overlap when their skill values differ, and overlapping demand adds up.
Rows with the same role and skill values must not overlap.

The body is application/json, an array of rows with skill columns as extra
keys, or text/csv with a header row: role, startDate, endDate, demand,
then any skill columns, in any order.

Nothing is stored if any row is invalid; the response is a 400. A field in
the wrong format, such as a time without an offset, a role that is not an
id, or a missing or negative demand, is reported with the field and its
row. A row that breaks an upload rule, such as an end before its start, a
time off the quarter hour, a value a skill column does not allow, or an
overlap, is reported as "Row 3: ...", counting from 1 and, in CSV, without
the header. A role id that does not exist returns RoleNotFound. A column
the role does not accept is ignored and listed in ignoredColumns.

## Request body

- UploadDemandForecastRow[]
  - `role` string, uuid, required: The id of the [role](/reference/v1/roles/get-role) the demand is for.
  - `startDate` string, required: Start of the range, included. ISO-8601 with an explicit offset, on a quarter hour.
  - `endDate` string, required: End of the range, not included. ISO-8601 with an explicit offset, on a quarter hour.
  - `demand` number, required: The number of staff needed throughout the range. 0 or more.

## Response `201`

- UploadDemandForecastResponse
  - `ignoredColumns` string[], required

## Other responses

- `400`: We follow the [RFC-9457](https://www.rfc-editor.org/rfc/rfc9457.html) Problem Detail specification for error responses. Errors should be identified by their `type` field. You can view extensions for each error response below.
- `404`: We follow the [RFC-9457](https://www.rfc-editor.org/rfc/rfc9457.html) Problem Detail specification for error responses. Errors should be identified by their `type` field. You can view extensions for each error response below.

---

[API](https://skmtc.dev/untetherlabs/apis/untether-labs-api.md) - [All operations](https://skmtc.dev/untetherlabs/apis/untether-labs-api/llms.txt) - [OpenAPI document](https://skmtc.dev/untetherlabs/apis/untether-labs-api/revisions/143223935df3?raw)
