---
title: Telemetry API
description: Telemetry API reference of the dltHub Runtime
keywords: [dlthub, runtime, rest api, openapi, telemetry-api]
---

<!-- generated from the Telemetry Service OpenAPI spec, do not edit -->

Telemetry of a workspace's pipelines: pipeline runs and their traces, the results job runs declare, and per-pipeline and per-dataset statistics.

## Base URL and authentication

Each workspace keeps its data on the data plane of its region. Its services are not on
`api.dlthub.com` and do not accept API keys. To call them:

1. Read `dataplane_url` from `GET https://api.dlthub.com/api/v1/workspaces/{workspace_id}`.
   This is the base URL of every data plane service: Dataplane, Logs and Telemetry API.
2. Exchange your API key for a data plane token with
   `POST https://api.dlthub.com/api/v1/workspaces/{workspace_id}/-issue-dataplane-token`.
   The token is valid for one workspace until `expires_at`, currently 10 minutes after it
   was issued; request a new one before then.
3. Call the service at `dataplane_url` with `Authorization: Bearer <token>`.

Every workspace member can read logs, telemetry and file lists. Only workspace owners can read
or change variables, so workspace API keys, which are DEVELOPER or VIEWER, cannot. Uploads use
a separate token: `POST .../deployments/-issue-upload` (or `/configurations/-issue-upload`)
returns an `upload_token` and the `upload_url` to send the files to, both valid for one upload
and 10 minutes.

The data planes today:

| Region | `dataplane_url` |
| --- | --- |
| Europe (`eu1`, europe-west4) | `https://eu1.dlthub.com` |
| US East (`us1`, us-east4) | `https://us1.dlthub.com` |

## Versioning

The API version is part of the path. Endpoints under `/v1` are stable: breaking changes ship
only under a new version prefix. New endpoints, optional parameters and response fields can
be added to `/v1` at any time, so ignore fields and enum values you do not know.

## Endpoints

### List dataset overview {#list_dataset_stats}

```text
GET /telemetry/v1/workspaces/{workspace_id}/datasets/-/stats
```

One row per dataset, aggregated over the pipeline runs whose `started_at` falls in the
time range `[start, end)`. Most recently run first by default.

Datasets are grouped by name across destinations. Runs without a dataset, for example
ones that failed before loading, are not counted.

Search with `q` over the dataset name, filter on what a row displays, and sort
with paired `sort` and `order` lists.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |

**Query parameters**

| Name | Type | Description |
| --- | --- | --- |
| `start` | string (date-time) or null | Lower bound (inclusive) on `started_at`, as a naive local time read in `timezone`, e.g. `2026-03-01T00:00:00`. Defaults to 7 days before `end`. |
| `end` | string (date-time) or null | Upper bound (exclusive) on `started_at`, as a naive local time read in `timezone`, e.g. `2026-03-11T00:00:00`. Defaults to now. |
| `timezone` | string | IANA timezone in which `start` and `end` are read, e.g. `Europe/Berlin`. Default UTC. |
| `q` | string or null | Case-insensitive substring match on `dataset_name`. |
| `dataset_name` | array of string or null | Exact match on `dataset_name`. Repeat the parameter to match any of several values. |
| `latest_status` | array of string or null | Exact match on `latest_status`. Repeat the parameter to match any of several values. One of: `succeeded`, `failed`. |
| `latest_destination_name` | array of string or null | Exact match on `latest_destination_name`. Repeat the parameter to match any of several values. |
| `sort` | array of string or null | Keys to sort by, applied in the order given. Pairs positionally with `order`, which must have the same number of entries. One of: `latest_pipeline_run_at`, `total_pipeline_run_count`, `success_ratio`, `average_duration_ms`, `total_rows_loaded`, `dataset_name`. |

**Pagination:** `limit`, `offset`, `order`, `cursor`, see [Lists and pagination](overview.md#lists-and-pagination)

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/datasets/-/stats" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/datasets/-/stats",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "items": [
    {
      "average_duration_ms": 0,
      "dataset_name": "<dataset_name>",
      "distinct_pipeline_count": 0,
      "latest_destination_name": "<latest_destination_name>",
      "latest_pipeline_name": "<latest_pipeline_name>",
      "latest_pipeline_run_at": "2026-01-01T00:00:00Z",
      "latest_pipeline_run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "latest_status": "succeeded",
      "schema_migration_count": 0,
      "succeeded_pipeline_run_count": 0,
      "success_ratio": 0,
      "table_count": 0,
      "total_bytes_loaded": 0,
      "total_pipeline_run_count": 0,
      "total_rows_loaded": 0
    }
  ],
  "limit": 0,
  "next_cursor": "<next_cursor>",
  "offset": 0,
  "total": 0
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `items` | array of object | The rows of this page, in sort order. |
| `items[].average_duration_ms` | integer or null | Mean `duration_ms` in the range, rounded; `null` when no run has a duration. |
| `items[].dataset_name` | string | Name of the dataset, across destinations. |
| `items[].distinct_pipeline_count` | integer | Number of distinct pipelines that ran into this dataset in the range. |
| `items[].latest_destination_name` | string or null | Destination of the most recent pipeline run in the time range. |
| `items[].latest_pipeline_name` | string or null | Pipeline of the most recent pipeline run in the time range. |
| `items[].latest_pipeline_run_at` | string (date-time) or null | When the most recent pipeline run in the time range started. |
| `items[].latest_pipeline_run_id` | string (uuid) or null | ID of the most recent pipeline run in the time range. |
| `items[].latest_status` | string or null | Status of the most recent pipeline run in the time range. One of: `succeeded`, `failed`. |
| `items[].schema_migration_count` | integer | Runs in the range with `has_schema_migration`. |
| `items[].succeeded_pipeline_run_count` | integer | Pipeline runs in the range with status `succeeded`. |
| `items[].success_ratio` | number | Succeeded ÷ total, from 0 to 1. |
| `items[].table_count` | integer | Distinct user tables ever seen in a dataset of this name (all time, `_dlt_*` excluded). Default: `0`. |
| `items[].total_bytes_loaded` | integer | Sum of normalized bytes (size of the normalized files) over succeeded runs in the time range. |
| `items[].total_pipeline_run_count` | integer | Pipeline runs in the range. |
| `items[].total_rows_loaded` | integer | Sum of normalized rows over succeeded runs in the time range. |
| `limit` | integer | The page size this page was fetched with. |
| `next_cursor` | string or null | Cursor for the page after this one; null on the last. |
| `offset` | integer | How many rows were skipped before this page. |
| `total` | integer | Rows matching the filters, reported up to 10001. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### Get job result {#get_job_run_result}

```text
GET /telemetry/v1/workspaces/{workspace_id}/job-runs/{job_run_id}/result
```

The result a job run declared: for an agent, its status, summary, token usage and cost;
for other jobs, the declared `result`. A run has at most one.

`job_run_id` is the job run's id from the runtime API, not a `pipeline_run_id`. Answers 404
when the run declared no result. For the agent's per-turn tool calls and everything else the
job delivered, read `/result-trace`.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |
| `job_run_id` | string (uuid) | ID of the job run |

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/job-runs/$JOB_RUN_ID/result" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/job-runs/<job_run_id>/result",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "agent_status": "<agent_status>",
  "cost_usd": "<cost_usd>",
  "created_at": "2026-01-01T00:00:00Z",
  "engine_version": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "input_tokens": 0,
  "job_ref": "<job_ref>",
  "job_run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "loop_type": "<loop_type>",
  "model": "<model>",
  "output_tokens": 0,
  "result": "<result>",
  "result_type": "<result_type>",
  "stop_reason": "<stop_reason>",
  "summary": "<summary>",
  "total_tokens": 0,
  "turn_count": 0,
  "updated_at": "2026-01-01T00:00:00Z"
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `agent_status` | string or null | The agent's self-report, which may legitimately disagree with the run's own status: a failed agent inside a succeeded run is normal. |
| `cost_usd` | string or null | Cost in USD the agent reported, as a decimal string. `null` when the result carries no agent trace, as for non-agent jobs. |
| `created_at` | string (date-time) | When the result was first stored, UTC. |
| `engine_version` | integer | Result format version; 1 when not declared. |
| `id` | string (uuid) | Id of the job result. |
| `input_tokens` | integer or null | Input tokens the agent used. `null` when the result carries no agent trace, as for non-agent jobs. |
| `job_ref` | string | The job that declared the result. |
| `job_run_id` | string (uuid) | Job run that declared the result. |
| `loop_type` | string or null | Agent loop that ran. `null` when the result carries no agent trace, as for non-agent jobs. |
| `model` | string or null | Model the agent ran with. `null` when the result carries no agent trace, as for non-agent jobs. |
| `output_tokens` | integer or null | Output tokens the agent used. `null` when the result carries no agent trace, as for non-agent jobs. |
| `result` | any | The job's declared output. Shape is job-defined. |
| `result_type` | string | `job.{category}.{name}`, for example `job.background_agent.{name}`. |
| `stop_reason` | string or null | Why the agent stopped. `null` when the result carries no agent trace, as for non-agent jobs. |
| `summary` | string or null | Model-authored markdown, unbounded. Untrusted — render as user content. |
| `total_tokens` | integer or null | Total tokens the agent used. `null` when the result carries no agent trace, as for non-agent jobs. |
| `turn_count` | integer or null | Number of turns the agent took. `null` when the result carries no agent trace, as for non-agent jobs. |
| `updated_at` | string (date-time) | When the result was last stored, UTC. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">404</code> Not Found
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### Get job result trace {#get_job_run_result_trace}

```text
GET /telemetry/v1/workspaces/{workspace_id}/job-runs/{job_run_id}/result-trace
```

The full result exactly as the job delivered it, including the agent's per-turn tool calls.
Fields beyond `type` and `job_ref` are passed through unchanged. `job_run_id` is the job run's
id from the runtime API, not a `pipeline_run_id`. Answers 404 when the run declared no result.

Results over 8 MiB answer 413 `job_result_trace_too_large`; the summary fields stay
available at `/result`.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |
| `job_run_id` | string (uuid) | ID of the job run |

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/job-runs/$JOB_RUN_ID/result-trace" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/job-runs/<job_run_id>/result-trace",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "job_ref": "<job_ref>",
  "type": "<type>"
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `job_ref` | string | The job that declared the result. |
| `type` | string | The result type, `job.{category}.{name}`; `job.background_agent.{name}` for an agent. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">404</code> Not Found
- <code className="api-error">413</code> Request Entity Too Large
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### List pipeline runs {#list_pipeline_runs}

```text
GET /telemetry/v1/workspaces/{workspace_id}/pipeline-runs
```

Lists the pipeline runs of a workspace whose `started_at` falls in the time range
`[start, end)`, newest first by default.

Search with `q` over the pipeline name; filter by status, pipeline, dataset,
destination, job run, or empty run; sort with paired `sort` and `order` lists.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |

**Query parameters**

| Name | Type | Description |
| --- | --- | --- |
| `start` | string (date-time) or null | Lower bound (inclusive) on `started_at`, as a naive local time read in `timezone`, e.g. `2026-03-01T00:00:00`. Defaults to 7 days before `end`. |
| `end` | string (date-time) or null | Upper bound (exclusive) on `started_at`, as a naive local time read in `timezone`, e.g. `2026-03-11T00:00:00`. Defaults to now. |
| `timezone` | string | IANA timezone in which `start` and `end` are read, e.g. `Europe/Berlin`. Default UTC. |
| `q` | string or null | Case-insensitive substring match on `pipeline_name`. |
| `status` | array of string or null | Exact match on `status`. Repeat the parameter to match any of several values. One of: `succeeded`, `failed`. |
| `pipeline_name` | array of string or null | Exact match on `pipeline_name`. Repeat the parameter to match any of several values. |
| `dataset_name` | array of string or null | Exact match on `dataset_name`. Repeat the parameter to match any of several values. |
| `destination_name` | array of string or null | Exact match on `destination_name`. Repeat the parameter to match any of several values. |
| `job_run_id` | array of string (uuid) or null | Exact match on `job_run_id`. Repeat the parameter to match any of several values. |
| `is_empty_run` | boolean or null | Match `is_empty_run`. Omit to list both. |
| `sort` | array of string or null | Keys to sort by, applied in the order given. Pairs positionally with `order`, which must have the same number of entries. One of: `started_at`, `duration_ms`, `total_rows_loaded`, `total_bytes_loaded`, `finished_at`. |

**Pagination:** `limit`, `offset`, `order`, `cursor`, see [Lists and pagination](overview.md#lists-and-pagination)

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/pipeline-runs" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/pipeline-runs",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "items": [
    {
      "created_at": "2026-01-01T00:00:00Z",
      "dataset_name": "<dataset_name>",
      "destination_name": "<destination_name>",
      "dlt_version": "<dlt_version>",
      "duration_ms": 0,
      "error_message": "<error_message>",
      "error_step": "<error_step>",
      "finished_at": "2026-01-01T00:00:00Z",
      "has_schema_migration": false,
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "pipeline_name": "<pipeline_name>",
      "started_at": "2026-01-01T00:00:00Z",
      "status": "succeeded",
      "total_bytes_loaded": 0,
      "total_rows_extracted": 0,
      "total_rows_loaded": 0,
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ],
  "limit": 0,
  "next_cursor": "<next_cursor>",
  "offset": 0,
  "total": 0
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `items` | array of object | The rows of this page, in sort order. |
| `items[].created_at` | string (date-time) | When this run was first stored from telemetry, UTC. |
| `items[].dataset_name` | string or null | Dataset the run loaded into, from its load step; `null` when the run did not reach load. |
| `items[].destination_name` | string or null | Destination the run loaded to, from its load step; `null` when the run did not reach load. |
| `items[].dlt_version` | string or null | dlt version that ran the pipeline, from the trace's execution context. |
| `items[].duration_ms` | integer or null | `finished_at` − `started_at` in milliseconds. |
| `items[].error_message` | string or null | Exception message of that step. |
| `items[].error_step` | string or null | First step that raised, for example `extract`, `normalize` or `load`. |
| `items[].finished_at` | string (date-time) or null | End of the run from the dlt trace, UTC. |
| `items[].has_schema_migration` | boolean or null | True when the load step created or changed user tables (`_dlt_*` tables are ignored). |
| `items[].id` | string (uuid) | ID of the pipeline run; not a job run ID. |
| `items[].pipeline_name` | string | Name of the dlt pipeline. |
| `items[].started_at` | string (date-time) or null | Start of the run from the dlt trace, UTC. |
| `items[].status` | string | `failed` when any step raised, otherwise `succeeded`. One of: `succeeded`, `failed`. |
| `items[].total_bytes_loaded` | integer | Bytes that dlt prepared for loading in this run (size of the normalized files). dlt does not report per-row load counts, so this is set even when loading failed. Default: `0`. |
| `items[].total_rows_extracted` | integer | Rows written by the extract step. Default: `0`. |
| `items[].total_rows_loaded` | integer | Rows that dlt prepared for loading in this run (normalized rows). dlt does not report per-row load counts, so this is set even when loading failed. Default: `0`. |
| `items[].updated_at` | string (date-time) | When this run was last updated from telemetry, UTC. |
| `limit` | integer | The page size this page was fetched with. |
| `next_cursor` | string or null | Cursor for the page after this one; null on the last. |
| `offset` | integer | How many rows were skipped before this page. |
| `total` | integer | Rows matching the filters, reported up to 10001. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### Get pipeline run {#get_pipeline_run}

```text
GET /telemetry/v1/workspaces/{workspace_id}/pipeline-runs/{pipeline_run_id}
```

One pipeline run (one dlt `pipeline.run()`), looked up by its `id`, with its per-table and
per-resource metrics and its load packages. Answers 404 when it is not in the workspace.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |
| `pipeline_run_id` | string (uuid) | ID of the pipeline run |

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/pipeline-runs/$PIPELINE_RUN_ID" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/pipeline-runs/<pipeline_run_id>",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "created_at": "2026-01-01T00:00:00Z",
  "dataset_name": "<dataset_name>",
  "destination_name": "<destination_name>",
  "dlt_plus_version": "<dlt_plus_version>",
  "dlt_version": "<dlt_version>",
  "duration_ms": 0,
  "error_message": "<error_message>",
  "error_step": "<error_step>",
  "error_traceback": "<error_traceback>",
  "extract_duration_ms": 0,
  "finished_at": "2026-01-01T00:00:00Z",
  "has_failed_load_jobs": false,
  "has_pending_packages": false,
  "has_schema_migration": false,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "is_empty_run": false,
  "job_run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "load_duration_ms": 0,
  "load_package_count": 0,
  "load_packages": [],
  "normalize_duration_ms": 0,
  "os_name": "<os_name>",
  "os_version": "<os_version>",
  "pipeline_name": "<pipeline_name>",
  "python_version": "<python_version>",
  "resources": [],
  "started_at": "2026-01-01T00:00:00Z",
  "status": "succeeded",
  "tables": [],
  "total_bytes_extracted": 0,
  "total_bytes_loaded": 0,
  "total_rows_extracted": 0,
  "total_rows_loaded": 0,
  "transaction_id": "<transaction_id>",
  "updated_at": "2026-01-01T00:00:00Z"
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `created_at` | string (date-time) | When this run was first stored from telemetry, UTC. |
| `dataset_name` | string or null | Dataset the run loaded into, from its load step; `null` when the run did not reach load. |
| `destination_name` | string or null | Destination the run loaded to, from its load step; `null` when the run did not reach load. |
| `dlt_plus_version` | string or null | Version of the `dlthub` package, from the trace's execution context; `null` when it was not installed. |
| `dlt_version` | string or null | dlt version that ran the pipeline, from the trace's execution context. |
| `duration_ms` | integer or null | `finished_at` − `started_at` in milliseconds. |
| `error_message` | string or null | Exception message of that step. |
| `error_step` | string or null | First step that raised, for example `extract`, `normalize` or `load`. |
| `error_traceback` | string or null | Stack trace of the step that raised. |
| `extract_duration_ms` | integer or null | Duration of the extract step in milliseconds; `null` when it did not run. |
| `finished_at` | string (date-time) or null | End of the run from the dlt trace, UTC. |
| `has_failed_load_jobs` | boolean or null | True when any load package has failed jobs. Default: `false`. |
| `has_pending_packages` | boolean or null | True when any load package is in a state other than `loaded`. Default: `false`. |
| `has_schema_migration` | boolean or null | True when the load step created or changed user tables (`_dlt_*` tables are ignored). |
| `id` | string (uuid) | ID of the pipeline run; not a job run ID. |
| `is_empty_run` | boolean or null | True when the run did not fail and extract produced no load packages. Default: `false`. |
| `job_run_id` | string (uuid) or null | Job run that sent this run's telemetry. |
| `load_duration_ms` | integer or null | Duration of the load step in milliseconds; `null` when it did not run. |
| `load_package_count` | integer or null | Number of load packages reported by the last step the run reached. Default: `0`. |
| `load_packages` | array of object | Load packages of the run. Default: `[]`. |
| `load_packages[].completed_at` | string (date-time) or null | When dlt completed the package, UTC; `null` while not completed. |
| `load_packages[].completed_job_count` | integer or null | Jobs that completed. Default: `0`. |
| `load_packages[].failed_job_count` | integer or null | Jobs that failed. Default: `0`. |
| `load_packages[].has_failed_load_jobs` | boolean or null | True when any load job of the package failed. Default: `false`. |
| `load_packages[].has_schema_migration` | boolean or null | True when loading this package created or changed user tables (`_dlt_*` tables are ignored). Default: `false`. |
| `load_packages[].is_pending` | boolean or null | True when the package is not fully loaded yet. Default: `false`. |
| `load_packages[].job_count` | integer or null | Number of jobs in the package. Default: `0`. |
| `load_packages[].load_id` | string | dlt load id of the package. |
| `load_packages[].load_status` | string or null | State dlt reported for the package; `loaded` means fully loaded. One of: `new`, `extracted`, `normalized`, `loaded`, `aborted`. |
| `load_packages[].schema_hash` | string or null | Version hash of that schema. |
| `load_packages[].schema_name` | string or null | dlt schema the package was created with. |
| `load_packages[].total_bytes_loaded` | integer or null | Bytes that dlt prepared for loading in this package (size of the normalized files, or of the extracted files when normalize reported none). dlt does not report per-row load counts, so this is set even when loading failed. Default: `0`. |
| `load_packages[].total_rows_loaded` | integer or null | Rows that dlt prepared for loading in this package (normalized rows, or extracted rows when normalize reported none). dlt does not report per-row load counts, so this is set even when loading failed. Default: `0`. |
| `normalize_duration_ms` | integer or null | Duration of the normalize step in milliseconds; `null` when it did not run. |
| `os_name` | string or null | Operating system that ran the pipeline, from the trace's execution context. |
| `os_version` | string or null | Version of that operating system. |
| `pipeline_name` | string | Name of the dlt pipeline. |
| `python_version` | string or null | Python version that ran the pipeline, from the trace's execution context. |
| `resources` | array of object | One entry per extracted resource and load package. Default: `[]`. |
| `resources[].bytes_extracted` | integer or null | Size of the intermediate files the extract step wrote for this resource. |
| `resources[].parent_resource_name` | string or null | Resource this transformer reads from; `null` for a top-level resource. |
| `resources[].resource_name` | string | Name of the dlt resource. |
| `resources[].rows_extracted` | integer or null | Rows the extract step wrote for this resource. |
| `resources[].schema_name` | string | dlt schema of the resource's load package; empty when not reported. Default: `""`. |
| `resources[].write_disposition` | string or null | From the resource hints. One of: `append`, `replace`, `merge`. |
| `started_at` | string (date-time) or null | Start of the run from the dlt trace, UTC. |
| `status` | string | `failed` when any step raised, otherwise `succeeded`. One of: `succeeded`, `failed`. |
| `tables` | array of object | One entry per table and load package. Default: `[]`. |
| `tables[].bytes_extracted` | integer or null | Size of the intermediate files the extract step wrote for this table; `null` when not extracted. |
| `tables[].bytes_loaded` | integer or null | Bytes that dlt prepared for loading for this table (size of the normalized files). dlt does not report per-row load counts, so this is set even when loading failed. |
| `tables[].file_format` | string or null | Format of the file dlt loaded, for example `parquet`. |
| `tables[].load_duration_ms` | integer or null | Duration of the table's load job in milliseconds; `null` when the table was not loaded. |
| `tables[].load_job_state` | string or null | `completed` or `failed`; `null` when the table was not loaded. One of: `completed`, `failed`. |
| `tables[].rows_extracted` | integer or null | Rows the extract step wrote for this table; `null` when not extracted. |
| `tables[].rows_loaded` | integer or null | Rows that dlt prepared for loading for this table (normalized rows). dlt does not report per-row load counts, so this is set even when loading failed. |
| `tables[].schema_name` | string | dlt schema of the table's load package; empty when not reported. Default: `""`. |
| `tables[].table_name` | string | Name of the table. |
| `tables[].write_disposition` | string or null | From the resource hints; `null` when the extract step reported none. One of: `append`, `replace`, `merge`. |
| `total_bytes_extracted` | integer | Size of the intermediate files written by the extract step. Default: `0`. |
| `total_bytes_loaded` | integer | Bytes that dlt prepared for loading in this run (size of the normalized files). dlt does not report per-row load counts, so this is set even when loading failed. Default: `0`. |
| `total_rows_extracted` | integer | Rows written by the extract step. Default: `0`. |
| `total_rows_loaded` | integer | Rows that dlt prepared for loading in this run (normalized rows). dlt does not report per-row load counts, so this is set even when loading failed. Default: `0`. |
| `transaction_id` | string | dlt trace transaction id; one pipeline run per transaction. |
| `updated_at` | string (date-time) | When this run was last updated from telemetry, UTC. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">404</code> Not Found
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### Get pipeline run trace {#get_pipeline_run_trace}

```text
GET /telemetry/v1/workspaces/{workspace_id}/pipeline-runs/{pipeline_run_id}/trace
```

Returns the dlt trace of this pipeline run as dlt produced it: its extract, normalize and load steps and the execution context. Answers 404 when the run or its trace is not found.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |
| `pipeline_run_id` | string (uuid) | ID of the pipeline run |

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/pipeline-runs/$PIPELINE_RUN_ID/trace" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/pipeline-runs/<pipeline_run_id>/trace",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "execution_context": {},
  "pipeline_name": "<pipeline_name>",
  "steps": [],
  "transaction_id": "<transaction_id>"
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `execution_context` | map of any | The dlt, Python and host versions the pipeline ran with. |
| `pipeline_name` | string | The pipeline that ran. |
| `steps` | array of map of any | The extract, normalize and load steps, in order. |
| `transaction_id` | string | The dlt transaction the trace records. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">404</code> Not Found
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### List pipeline overview {#list_pipeline_stats}

```text
GET /telemetry/v1/workspaces/{workspace_id}/pipelines/-/stats
```

One row per pipeline, aggregated over the pipeline runs whose `started_at` falls in the
time range `[start, end)`. Most recently run first by default.

Search with `q` over the pipeline name, filter on what a row displays, and sort
with paired `sort` and `order` lists.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |

**Query parameters**

| Name | Type | Description |
| --- | --- | --- |
| `start` | string (date-time) or null | Lower bound (inclusive) on `started_at`, as a naive local time read in `timezone`, e.g. `2026-03-01T00:00:00`. Defaults to 7 days before `end`. |
| `end` | string (date-time) or null | Upper bound (exclusive) on `started_at`, as a naive local time read in `timezone`, e.g. `2026-03-11T00:00:00`. Defaults to now. |
| `timezone` | string | IANA timezone in which `start` and `end` are read, e.g. `Europe/Berlin`. Default UTC. |
| `q` | string or null | Case-insensitive substring match on `pipeline_name`. |
| `pipeline_name` | array of string or null | Exact match on `pipeline_name`. Repeat the parameter to match any of several values. |
| `latest_status` | array of string or null | Exact match on `latest_status`. Repeat the parameter to match any of several values. One of: `succeeded`, `failed`. |
| `latest_destination_name` | array of string or null | Exact match on `latest_destination_name`. Repeat the parameter to match any of several values. |
| `sort` | array of string or null | Keys to sort by, applied in the order given. Pairs positionally with `order`, which must have the same number of entries. One of: `latest_pipeline_run_at`, `total_pipeline_run_count`, `success_ratio`, `average_duration_ms`, `total_rows_loaded`, `pipeline_name`. |

**Pagination:** `limit`, `offset`, `order`, `cursor`, see [Lists and pagination](overview.md#lists-and-pagination)

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/pipelines/-/stats" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/pipelines/-/stats",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "items": [
    {
      "average_duration_ms": 0,
      "latest_dataset_name": "<latest_dataset_name>",
      "latest_destination_name": "<latest_destination_name>",
      "latest_pipeline_run_at": "2026-01-01T00:00:00Z",
      "latest_source_names": ["<latest_source_names>"],
      "latest_status": "succeeded",
      "pipeline_name": "<pipeline_name>",
      "succeeded_pipeline_run_count": 0,
      "success_ratio": 0,
      "total_bytes_loaded": 0,
      "total_duration_ms": 0,
      "total_pipeline_run_count": 0,
      "total_rows_extracted": 0,
      "total_rows_loaded": 0
    }
  ],
  "limit": 0,
  "next_cursor": "<next_cursor>",
  "offset": 0,
  "total": 0
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `items` | array of object | The rows of this page, in sort order. |
| `items[].average_duration_ms` | integer or null | Mean `duration_ms` in the range, rounded; `null` when no run has a duration. |
| `items[].latest_dataset_name` | string or null | Dataset of the most recent pipeline run in the time range. |
| `items[].latest_destination_name` | string or null | Destination of the most recent pipeline run in the time range. |
| `items[].latest_pipeline_run_at` | string (date-time) or null | When the most recent pipeline run in the time range started. |
| `items[].latest_source_names` | array of string or null | Sources read by the most recent pipeline run in the time range. |
| `items[].latest_status` | string or null | Status of the most recent pipeline run in the time range. One of: `succeeded`, `failed`. |
| `items[].pipeline_name` | string | Name of the dlt pipeline. |
| `items[].succeeded_pipeline_run_count` | integer | Pipeline runs in the range with status `succeeded`. |
| `items[].success_ratio` | number | Succeeded ÷ total, from 0 to 1. |
| `items[].total_bytes_loaded` | integer | Sum of normalized bytes (size of the normalized files) over succeeded runs in the time range. |
| `items[].total_duration_ms` | integer | Sum of `duration_ms` over the runs in the range. Default: `0`. |
| `items[].total_pipeline_run_count` | integer | Pipeline runs in the range. |
| `items[].total_rows_extracted` | integer | Sum of extracted rows over all runs in the range. |
| `items[].total_rows_loaded` | integer | Sum of normalized rows over succeeded runs in the time range. |
| `limit` | integer | The page size this page was fetched with. |
| `next_cursor` | string or null | Cursor for the page after this one; null on the last. |
| `offset` | integer | How many rows were skipped before this page. |
| `total` | integer | Rows matching the filters, reported up to 10001. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>

### Get telemetry watermark {#get_workspace_watermark}

```text
GET /telemetry/v1/workspaces/{workspace_id}/watermark
```

The most recent `updated_at` of any pipeline run in the workspace, or `null` when there are
none. Poll this and refetch lists only when it changes.

<div class="api-details">
<details>
<summary>Request</summary>

<Tabs groupId="api-request">
  <TabItem value="parameters" label="Parameters">

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `workspace_id` | string (uuid) | ID of the workspace |

  </TabItem>
  <TabItem value="curl" label="curl">

```sh
curl "$DATAPLANE_URL/telemetry/v1/workspaces/$WORKSPACE_ID/watermark" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/telemetry/v1/workspaces/<workspace_id>/watermark",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
)
response.raise_for_status()
print(response.json())
```

  </TabItem>
</Tabs>

</details>
</div>

<div class="api-details">
<details>
<summary>Response (<code>200</code>, <code className="api-error">4xx</code>, <code className="api-error">5xx</code>)</summary>

<Tabs groupId="api-response">
  <TabItem value="example" label="Example">

```json
{
  "updated_at": "2026-01-01T00:00:00Z"
}
```

  </TabItem>
  <TabItem value="fields" label="Fields">

| Field | Type | Description |
| --- | --- | --- |
| `updated_at` | string (date-time) or null | When a pipeline run in the workspace was last recorded or updated; `null` when there are none. If it is older than the time range you read, empty results may mean telemetry is behind rather than that nothing ran. |

  </TabItem>
  <TabItem value="errors" label="Errors">

- <code className="api-error">400</code> Bad Request
- <code className="api-error">401</code> Unauthorized
- <code className="api-error">403</code> Forbidden
- <code className="api-error">500</code> Internal Server Error

Each error returns the [error body](overview.md#errors).

  </TabItem>
</Tabs>

</details>
</div>
