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

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

Workspace data kept on the data plane: workspace variables and the files of uploaded deployments and configurations.

## 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.

## Files

### Get configuration files manifest {#get_configuration_files}

```text
GET /api/v1/workspaces/{workspace_id}/configurations/{configuration_id}/files
```

Lists every file in an uploaded configuration: path, size and SHA3-256 digest. Any workspace member can call it. Answers 404 when the workspace has no configuration with this id. To get the latest configuration's id, call `GET https://api.dlthub.com/api/v1/workspaces/{workspace_id}/configurations/latest`.

<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 |
| `configuration_id` | string (uuid) | ID of the configuration |

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

```sh
curl "$DATAPLANE_URL/api/v1/workspaces/$WORKSPACE_ID/configurations/$CONFIGURATION_ID/files" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

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

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/api/v1/workspaces/<workspace_id>/configurations/<configuration_id>/files",
    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
{
  "engine_version": 0,
  "files": [
    {
      "linkname": "<linkname>",
      "relative_path": "<relative_path>",
      "sha3_256": "<sha3_256>",
      "size_bytes": 0
    }
  ]
}
```

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

| Field | Type | Description |
| --- | --- | --- |
| `engine_version` | integer | Version of the manifest format. |
| `files` | array of object | The files in the package, sorted by `relative_path`. |
| `files[].linkname` | string or null | Symlink target; set for symlink entries only. |
| `files[].relative_path` | string | Path of the file inside the package, with `/` separators: relative to the workspace root for a deployment, and to the `.dlt` settings folder for a configuration. |
| `files[].sha3_256` | string | Base64 SHA3-256 of the file contents, or of the `linkname` string for a symlink. |
| `files[].size_bytes` | integer | Size of the file in bytes; 0 for a symlink. |

  </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 deployment files manifest {#get_deployment_files}

```text
GET /api/v1/workspaces/{workspace_id}/deployments/{deployment_id}/files
```

Lists every file in an uploaded deployment: path, size and SHA3-256 digest. Any workspace member can call it. Answers 404 when the workspace has no deployment with this id. To get the latest deployment's id, call `GET https://api.dlthub.com/api/v1/workspaces/{workspace_id}/deployments/latest`.

<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 |
| `deployment_id` | string (uuid) | ID of the deployment |

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

```sh
curl "$DATAPLANE_URL/api/v1/workspaces/$WORKSPACE_ID/deployments/$DEPLOYMENT_ID/files" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN"
```

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

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/api/v1/workspaces/<workspace_id>/deployments/<deployment_id>/files",
    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
{
  "engine_version": 0,
  "files": [
    {
      "linkname": "<linkname>",
      "relative_path": "<relative_path>",
      "sha3_256": "<sha3_256>",
      "size_bytes": 0
    }
  ]
}
```

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

| Field | Type | Description |
| --- | --- | --- |
| `engine_version` | integer | Version of the manifest format. |
| `files` | array of object | The files in the package, sorted by `relative_path`. |
| `files[].linkname` | string or null | Symlink target; set for symlink entries only. |
| `files[].relative_path` | string | Path of the file inside the package, with `/` separators: relative to the workspace root for a deployment, and to the `.dlt` settings folder for a configuration. |
| `files[].sha3_256` | string | Base64 SHA3-256 of the file contents, or of the `linkname` string for a symlink. |
| `files[].size_bytes` | integer | Size of the file in bytes; 0 for a symlink. |

  </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>

## Uploads

### Upload configuration bytes {#upload_configuration}

```text
POST /api/v1/workspaces/{workspace_id}/configurations/{configuration_id}/-upload
```

Uploads the configuration package for the id you got from `configurations/-issue-upload`, using that call's `upload_token`. Profiles are read from `<profile>.config.toml` and `<profile>.secrets.toml` file names. On success the configuration is created and returned. A package that cannot be read answers 400; a request body over 10 MB answers 413.

<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 |
| `configuration_id` | string (uuid) | ID of the configuration |

  </TabItem>
  <TabItem value="body" label="Body">

Sent as `multipart/form-data`.

| Field | Type | Description |
| --- | --- | --- |
| `file` | string (binary) |  |

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

```sh
curl -X POST "$DATAPLANE_URL/api/v1/workspaces/$WORKSPACE_ID/configurations/$CONFIGURATION_ID/-upload" \
  -H "Authorization: Bearer $UPLOAD_TOKEN" \
  -F "file=@<file>"
```

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

```python
import os

import requests

response = requests.post(
    "<dataplane_url>/api/v1/workspaces/<workspace_id>/configurations/<configuration_id>/-upload",
    headers={"Authorization": f"Bearer {os.environ['UPLOAD_TOKEN']}"},
    files={"file": open("<file>", "rb")},
)
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
{
  "content_hash": "<content_hash>",
  "created_at": "2026-01-01T00:00:00Z",
  "created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "file_count": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "profiles": ["<profiles>"],
  "size_bytes": 0,
  "updated_at": "2026-01-01T00:00:00Z",
  "version": 0,
  "workspace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```

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

| Field | Type | Description |
| --- | --- | --- |
| `content_hash` | string | Hash of the package contents: base64 SHA3-256 over each file's path and digest, as dlt computes when building the package; identical file sets give identical hashes. |
| `created_at` | string (date-time) | When it was created. |
| `created_by` | string (uuid) | The `organization_membership_id` of the member who uploaded it. |
| `file_count` | integer | Number of files in the package. |
| `id` | string (uuid) | Unique ID (a UUID). |
| `profiles` | array of string | Profile names found in `<profile>.config.toml` / `<profile>.secrets.toml` file names, sorted. |
| `size_bytes` | integer | Size of the uploaded package in bytes. |
| `updated_at` | string (date-time) | When it was last changed. |
| `version` | integer | Version number within the workspace, starting at 1 and incremented by each upload. |
| `workspace_id` | string (uuid) | The ID of the workspace it belongs to. |

  </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">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>

### Upload deployment bytes {#upload_deployment}

```text
POST /api/v1/workspaces/{workspace_id}/deployments/{deployment_id}/-upload
```

Uploads the package for the deployment id you got from `deployments/-issue-upload`, using that call's `upload_token`. `file` is the tar.gz package built by dlt (it must contain `manifest.yaml`). `requirements_file` is the workspace requirements manifest. On success the deployment is created and returned. A package that cannot be read answers 400; a request body over 10 MB answers 413.

<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 |
| `deployment_id` | string (uuid) | ID of the deployment |

  </TabItem>
  <TabItem value="body" label="Body">

Sent as `multipart/form-data`.

| Field | Type | Description |
| --- | --- | --- |
| `file` | string (binary) | **Required.** The deployment package: a tar.gz built by dlt that contains `manifest.yaml`. |
| `requirements_file` | string (binary) | **Required.** The workspace requirements manifest. |

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

```sh
curl -X POST "$DATAPLANE_URL/api/v1/workspaces/$WORKSPACE_ID/deployments/$DEPLOYMENT_ID/-upload" \
  -H "Authorization: Bearer $UPLOAD_TOKEN" \
  -F "file=@<file>" \
  -F "requirements_file=@<requirements_file>"
```

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

```python
import os

import requests

response = requests.post(
    "<dataplane_url>/api/v1/workspaces/<workspace_id>/deployments/<deployment_id>/-upload",
    headers={"Authorization": f"Bearer {os.environ['UPLOAD_TOKEN']}"},
    files={"file": open("<file>", "rb"), "requirements_file": open("<requirements_file>", "rb")},
)
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
{
  "content_hash": "<content_hash>",
  "created_at": "2026-01-01T00:00:00Z",
  "created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "file_count": 0,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "requirements_size_bytes": 0,
  "size_bytes": 0,
  "updated_at": "2026-01-01T00:00:00Z",
  "version": 0,
  "workspace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```

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

| Field | Type | Description |
| --- | --- | --- |
| `content_hash` | string | Hash of the package contents: base64 SHA3-256 over each file's path and digest, as dlt computes when building the package; identical file sets give identical hashes. |
| `created_at` | string (date-time) | When it was created. |
| `created_by` | string (uuid) | The `organization_membership_id` of the member who uploaded it. |
| `file_count` | integer | Number of files in the package. |
| `id` | string (uuid) | Unique ID (a UUID). |
| `requirements_size_bytes` | integer | Size of the uploaded requirements file in bytes. |
| `size_bytes` | integer | Size of the uploaded package in bytes. |
| `updated_at` | string (date-time) | When it was last changed. |
| `version` | integer | Version number within the workspace, starting at 1 and incremented by each upload. |
| `workspace_id` | string (uuid) | The ID of the workspace it belongs to. |

  </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">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>

## Variables

### List workspace variables {#list_workspace_variables}

```text
GET /api/v1/workspaces/{workspace_id}/variables
```

Lists workspace variables: the workspace-wide scope first, then each profile scope that holds variables, sorted by profile name. Pass `scope_profile` or `is_workspace_scope=true` to read one scope; an empty scope returns an empty list. Secret values are always `null`. Only workspace owners, and job runs in the workspace, can call this.

<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 |
| --- | --- | --- |
| `scope_profile` | string or null | Read only this profile's scope. |
| `is_workspace_scope` | boolean or null | `true` reads only the workspace-wide scope. Cannot be combined with `scope_profile`; `false` is rejected. |

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

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

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

```python
import os

import requests

response = requests.get(
    "<dataplane_url>/api/v1/workspaces/<workspace_id>/variables",
    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
{
  "scopes": [
    {
      "profile": "<profile>",
      "variables": [
        {
          "created_at": "2026-01-01T00:00:00Z",
          "created_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "name": "<name>",
          "type": "plain",
          "updated_at": "2026-01-01T00:00:00Z",
          "updated_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "value": "<value>"
        }
      ]
    }
  ]
}
```

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

| Field | Type | Description |
| --- | --- | --- |
| `scopes` | array of object | The workspace-wide scope first (always present, possibly empty), then each profile that holds variables, sorted by name. |
| `scopes[].profile` | string or null | Profile owning this scope; `null` is the workspace-wide scope |
| `scopes[].variables` | array of object | Variables in this scope, secrets redacted |

  </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>

### Change workspace variables {#update_workspace_variables}

```text
PATCH /api/v1/workspaces/{workspace_id}/variables
```

Create, update and delete variables in one scope, applied atomically. `profile` `null` targets the workspace-wide scope. Upserts create or replace by name. Deleting a name that was not set is reported as `not_found` rather than failing the request. Results list upserts first, then deletes. At most 200 variables per scope (400 `variables_cap_exceeded`). Reserved names are rejected case-insensitively (400 `reserved_variable_names`). A concurrent change answers 409 `variables_scope_locked`; retry. In a run, profile values override workspace-wide values with the same name. Only workspace owners can call this.

<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="body" label="Body">

| Field | Type | Description |
| --- | --- | --- |
| `deletes` | array of string | Variable names to remove |
| `profile` | string or null | **Required.** Profile whose scope to change: letters, digits, `_` and `-`, starting with a letter or digit, up to 64 characters; `null` targets the workspace-wide scope |
| `upserts` | array of object | Variables to create or update |

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

```sh
curl -X PATCH "$DATAPLANE_URL/api/v1/workspaces/$WORKSPACE_ID/variables" \
  -H "Authorization: Bearer $DATAPLANE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"profile": "<profile>"}'
```

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

```python
import os

import requests

response = requests.patch(
    "<dataplane_url>/api/v1/workspaces/<workspace_id>/variables",
    headers={"Authorization": f"Bearer {os.environ['DATAPLANE_TOKEN']}"},
    json={"profile": "<profile>"},
)
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
{
  "results": [
    {
      "name": "<name>",
      "status": "upserted"
    }
  ]
}
```

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

| Field | Type | Description |
| --- | --- | --- |
| `results` | array of object | Outcome per requested variable |
| `results[].name` | string | Environment variable name |
| `results[].status` | string | What happened to this variable: `upserted` (created or updated), `removed`, or `not_found` (a delete named a variable that does not exist). One of: `upserted`, `removed`, `not_found`. |

  </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">409</code> Conflict
- <code className="api-error">500</code> Internal Server Error

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

  </TabItem>
</Tabs>

</details>
</div>
