> ## Documentation Index
> Fetch the complete documentation index at: https://docs.labelbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Performance API

> Read the numbers behind the Performance dashboard and the Monitor through the REST API or the Python SDK.

The Performance API returns the numbers shown on a project's [Performance dashboard](/horizon/guides/performance-dashboard) and on the workspace [Monitor](/horizon/guides/monitor): throughput, time, and quality metrics over a period, and totals for each labeler and reviewer. Use it to bring those numbers into your own reporting.

The API runs the same reports as the pages. For the same dates and filters it returns the same values, and the same rules decide what you can see.

## Before you start

You need:

* A Labelbox [API key](/horizon/developer-guides/create-api-key).
* Access to performance data. If you can open a project's Performance dashboard, you can read that project through the API. Workspace reports need permission to view performance across your organization, as the Monitor does.
* For the Python examples, Python SDK version 7.13.0 or later.

All endpoints are `GET` requests under this base URL, authenticated with your API key as a bearer token:

```text theme={"theme":"css-variables"}
https://api.labelbox.com/api/v1/performance
```

## Quick start

Get the number of labels created per day in a project:

<CodeGroup>
  ```python Python theme={"theme":"css-variables"}
  import labelbox as lb

  client = lb.Client(api_key="<YOUR_API_KEY>")
  project = client.get_project("<PROJECT_ID>")

  series = project.get_performance_metric(
      "labels_created", "2026-01-01", "2026-01-31"
  )
  for bucket in series.buckets:
      print(bucket.start.date(), bucket.value)
  ```

  ```bash cURL theme={"theme":"css-variables"}
  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/projects/$PROJECT_ID/metrics/labels_created?startDate=2026-01-01&endDate=2026-01-31"
  ```
</CodeGroup>

The response has one bucket per day:

```json theme={"theme":"css-variables"}
{
  "projectId": "<PROJECT_ID>",
  "metric": "labels_created",
  "unit": "count",
  "bucketType": "time",
  "buckets": [
    {
      "start": "2026-01-01T00:00:00.000Z",
      "end": "2026-01-02T00:00:00.000Z",
      "value": 412,
      "numerator": null,
      "denominator": null,
      "modes": null
    }
  ],
  "interval": "day",
  "startDate": "2026-01-01",
  "endDate": "2026-01-31",
  "generatedAt": "2026-02-01T09:30:00.000Z"
}
```

## Endpoints

| Endpoint | Python SDK | Returns |
| - | - | - |
| `GET /metrics` | `client.get_performance_metrics()` | The metrics you can query |
| `GET /projects/{projectId}/metrics/{metric}` | `project.get_performance_metric()` | One metric for a project, a value per interval |
| `GET /projects/{projectId}/labelers` | `project.get_labeler_performance()` | Totals for each labeler |
| `GET /projects/{projectId}/reviewers` | `project.get_reviewer_performance()` | Totals for each reviewer |
| `GET /projects/{projectId}/download` | `project.get_performance_report_download()` | A link to the report as a file |
| `GET /workspace/metrics/{metric}` | `client.get_workspace_performance_metric()` | One metric across your workspace |
| `GET /workspace/labelers` | `client.get_workspace_labeler_performance()` | Totals for each labeler on each project |

Every table is paged. [Filters](#filters) lists which parameters each report takes. See [Paging](#paging) and [Reliability](#reliability) before you build on the API.

## Units and dates

* **Time** is in seconds.
* **Percentages and agreement scores** are fractions from 0 to 1. A value of `0.9` means 90%.
* **Dates** are calendar days in UTC, written as `2026-01-31`. Reports are bucketed by UTC day. `startDate` and `endDate` are the first and last day of the period, both included, and a request can cover at most 366 days.
* **Wider buckets** keep their calendar bounds: a week runs from Monday, a month from the 1st. The first and last bucket of a series can reach beyond your period, but they count only the days inside it. For counts and time totals, a wider bucket is the sum of its days. Averages and percentages are worked out over the whole bucket, so they are not.

In the Python SDK you can pass a `date`, a `datetime`, or a string. A `datetime` with a time zone is converted to its UTC day.

## Metrics

List the metrics available to you, with their units and definitions:

<CodeGroup>
  ```python Python theme={"theme":"css-variables"}
  for metric in client.get_performance_metrics():
      print(metric.name, metric.unit, metric.description)
  ```

  ```bash cURL theme={"theme":"css-variables"}
  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/metrics"
  ```
</CodeGroup>

### Throughput

| Metric | Unit | What it measures |
| - | - | - |
| `done_datarows` | count | Data rows in the Done step of the workflow |
| `accepted_datarows` | count | Data rows in the Accepted step of the workflow |
| `labels_created` | count | Labels created in the period |
| `annotations_created` | count | Annotations created in the period |
| `approved_reviews` | count | Approve actions, through review tasks in the workflow, on labels created in the period |
| `rejected_reviews` | count | Reject actions, through review tasks in the workflow, on labels created in the period |
| `media_duration` | seconds | Total media duration of data rows in the Done step |
| `media_duration_in_review` | seconds | Total media duration of data rows currently in a Review step |
| `media_duration_accepted` | seconds | Total media duration of accepted data rows |
| `total_time` | seconds | Total labeling, review, and rework time spent on labels created in the period |
| `labeling_time` | seconds | Total time spent in labeling mode while the timer is on |
| `review_time` | seconds | Total time spent in review mode while the timer is on |
| `rework_time` | seconds | Total time spent in rework mode while the timer is on |

### Efficiency

| Metric | Unit | What it measures |
| - | - | - |
| `avg_time_per_label` | seconds | Total labeling time divided by the number of labels worked on, including skipped and unsaved labels |
| `avg_review_time` | seconds | Total review time divided by the number of data rows reviewed |
| `avg_rework_time` | seconds | Total rework time divided by the number of data rows reworked |
| `aht_labeled_datarows` | seconds | Average handling time per data row with at least one label |
| `aht_done_datarows` | seconds | Average handling time per data row marked Done |
| `aht_accepted_datarows` | seconds | Average handling time per data row marked Accepted |
| `aht_all_labels` | seconds | Average handling time per label created, counting skipped, abandoned, and submitted labels |
| `aht_submitted_labels` | seconds | Average handling time per submitted label |
| `aht_done_labels` | seconds | Average handling time per label on done data rows |
| `aht_accepted_labels` | seconds | Average handling time per label on accepted data rows |

Average handling time includes creating, reviewing, and reworking time. The `aht_` metrics are listed and can be queried only when average handling time charts are enabled for your organization. Their buckets also carry `modes`, the handling time split into `creating`, `reviewing`, and `reworking`.

### Quality

| Metric | Unit | What it measures |
| - | - | - |
| `benchmark_percentage` | percentage | Average benchmark agreement score of labels |
| `benchmark_distribution` | count | Histogram of benchmark agreement scores |
| `consensus_percentage` | percentage | Average consensus agreement score of labels |
| `consensus_distribution` | count | Histogram of consensus agreement scores |

The two distribution metrics return score ranges instead of time intervals. Their response has `"bucketType": "score"` and ten buckets, each with `rangeStart`, `rangeEnd`, and `value`, the number of labels whose score falls in that range. The ranges are fractions like the scores themselves: `0` to `0.1`, up to `0.9` to `1`.

## Filters

These parameters narrow a report. Lists are comma-separated in a request, or a Python list in the SDK.

| Parameter | Python SDK | Applies to | Description |
| - | - | - | - |
| `startDate`, `endDate` | `start_date`, `end_date` | All reports | Required. The period, as UTC days. |
| `interval` | `interval` | Metrics | Bucket width: `day` (default), `week`, `month`, `quarter`, or `year`. |
| `userIds` | `user_ids` | All reports | Count only work by these users. Up to 100. |
| `userGroupIds` | `user_group_ids` | Project reports | Count only work by the current members of these user groups. Up to 25. With `userIds` too, only users in both are counted. |
| `batchIds` | `batch_ids` | All reports | Count only these batches. Up to 100. |
| `projectIds` | `project_ids` | Workspace reports | Narrow the report to these projects. Up to 50. |
| `memberType` | `member_type` | Workspace reports | `internal` for members of your organization, `external` for members of other organizations. See [Shared projects and workforce providers](#shared-projects-and-workforce-providers). |
| `ownerOrganizationIds` | `owner_organization_ids` | Workspace reports | Count only projects owned by these organizations. Up to 100. |
| `deletedLabels` | `deleted_labels` | All reports | `include` (default), `exclude`, or `only`. |

A user group filter follows the group's current membership. A group that is empty, is no longer attached to the project, or that you cannot view matches no work.

A parameter that an endpoint does not have is refused by name, so a misspelled filter never runs an unfiltered report.

A list that is sent must name at least one ID. An empty list is refused with `400` (`userIds=`), and the Python SDK raises `ValueError` for one (`user_ids=[]`). It is not read as "no filter", which would report on everyone. To leave a filter off, leave the parameter out, or pass `None` in the SDK.

## Labeler and reviewer totals

These are the two tables on the Performance dashboard: one row per member with their totals for the period.

<CodeGroup>
  ```python Python theme={"theme":"css-variables"}
  labelers = project.get_labeler_performance(
      "2026-01-01",
      "2026-01-31",
      sort_by="labels_created",
      descending=True,
  )
  print(labelers.total)
  for labeler in labelers:
      print(labeler.email, labeler.labels_created, labeler.avg_time_per_label)

  for reviewer in project.get_reviewer_performance("2026-01-01", "2026-01-31"):
      print(reviewer.email, reviewer.labels_reviewed)
  ```

  ```bash cURL theme={"theme":"css-variables"}
  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/projects/$PROJECT_ID/labelers?startDate=2026-01-01&endDate=2026-01-31&sort=labelsCreated&order=desc"

  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/projects/$PROJECT_ID/reviewers?startDate=2026-01-01&endDate=2026-01-31"
  ```
</CodeGroup>

Both tables are paged and sortable. See [Paging](#paging).

The response wraps the rows in `data` and adds `page`, `perPage`, and `total`:

```json theme={"theme":"css-variables"}
{
  "projectId": "<PROJECT_ID>",
  "data": [
    {
      "userId": "<USER_ID>",
      "projectId": "<PROJECT_ID>",
      "email": "labeler@example.com",
      "memberType": "internal",
      "labelsCreated": 128,
      "labelsSkipped": 3,
      "memberTotalTime": 21640,
      "labelingTimeSubmitted": 19200,
      "labelingTimeSkipped": 240,
      "reviewsReceived": 40,
      "reviewTimeAll": 1800,
      "reworkTimeAll": 600,
      "avgTimePerLabel": 148.4,
      "avgReviewTimeAll": 45,
      "avgReworkTimeAll": 150,
      "avgBenchmarkAgreement": 0.92,
      "avgConsensusAgreement": 0.87,
      "reworkPercentage": 0.03,
      "approvalPercentage": 0.95,
      "avgRating": 4.5,
      "isRemoved": false
    }
  ],
  "page": 1,
  "perPage": 50,
  "total": 37,
  "startDate": "2026-01-01",
  "endDate": "2026-01-31",
  "cachedAt": null,
  "generatedAt": "2026-02-01T09:30:00.000Z"
}
```

### Labeler fields

The Python SDK uses the same names in snake case, such as `labels_created`.

| Field | Description |
| - | - |
| `userId`, `email` | The member. See [Access and privacy](#access-and-privacy) for when the email is masked. |
| `memberType` | `internal` if the member's account belongs to your organization, `external` if it belongs to another one. `null` if the account no longer exists. |
| `labelsCreated` | Labels created by the member in the period. |
| `labelsSkipped` | Labels skipped by the member in the period. |
| `memberTotalTime` | Total time the member spent in the editor during the period. |
| `labelingTimeSubmitted` | Labeling time spent by the member on submitted or unsaved labels. |
| `labelingTimeSkipped` | Labeling time spent by the member on skipped labels. |
| `reviewsReceived` | Approve and Reject actions, through the workflow review queue, on labels the member created. |
| `reviewTimeAll` | Total review time spent on labels the member created. |
| `reworkTimeAll` | Total rework time spent on labels the member created. |
| `avgTimePerLabel` | Total labeling time divided by the number of labels worked on, including skipped and unsaved labels. |
| `avgReviewTimeAll` | Total review time divided by the number of labels reviewed, for labels the member created. |
| `avgReworkTimeAll` | Total rework time divided by the number of labels reworked, for labels the member created. |
| `reworkPercentage` | Labels the member created that were reworked, divided by the labels they created. |
| `approvalPercentage` | Labels the member created that received an Approve action, divided by those with any review action. |
| `avgConsensusAgreement` | Average agreement with other consensus labels, for labels the member created. |
| `avgBenchmarkAgreement` | Average agreement with benchmark labels, for labels the member created. |
| `avgRating` | The member's average rating on the project, or `null` if they have none. |
| `isRemoved` | `true` when the member is no longer on the project. |

### Reviewer fields

| Field | Description |
| - | - |
| `userId`, `email` | The member. |
| `memberType` | `internal` or `external`, as for labelers. |
| `labelsReviewed` | Labels that had review time from the member in the period. |
| `labelsReworked` | Labels that had rework time from the member in the period. |
| `memberTotalTime` | Total time the member spent in the editor during the period. |
| `reviewTime` | Total review time spent by the member. |
| `reworkTime` | Total rework time spent by the member. |
| `avgReviewTime` | Total review time divided by the number of labels reviewed. |
| `avgReworkTime` | Total rework time divided by the number of labels reworked. |
| `reworkPercentage` | Labels reviewed by the member that were reworked, divided by the labels they reviewed. |
| `approvalPercentage` | Labels with an Approve action by the member, divided by labels with any review action by the member. |
| `isRemoved` | `true` when the member is no longer on the project. |

## Download the report

Get a link to the project's performance report as a file. This is the file the Download button on the Performance dashboard gives you.

<CodeGroup>
  ```python Python theme={"theme":"css-variables"}
  report = project.get_performance_report_download("2026-01-01", "2026-01-31")
  print(report.url)
  ```

  ```bash cURL theme={"theme":"css-variables"}
  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/projects/$PROJECT_ID/download?startDate=2026-01-01&endDate=2026-01-31"
  ```
</CodeGroup>

The response has the `url` and `expiresInSeconds`. The link works for 24 hours.

<Warning>
  Anyone who has the link can open the file until it expires. Treat it like the data it points to.
</Warning>

## Workspace reports

Workspace reports are the Monitor's data. They cover every project your organization owns and every project another organization shares with yours, or only the projects you name in `projectIds`.

<CodeGroup>
  ```python Python theme={"theme":"css-variables"}
  series = client.get_workspace_performance_metric(
      "labels_created", "2026-01-01", "2026-01-31", interval="week"
  )

  rows = client.get_workspace_labeler_performance(
      "2026-01-01", "2026-01-31", sort_by="labels_created", descending=True
  )
  print(rows.total)
  for row in rows:
      print(row.project_name, row.email, row.member_type, row.labels_created)
  ```

  ```bash cURL theme={"theme":"css-variables"}
  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/workspace/metrics/labels_created?startDate=2026-01-01&endDate=2026-01-31&interval=week"

  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/workspace/labelers?startDate=2026-01-01&endDate=2026-01-31&sort=labelsCreated&order=desc"
  ```
</CodeGroup>

`workspace/labelers` returns one row for each labeler on each project they worked on in the period. A row has the [labeler fields](#labeler-fields), without `avgRating`, plus:

| Field | Description |
| - | - |
| `projectId`, `projectName` | The project. `projectName` is `null` for a project that can no longer be found. |
| `projectOrganizationId`, `projectOrganizationName` | The organization that owns the project. It is not yours for a project shared with you. Both are `null` for a project that can no longer be found. |
| `isProjectDeleted` | `true` when the project has been deleted. Its rows and figures stay in the report. |

The table is [paged](#paging) like the project tables. Without `sort`, rows are ordered by project, then member.

Workspace reports do not take a user group filter.

## Shared projects and workforce providers

A project belongs to one organization and can be shared with another, for example when you add a workforce provider to it. The provider's members then work in your project, and the project appears in the provider's workspace as a shared project. The API reflects both sides of that connection.

**What a workspace report covers.** The projects your organization owns, and the projects shared with it. Two fields tell them apart on every row of `workspace/labelers`:

* `projectOrganizationId` is the organization that owns the project.
* `memberType` is whose member did the work: `internal` for a member of your organization, `external` for a member of another organization or an Alignerr member. The Monitor calls external members **Workforce members**.

Both are told from your side. To a workforce provider its own labelers are `internal`, and a customer's project has the customer's `projectOrganizationId`. Alignerr members are the exception: they are `external` to everyone, including to Alignerr's own organization.

**Split a report.** Use `memberType` and `ownerOrganizationIds` on either workspace report. These are the Monitor's **Member Type** and **Owned by** filters.

<CodeGroup>
  ```python Python theme={"theme":"css-variables"}
  # Labels created each week by your workforce provider's members
  external = client.get_workspace_performance_metric(
      "labels_created",
      "2026-01-01",
      "2026-01-31",
      interval="week",
      member_type="external",
  )

  # The same for your own members
  internal = client.get_workspace_performance_metric(
      "labels_created",
      "2026-01-01",
      "2026-01-31",
      interval="week",
      member_type="internal",
  )

  # As a workforce provider: your members' totals on one customer's projects
  rows = client.get_workspace_labeler_performance(
      "2026-01-01",
      "2026-01-31",
      owner_organization_ids=["<CUSTOMER_ORGANIZATION_ID>"],
      member_type="internal",
  )
  ```

  ```bash cURL theme={"theme":"css-variables"}
  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/workspace/metrics/labels_created?startDate=2026-01-01&endDate=2026-01-31&interval=week&memberType=external"

  curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
    "https://api.labelbox.com/api/v1/performance/workspace/labelers?startDate=2026-01-01&endDate=2026-01-31&ownerOrganizationIds=$CUSTOMER_ORGANIZATION_ID&memberType=internal"
  ```
</CodeGroup>

A few rules apply:

* **A split covers members and projects with activity in the period.** As on the Monitor, `memberType` and `ownerOrganizationIds` select from the members and projects that have rows in the report for your dates and other filters. If none match, every bucket is zero. It is never the unfiltered report.
* **A split by owner leaves out projects that are gone.** A project that can no longer be found has no owner, so `ownerOrganizationIds` never selects it, and the splits by owner can add up to slightly less than the whole workspace.
* **A split by owner leaves out projects you cannot report on.** Your workspace can include a project you have no role in. A metric split by `ownerOrganizationIds` does not count it, and lists it in `skippedProjectIds` so that you know.
* **A split of a metric needs permission to view everyone's performance.** With `projectIds`, `memberType` and `ownerOrganizationIds` on `workspace/metrics` need permission to view all performance in every project you name. Otherwise the request returns `403`. On `workspace/labelers` they filter the rows you can already see.
* **Project reports carry `memberType` too.** The labeler and reviewer tables of a project give each member's `memberType`.
* **Names and emails are masked between a customer and a workforce provider.** See [Access and privacy](#access-and-privacy). IDs are never masked, so you can always group and filter by `userId`, `projectId`, and `projectOrganizationId`.
* **User groups belong to the project's organization.** A `userGroupIds` filter on a project shared with you matches no work.

## Paging

The three tables (`projects/{projectId}/labelers`, `projects/{projectId}/reviewers`, and `workspace/labelers`) are paged. In the Python SDK, iterating fetches one page at a time, and `total` gives the number of rows without reading them all:

```python Python theme={"theme":"css-variables"}
rows = client.get_workspace_labeler_performance(
    "2026-01-01", "2026-01-31", page_size=100
)
print(rows.total)
for row in rows:  # fetches page after page as you go
    ...
```

In a request, use these parameters:

| Parameter | Description |
| - | - |
| `page` | Page number, starting at 1. Written in digits only. |
| `perPage` | Rows per page, up to 100. The default is 50. In the SDK, `page_size`. |
| `sort` | A field of the row, such as `labelsCreated`. In the SDK, `sort_by="labels_created"`. |
| `order` | `asc` (default) or `desc`. In the SDK, `descending=True`. |
| `useCache` | `false` reads the report again instead of using rows read in the last few minutes. In the SDK, `use_cache=False`. Not available on reviewers. |

Every response has `page`, `perPage`, and `total`, the number of rows across all pages. Request pages until you have `total` rows. A page past the end returns an empty `data`.

```bash cURL theme={"theme":"css-variables"}
curl -H "Authorization: Bearer $LABELBOX_API_KEY" \
  "https://api.labelbox.com/api/v1/performance/workspace/labelers?startDate=2026-01-01&endDate=2026-01-31&perPage=100&page=2"
```

Keep these in mind when you read a whole table:

* **Leave the cache on.** The pages of `workspace/labelers` are cut from one reading of the report, kept for five minutes, so they agree with each other and the rows are in a fixed order. `cachedAt` says when that reading was taken. With `useCache=false` every page reads the report again.
* **Sorting.** Every table sorts by the figures in its rows, such as `labelsCreated` or `avgTimePerLabel`, except `avgRating`. Without `sort`, the project tables come ordered by email, as on the Performance dashboard, and rows that tie under a sort are in that order too. A masked email keeps the place of the real one. `workspace/labelers` comes ordered by project, then member.
* **Metrics are not paged.** A metric returns at most one bucket per day of the period, and the period is limited to 366 days.

## Access and privacy

The API shows you what the pages show you.

* **Your own performance only.** If you can only view your own performance on a project, every project report returns only your data, whatever `userIds` you pass.
* **Workspace reports.** Without `projectIds`, you need permission to view performance across your organization. With `projectIds`, you need performance access to each project you name.
* **Projects you cannot see.** A project you have no performance access to returns `404`, the same as a project that does not exist.
* **Splitting by member type or owner.** On `workspace/metrics`, `memberType` and `ownerOrganizationIds` need permission to view all performance in every project you name in `projectIds`. A project found through its owner that you cannot report on is left out and listed in `skippedProjectIds`.
* **Masked emails.** The email of a member you may not identify is replaced with a masked value, as on the pages. For example, a workforce provider's members appear masked to the project's owner, and the owner's members appear masked to the provider.
* **Masked project and organization names.** A workforce provider does not see the names of the projects shared with it or of the organizations that own them. In `workspace/labelers`, `projectName` and `projectOrganizationName` are replaced with masked values for those projects. The IDs are unchanged.
* **Restricted API keys.** A key created with a limited set of permissions needs a performance permission, and is limited to the ones it has. A key that only has permission to view your own performance returns only your own.

## Data freshness

The API reads the same data as the Performance dashboard and the Monitor, so it is never more current than those pages. That data is rebuilt periodically, not continuously: the current day's figures can trail real activity by anything from a quarter of an hour to over an hour. A response served from the cache can be up to five minutes older again. `generatedAt` is when the response was produced. `cachedAt`, on the labeler tables, is set when the rows came from a result computed in the last few minutes. Neither tells you how recent the underlying activity data is, and no response carries an as-of time for it.

Two things follow for the current day:

* **Today's bucket keeps growing.** Read settled days for totals you will not revisit.
* **Charts and tables do not refresh together.** For settled days a metric and the labeler table normally add up to the same total. For the current day they can differ until both have caught up.

## Limits

| Limit | Value |
| - | - |
| Period | 366 days per request |
| `userIds`, `batchIds` | 100 IDs each |
| `userGroupIds` | 25 groups, with at most 5,000 members between them |
| `projectIds` | 50 projects |
| `ownerOrganizationIds` | 100 organizations |
| `perPage` | 100 rows |

Requests count toward your [API rate limit](/horizon/guides/limits), like the rest of the Labelbox API.

## Reliability

* **Requests are safe to repeat.** Every endpoint is a `GET` and changes nothing, so you can retry any request.
* **Retry `5xx` responses and timeouts** with a growing delay between attempts. The Python SDK does this for you, for up to four minutes, and tries once more after a request that timed out. When it stops retrying, it raises the error that kept happening.
* **Do not retry `4xx` responses** without changing the request. For `429`, wait before sending more.
* **Allow for long requests.** Reports are computed when you ask, and a long period over a large workspace can take a while. The Python SDK waits up to 120 seconds for a response.
* **Prefer fewer, wider requests.** One request for a month at `interval=day` costs less than a request for each day. To read every member's totals for a project as a file, use [Download the report](#download-the-report).
* **Expect new fields.** Responses can gain fields over time. Ignore the ones you do not know. The Python SDK does.

## Errors

| Status | Meaning | Python SDK exception |
| - | - | - |
| `400` | A parameter is invalid. The response lists each one in `details`. | `InvalidQueryError` |
| `401` | The API key is missing or invalid. | `AuthenticationError` |
| `403` | Your role or API key does not include the permission, or you named a project in a split by member type or owner where you cannot view everyone's performance. | `AuthorizationError` |
| `404` | The metric does not exist, or the project does not exist or is not visible to you. | `ResourceNotFoundError` |
| `429` | You have made too many requests. | `ApiLimitError` |
| `500` | The report could not be produced. Try again. | `InternalServerError` |
| `503` | Performance reports are under maintenance. Try again later. | `InternalServerError` |

The Python SDK raises `TimeoutError` when a request gets no response in time, and `NetworkError` when it cannot connect. All of these exceptions are in `lbox.exceptions`. It raises Python's `ValueError`, before sending anything, for a filter given as an empty list.

A `400` response names each parameter that was refused:

```json theme={"theme":"css-variables"}
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid query parameters",
  "details": [
    { "parameter": "endDate", "message": "must not be before startDate" }
  ]
}
```
