# SQL REST API quickstart

Go from an API key to your first exported query result. Each request below is shown three ways: as a cURL or PowerShell command that returns CSV, and as Python that reads Arrow.

## Prerequisites

Each step below is shown three ways, and the tabs ask for different formats from the REST API. The cURL and PowerShell examples set `result_format` to CSV, which needs no decoding. The Python examples leave it unset and get the default Arrow stream. See [Result formats](https://dev.nominal.io/sql-reference/rest-api/query.md#result-formats) for more information.

For the **cURL** path, all you need is [cURL](https://curl.se/) itself, which ships with macOS, most Linux distributions, and Windows 10 and later. Those examples ask the REST API for CSV, which is human-readable.

For the **PowerShell** path, nothing to install: `Invoke-RestMethod` is built in.

For the **Python** path, you need [Python](https://www.python.org/downloads/) 3.11 or later plus three libraries: the Nominal Python client, which reads your credentials, and two for the requests and their results. Python examples request Arrow rather than CSV, because Arrow carries column types and loads directly into pandas or Polars:

```shell
pip install nominal pyarrow requests
```

> **Warning**
>
> **On Windows**, use the **PowerShell** tab rather than the cURL one. In PowerShell, `curl` is an alias for `Invoke-WebRequest` rather than the real cURL, and quoting rules differ from POSIX shells, so the cURL examples will not run as written.

## Gather your Nominal credentials

Copy your workspace RID, API key, and REST API URL from the Nominal app. Each cURL and PowerShell example below sets them in its first lines, so paste yours in there. The Python examples read them from the `default` profile in your Nominal config file. If you haven’t created it yet, follow [API keys setup](https://dev.nominal.io/quickstart/api-keys-setup.md), which stores the same three values.

Where to find these values

Open your organization’s **API keys** page in Settings:

1. **API key:** Click **CREATE KEY**, name it, set an expiration, and click **GENERATE KEY**. Copy the key. *You won’t be able to see it again. Store your API key securely to protect your Nominal data.*
2. **REST API URL:** On the same page, find **API URL** and click **COPY URL**.
3. **Workspace RID:** On the same page, find your workspace under **WORKSPACES** and click **COPY RID**.

![Settings API keys page](https://dev.nominal.io/sql-guides/_images/settings-api-keys-page.png)

## Find a dataset to query

Telemetry queries must name the dataset(s) they read, so start by listing the datasets in your workspace. The `datasets` table needs no scoping:

**cURL**

Ask for CSV so the result prints straight to your terminal:

```shell
API_URL='https://api.gov.nominal.io/api'
API_KEY='<api key>'  # DO NOT COMMIT THIS
WORKSPACE_RID='<ri.security.instance.workspace.identifier>'

curl --request POST "$API_URL/sql/v1/query" \
  --header "Authorization: Bearer $API_KEY" \
  --header 'Content-Type: application/json' \
  --fail-with-body \
  --data "{
    \"query\": \"SELECT dataset_rid, name FROM datasets LIMIT 20\",
    \"workspace_rid\": \"$WORKSPACE_RID\",
    \"result_format\": \"SQL_SERVICE_QUERY_RESULT_FORMAT_CSV\"
  }"
```

```text
"dataset_rid","name"
"ri.catalog.example.dataset.7c...a7","endurance"
"ri.catalog.example.dataset.e5...7b","shakedown"
```

**PowerShell**

```powershell
$apiUrl = 'https://api.gov.nominal.io/api'
$apiKey = '<api key>'  # DO NOT COMMIT THIS
$workspaceRid = '<ri.security.instance.workspace.identifier>'

$body = @{
  query         = "SELECT dataset_rid, name FROM datasets LIMIT 20"
  workspace_rid = $workspaceRid
  result_format = "SQL_SERVICE_QUERY_RESULT_FORMAT_CSV"
} | ConvertTo-Json

Invoke-RestMethod -Method Post -Uri "$apiUrl/sql/v1/query" `
  -Headers @{ Authorization = "Bearer $apiKey" } `
  -ContentType 'application/json' `
  -Body $body
```

Returns the same CSV as the cURL tab.

**Python**

```python
import pyarrow as pa
import requests
from nominal.config import NominalConfig

profile = NominalConfig.from_yaml().get_profile("default")

response = requests.post(
    f"{profile.base_url}/sql/v1/query",
    headers={"Authorization": f"Bearer {profile.token}"},
    json={
        "query": "SELECT dataset_rid, name FROM datasets LIMIT 20",
        "workspace_rid": profile.workspace_rid,
    },
)
response.raise_for_status()

table = pa.ipc.open_stream(pa.py_buffer(response.content)).read_all()
for row in table.to_pylist():
    print(row["dataset_rid"], row["name"])
```

Pick a `dataset_rid` from the result, and paste it into the dataset RID line at the top of each example below.

## Run your first telemetry query

Now read telemetry from that dataset:

**cURL**

```shell
API_URL='https://api.gov.nominal.io/api'
API_KEY='<api key>'  # DO NOT COMMIT THIS
WORKSPACE_RID='<ri.security.instance.workspace.identifier>'
DATASET_RID='<ri.catalog.instance.dataset.identifier>'

curl --request POST "$API_URL/sql/v1/query" \
  --header "Authorization: Bearer $API_KEY" \
  --header 'Content-Type: application/json' \
  --fail-with-body \
  --data "{
    \"query\": \"SELECT ts, channel, value FROM points_double WHERE dataset_rid = '$DATASET_RID' ORDER BY ts LIMIT 100\",
    \"workspace_rid\": \"$WORKSPACE_RID\",
    \"result_format\": \"SQL_SERVICE_QUERY_RESULT_FORMAT_CSV\"
  }"
```

```text
"ts","channel","value"
"2026-07-17T21:24:40.107000064Z","channel_a",0
"2026-07-17T21:24:40.107000064Z","channel_b",0
"2026-07-17T21:24:40.144999936Z","channel_a",7.114995682427262
"2026-07-17T21:24:40.144999936Z","channel_b",7.114995682427262
"2026-07-17T21:24:40.150000128Z","channel_a",-0.2923734171138021
"2026-07-17T21:24:40.150000128Z","channel_b",-0.2923734171138021
```

Timestamps are ISO 8601 in UTC, with their full nanosecond precision.

The query reads every channel in the dataset, so each timestamp repeats once per channel. Add `AND channel = '<channel-name>'` to follow a single one, and see [Filtering](https://dev.nominal.io/sql-guides/writing-queries.md#filtering) for narrowing further.

**PowerShell**

```powershell
$apiUrl = 'https://api.gov.nominal.io/api'
$apiKey = '<api key>'  # DO NOT COMMIT THIS
$workspaceRid = '<ri.security.instance.workspace.identifier>'
$datasetRid = '<ri.catalog.instance.dataset.identifier>'

$body = @{
  query         = "SELECT ts, channel, value FROM points_double WHERE dataset_rid = '$datasetRid' ORDER BY ts LIMIT 100"
  workspace_rid = $workspaceRid
  result_format = "SQL_SERVICE_QUERY_RESULT_FORMAT_CSV"
} | ConvertTo-Json

Invoke-RestMethod -Method Post -Uri "$apiUrl/sql/v1/query" `
  -Headers @{ Authorization = "Bearer $apiKey" } `
  -ContentType 'application/json' `
  -Body $body
```

**Python**

```python
import datetime

import pyarrow as pa
import requests
from nominal.config import NominalConfig

NOMINAL_DATASET_RID = "<ri.catalog.instance.dataset.identifier>"

profile = NominalConfig.from_yaml().get_profile("default")

query = f"""
SELECT ts, channel, value
FROM points_double
WHERE dataset_rid = '{NOMINAL_DATASET_RID}'
ORDER BY ts
LIMIT 100
"""

response = requests.post(
    f"{profile.base_url}/sql/v1/query",
    headers={"Authorization": f"Bearer {profile.token}"},
    json={
        "query": query,
        "workspace_rid": profile.workspace_rid,
    },
)
response.raise_for_status()

table = pa.ipc.open_stream(pa.py_buffer(response.content)).read_all()
print(table.schema)
print(table.num_rows, "rows")

timestamps = table.column("ts")
channels = table.column("channel")
values = table.column("value")

for i in range(min(5, table.num_rows)):
    # ts arrives as nanosecond-precision Arrow, which Python's datetime cannot
    # represent, so it has to be read as an integer and converted by hand.
    moment = datetime.datetime.fromtimestamp(timestamps[i].value / 1e9, datetime.timezone.utc)
    print(f"{moment.isoformat()} {channels[i].as_py()}\t\t{values[i].as_py()}")
```

> **Note**
>
> Queries are read-only and limited to a single `SELECT`, and telemetry tables always require a `dataset_rid`. See the [SQL REST API](https://dev.nominal.io/sql-reference/rest-api.md) for the full interface.

## Export the result

You now have a telemetry query that returns the data you care about, cut to the first 100 rows by the `LIMIT`. To take the whole result with you as a file, drop the `LIMIT` and send the query to the export endpoint. It executes the query and writes the complete result to object storage, returning a presigned URL to download it:

**cURL**

The export response is JSON, so it prints safely:

```shell
API_URL='https://api.gov.nominal.io/api'
API_KEY='<api key>'  # DO NOT COMMIT THIS
WORKSPACE_RID='<ri.security.instance.workspace.identifier>'
DATASET_RID='<ri.catalog.instance.dataset.identifier>'

curl --request POST "$API_URL/sql/v1/query/export" \
  --header "Authorization: Bearer $API_KEY" \
  --header 'Content-Type: application/json' \
  --data "{
    \"query\": \"SELECT ts, channel, value FROM points_double WHERE dataset_rid = '$DATASET_RID' ORDER BY ts\",
    \"workspace_rid\": \"$WORKSPACE_RID\"
  }"
```

Copy the `presignedUrl` value out of the response and download the CSV:

```shell
curl --fail-with-body --output export.csv '<presignedUrl-from-the-response>'
```

Be sure to keep the URL in quotes.

You can also open the `presignedUrl` in a browser. The URL is time-limited.

**PowerShell**

```powershell
$apiUrl = 'https://api.gov.nominal.io/api'
$apiKey = '<api key>'  # DO NOT COMMIT THIS
$workspaceRid = '<ri.security.instance.workspace.identifier>'
$datasetRid = '<ri.catalog.instance.dataset.identifier>'

$body = @{
  query         = "SELECT ts, channel, value FROM points_double WHERE dataset_rid = '$datasetRid' ORDER BY ts"
  workspace_rid = $workspaceRid
} | ConvertTo-Json

$response = Invoke-RestMethod -Method Post -Uri "$apiUrl/sql/v1/query/export" `
  -Headers @{ Authorization = "Bearer $apiKey" } `
  -ContentType 'application/json' `
  -Body $body

Invoke-WebRequest -Uri $response.presignedUrl -OutFile export.csv
```

`Invoke-RestMethod` parses the JSON response, so `$response.presignedUrl` reaches the download step without you copying the URL by hand.

**Python**

```python
import requests
from nominal.config import NominalConfig

NOMINAL_DATASET_RID = "<ri.catalog.instance.dataset.identifier>"

profile = NominalConfig.from_yaml().get_profile("default")

query = f"""
SELECT ts, channel, value
FROM points_double
WHERE dataset_rid = '{NOMINAL_DATASET_RID}'
ORDER BY ts
"""

response = requests.post(
    f"{profile.base_url}/sql/v1/query/export",
    headers={"Authorization": f"Bearer {profile.token}"},
    json={
        "query": query,
        "workspace_rid": profile.workspace_rid,
    },
)
response.raise_for_status()
url = response.json()["presignedUrl"]

with open("export.csv", "wb") as destination:
    destination.write(requests.get(url).content)
```

> **Note**
>
> Export works for queries over the metadata tables as well as the telemetry tables. See [Export](https://dev.nominal.io/sql-reference/rest-api/export.md) for the options.

## Next steps

Start [writing queries](https://dev.nominal.io/sql-guides/writing-queries.md) against the [SQL REST API](https://dev.nominal.io/sql-reference/rest-api.md).
