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

# Export data programmatically

> Export telemetry data from Sift using the REST API, an official client library, or a client generated with Buf.

export const MintTable = ({columns = [], rows = [], columnWidths = []}) => {
  const pushTextWithLineBreaks = (parts, text, keyBase) => {
    const segments = String(text).split(/\\n|\n/);
    segments.forEach((segment, idx) => {
      if (segment) {
        parts.push(<span key={`${keyBase}-text-${idx}`}>{segment}</span>);
      }
      if (idx < segments.length - 1) {
        parts.push(<br key={`${keyBase}-br-${idx}`} />);
      }
    });
  };
  const parseMarkdown = text => {
    if (text === null || text === undefined) return "";
    const str = String(text);
    const parts = [];
    let lastIndex = 0;
    const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[([^\]]+)\]\(([^)]+)\))/g;
    let match;
    while (true) {
      match = pattern.exec(str);
      if (match === null) {
        break;
      }
      if (match.index > lastIndex) {
        pushTextWithLineBreaks(parts, str.substring(lastIndex, match.index), `before-${lastIndex}`);
      }
      const fullMatch = match[0];
      if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
        parts.push(<code key={match.index}>{fullMatch.slice(1, -1)}</code>);
      } else if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
        parts.push(<strong key={match.index}>{fullMatch.slice(2, -2)}</strong>);
      } else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
        parts.push(<em key={match.index}>{fullMatch.slice(1, -1)}</em>);
      } else if (fullMatch.startsWith("[")) {
        const linkText = match[2];
        const linkUrl = match[3];
        parts.push(<a key={match.index} href={linkUrl} className="text-black-600 dark:text-black-400">
            {linkText}
          </a>);
      }
      lastIndex = pattern.lastIndex;
    }
    if (lastIndex < str.length) {
      pushTextWithLineBreaks(parts, str.substring(lastIndex), `tail-${lastIndex}`);
    }
    if (parts.length > 0) {
      return parts;
    }
    const plainParts = [];
    pushTextWithLineBreaks(plainParts, str, "plain");
    return plainParts.length ? plainParts : str;
  };
  const safeColumns = Array.isArray(columns) ? columns : [];
  const safeRows = Array.isArray(rows) ? rows : [];
  const safeColumnWidths = Array.isArray(columnWidths) ? columnWidths : [];
  const hasColumnWidths = safeColumnWidths.some(w => w !== null && w !== undefined && w !== "");
  const toCssWidth = width => typeof width === "number" ? `${width}px` : String(width);
  const getColumnStyle = idx => {
    const rawWidth = safeColumnWidths[idx];
    if (rawWidth === null || rawWidth === undefined || rawWidth === "") {
      return undefined;
    }
    const width = toCssWidth(rawWidth);
    return {
      width,
      minWidth: width
    };
  };
  const containerStyle = hasColumnWidths ? undefined : {
    overflowX: "auto"
  };
  const tableStyle = hasColumnWidths ? {
    tableLayout: "fixed",
    width: "100%"
  } : {
    width: "max-content",
    minWidth: "100%"
  };
  if (!Array.isArray(columns) || !Array.isArray(rows) || !Array.isArray(columnWidths)) {
    console.warn("MintTable received invalid props:", {
      columns,
      rows,
      columnWidths
    });
  }
  if (!safeColumns.length && !safeRows.length) {
    return null;
  }
  return <div className="mint-table-container" style={containerStyle}>
      <table style={tableStyle}>
        {hasColumnWidths && <colgroup>
            {safeColumns.map((_, idx) => {
    const style = getColumnStyle(idx);
    return <col key={idx} style={style} />;
  })}
          </colgroup>}
        <thead>
          <tr>
            {safeColumns.map((col, idx) => <th key={idx} className="text-left" style={getColumnStyle(idx)}>
                <b>{parseMarkdown(col)}</b>
              </th>)}
          </tr>
        </thead>
        <tbody>
          {safeRows.map((row, rIdx) => {
    const safeRow = Array.isArray(row) ? row : [];
    return <tr key={rIdx}>
                {safeRow.map((cell, cIdx) => <td key={cIdx} style={getColumnStyle(cIdx)}>
                    {parseMarkdown(cell)}
                  </td>)}
              </tr>;
  })}
        </tbody>
      </table>
    </div>;
};

Sift's REST API and client libraries let you export telemetry data from Runs programmatically for use in external tools, custom pipelines, and downstream analysis environments such as MATLAB or Python. Official clients are available for Python, Rust, and Go. Clients for other languages can be generated using [Buf](/api/clients/generate-a-client-with-buf).

Data captured from a Run can also be exported directly from the [UI](/documentation/export/export-data-from-a-file) without writing code, in CSV, Parquet, or Sun (WinPlot) format.

## Before you export

To export data from Sift programmatically, make sure you have the following:

* [A Sift API key and your base URLs](/documentation/manage/set-up-api-access).
* The ID of the Run and the IDs of the Channels you want to export.

## How does programmatic export work

Sift provides two programmatic export [mechanisms](#mechanisms), each available through the REST API and client libraries.

### Supported clients

Official clients are available for the following languages:

* [Python](https://sift-stack.github.io/sift/python/latest/reference/sift_client/)
* [Rust](https://github.com/sift-stack/sift/tree/main/rust)
* [Go](https://github.com/sift-stack/sift/tree/main/go)

Clients for other languages can be generated using [Buf](/api/clients/generate-a-client-with-buf).

### Mechanisms

The table below compares both mechanisms across the REST API and client libraries.

<MintTable
  columns={['', '[Mechanism 1: Data querying](#mechanism-1-data-querying)', '[Mechanism 2: Export data to file](#mechanism-2-export-data-to-file)']}
  columnWidths={['5%', '42%', '43%']}
  rows={[
['**What it does**', 'Queries specific Channel data and returns it immediately', 'Submits a background export job and returns a download link when ready'],
['**Best for**', 'Querying a few Channels over a specific time window', 'Exporting many Channels or an entire Run at once'],
['**Output**', 'JSON response body (REST) or Apache Arrow (Python client)', 'ZIP file containing CSV, Parquet, or Sun/WinPlot files. \n\n Sun/WinPlot is feature-flagged for certain gov cloud customers only'],
['**REST API**', '[POST /api/v2/data](/api-reference/dataservice/getdata-1)', '[POST /api/v1/export](/api-reference/exportservice/exportdata) to submit the job, then [GET /api/v1/export/{jobId}/download-url](/api-reference/exportservice/getdownloadurl) to retrieve the download link once the job is complete'],
['**Python client**', '[client.channels.get_data()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data) or [client.channels.get_data_as_arrow()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data_as_arrow)', '[client.data_export.export()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.DataExportAPI.export) to submit the job, then [client.jobs.wait_and_download()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.JobsAPI.wait_and_download) to poll for completion and download the result'],
]}
/>

## Mechanism 1: Data querying

Data querying requests Channel data over a defined time window and returns the response immediately.

Available via [POST /api/v2/data](/api-reference/dataservice/getdata-1) in the REST API, through the official [Python](https://sift-stack.github.io/sift/python/latest/reference/sift_client/), [Rust](https://github.com/sift-stack/sift/tree/main/rust), and [Go](https://github.com/sift-stack/sift/tree/main/go) clients, and through any client generated using [Buf](/api/clients/generate-a-client-with-buf).

### REST API

The examples below use cURL to call [POST /api/v2/data](/api-reference/dataservice/getdata-1) (`GetData`) directly. See the [GetData endpoint reference](/api-reference/dataservice/getdata-1) for the full schema.

<Steps>
  <Step title="Query Channel data">
    ```bash theme={null}
    curl --request POST \
      --url "https://api.siftstack.com/api/v2/data" \
      --header "Authorization: Bearer $SIFT_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "queries": [
          {
            "channel": {
              "channelId": "YOUR_CHANNEL_ID_1",
              "runId": "YOUR_RUN_ID"
            }
          },
          {
            "channel": {
              "channelId": "YOUR_CHANNEL_ID_2",
              "runId": "YOUR_RUN_ID"
            }
          }
        ],
        "startTime": "2026-03-13T19:43:25.557Z",
        "endTime": "2026-03-13T19:48:25.557Z",
        "sampleMs": 100,
        "pageSize": 100000,
        "pageToken": ""
      }'
    ```

    <MintTable
      columns={['Field', 'Description']}
      columnWidths={['40%', '75%']}
      rows={[
    ['`https://api.siftstack.com`', 'Replace with your REST base URL'],
    ['`$SIFT_API_KEY`', 'Replace with your API key or set it as an environment variable'],
    ['`channelId`', 'UUID of the Channel to query. Add multiple objects to the `queries` array to query more than one Channel'],
    ['`runId`', 'UUID of the Run to scope the query to'],
    ['`startTime`', 'Inclusive start of the time window, ISO 8601 UTC. Example: `2026-03-13T19:43:25.557Z`'],
    ['`endTime`', 'Exclusive end of the time window, ISO 8601 UTC'],
    ['`sampleMs`', 'Downsample to one point every N milliseconds using LTTB. Omit to return data at the native rate. \n\n Common values: `1` (1000 Hz), `10` (100 Hz), `100` (10 Hz), `1000` (1 Hz)'],        
    ['`pageSize`', 'Optional. Maximum values to return per response. Defaults to 10,000, maximum is 100,000'],
    ['`pageToken`', 'Leave empty for the first request. Set to the `nextPageToken` value from the previous response to retrieve the next page'],
  ]}
    />
  </Step>

  <Step title="Paginate if needed">
    If the response includes a non-empty `nextPageToken`, resend the same request with `pageToken` set to that value to retrieve the next batch. Repeat until `nextPageToken` comes back empty.

    ```bash theme={null}
    curl --request POST \
      --url "https://api.siftstack.com/api/v2/data" \
      --header "Authorization: Bearer $SIFT_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "queries": [
          {
            "channel": {
              "channelId": "YOUR_CHANNEL_ID_1",
              "runId": "YOUR_RUN_ID"
            }
          }
        ],
        "startTime": "2026-03-13T19:43:25.557Z",
        "endTime": "2026-03-13T19:48:25.557Z",
        "sampleMs": 100,
        "pageSize": 100000,
        "pageToken": "TOKEN_FROM_PREVIOUS_RESPONSE"
      }'
    ```
  </Step>
</Steps>

### Python client

The [official Python client](https://pypi.org/project/sift-stack-py/) provides two methods for querying Channel data:

* [client.channels.get\_data\_as\_arrow()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data_as_arrow) returns an Apache Arrow table. Recommended when converting data for use in other tools.
* [client.channels.get\_data()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data) returns typed Python value objects.

<Steps>
  <Step title="Initialize the client">
    ```python theme={null}
    import os
    from datetime import datetime, timezone
    from dotenv import load_dotenv
    from sift_client import SiftClient
    load_dotenv()

    client = SiftClient(
        api_key=os.getenv("SIFT_API_KEY"),
        grpc_url=os.getenv("SIFT_GRPC_URI"),
        rest_url=os.getenv("SIFT_REST_URI"),
    )
    ```
  </Step>

  <Step title="Retrieve the data">
    Fetch the Channel object using its ID, then call [client.channels.get\_data\_as\_arrow()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data_as_arrow). The method returns a `dict` where each key is a Channel name and the value is an Apache Arrow table.

    ```python theme={null}
    channel = client.channels.get(channel_id="YOUR_CHANNEL_ID")

    result = client.channels.get_data_as_arrow(
        channels=[channel],
        run="YOUR_RUN_ID",
        start_time=datetime(YYYY, MM, DD, HH, MM, SS, microsecond, tzinfo=timezone.utc),  # e.g. datetime(2026, 4, 15, 0, 17, 49, 984000, tzinfo=timezone.utc) = 2026-04-15T00:17:49.984Z
        end_time=datetime(YYYY, MM, DD, HH, MM, SS, microsecond, tzinfo=timezone.utc),    # e.g. datetime(2026, 4, 15, 0, 18, 26, 609000, tzinfo=timezone.utc) = 2026-04-15T00:18:26.609Z
    )

    # Access the table by Channel name and convert to a pandas DataFrame
    df = result["YOUR_CHANNEL_NAME"].to_pandas()
    ```

    Or using [client.channels.get\_data()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data), which returns a `dict[str, pd.DataFrame]` directly with no conversion needed:

    ```python theme={null}
    result = client.channels.get_data(
        channels=[channel],
        run="YOUR_RUN_ID",
        start_time=datetime(YYYY, MM, DD, HH, MM, SS, microsecond, tzinfo=timezone.utc),  # e.g. datetime(2026, 4, 15, 0, 17, 49, 984000, tzinfo=timezone.utc) = 2026-04-15T00:17:49.984Z
        end_time=datetime(YYYY, MM, DD, HH, MM, SS, microsecond, tzinfo=timezone.utc),    # e.g. datetime(2026, 4, 15, 0, 18, 26, 609000, tzinfo=timezone.utc) = 2026-04-15T00:18:26.609Z
    )

    # Access the DataFrame by Channel name
    df = result["YOUR_CHANNEL_NAME"]
    ```
  </Step>
</Steps>

## Mechanism 2: Export data to file

Export data to file processes the export in the background and makes the result available as a downloadable ZIP file.

Supported output formats: CSV, Parquet, and Sun/WinPlot. Sun/WinPlot is feature-flagged and only available to certain gov cloud customers.

Available via [POST /api/v1/export](/api-reference/exportservice/exportdata) in the REST API, through the official [Python](https://pypi.org/project/sift-stack-py/), [Rust](https://crates.io/crates/sift_rs), and [Go](https://pkg.go.dev/github.com/sift-stack/sift/go) clients, and through any client generated using [Buf](/api/clients/generate-a-client-with-buf).

### REST API

The examples below use cURL to call [POST /api/v1/export](/api-reference/exportservice/exportdata) (`ExportData`) and [GET /api/v1/export/{jobId}/download-url](/api-reference/exportservice/getdownloadurl) (`GetDownloadUrl`) directly. See the [ExportData endpoint reference](/api-reference/exportservice/exportdata) and the [GetDownloadUrl endpoint reference](/api-reference/exportservice/getdownloadurl) for the full schema.

<Steps>
  <Step title="Submit the export job">
    ```bash theme={null}
    curl --request POST \
      --url "https://api.siftstack.com/api/v1/export" \
      --header "Authorization: Bearer $SIFT_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "runsAndTimeRange": {
          "runIds": ["YOUR_RUN_ID"]
        },
        "channelIds": ["CHANNEL_ID_1", "CHANNEL_ID_2"],
        "outputFormat": "EXPORT_OUTPUT_FORMAT_PARQUET"
      }'
    ```

    <MintTable
      columns={['Field', 'Description']}
      columnWidths={['40%', '75%']}
      rows={[
    ['`https://api.siftstack.com`', 'Replace with your REST base URL'],
    ['`$SIFT_API_KEY`', 'Replace with your API key or set it as an environment variable'],
    ['`runIds`', 'One or more Run UUIDs to export'],
    ['`channelIds`', 'Specific Channel UUIDs to include. Omit to export all Channels in the Run'],
    ['`outputFormat`', '`EXPORT_OUTPUT_FORMAT_CSV`, `EXPORT_OUTPUT_FORMAT_PARQUET`, or `EXPORT_OUTPUT_FORMAT_SUN`'],
  ]}
    />
  </Step>

  <Step title="Retrieve the download link">
    If the response includes a `presignedUrl`, the export completed immediately and the file is ready to download. If the response includes a `jobId` instead, poll this endpoint until `presignedUrl` is populated:

    ```bash theme={null}
    curl --request GET \
      --url "https://api.siftstack.com/api/v1/export/YOUR_JOB_ID/download-url" \
      --header "Authorization: Bearer $SIFT_API_KEY"
    ```

    <MintTable
      columns={['Placeholder', 'Description']}
      columnWidths={['40%', '65%']}
      rows={[
    ['`https://api.siftstack.com`', 'Replace with your REST base URL'],
    ['`YOUR_JOB_ID`', 'Replace with the `jobId` returned from the export response'],
    ['`$SIFT_API_KEY`', 'Replace with your API key or set it as an environment variable'],
  ]}
    />
  </Step>
</Steps>

### Python client

The [official Python client](https://sift-stack.github.io/sift/python/latest/#installation) provides two methods for exporting data to file:

* [client.data\_export.export()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.DataExportAPI.export) submits the export job.
* [job.wait\_and\_download()](https://sift-stack.github.io/sift/python/latest/reference/sift_client/sift_types/job/#sift_client.sift_types.job.Job.wait_and_download) polls until complete, downloads, and extracts the result automatically. Returns a list of `Path` objects pointing to the extracted files.

<Steps>
  <Step title="Initialize the client">
    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from sift_client import SiftClient
    from sift_client.sift_types.export import ExportOutputFormat

    load_dotenv()

    client = SiftClient(
        api_key=os.getenv("SIFT_API_KEY"),
        grpc_url=os.getenv("SIFT_GRPC_URI"),
        rest_url=os.getenv("SIFT_REST_URI"),
    )
    ```
  </Step>

  <Step title="Submit the export job and download the result">
    ```python theme={null}
    job = client.data_export.export(
        output_format=ExportOutputFormat.PARQUET,  # or ExportOutputFormat.CSV, ExportOutputFormat.SUN
        runs=["YOUR_RUN_ID"],
        channels=["YOUR_CHANNEL_ID_1", "YOUR_CHANNEL_ID_2"],
    )

    paths = job.wait_and_download(output_dir=".")  # downloads and extracts to the directory where the script is run
    ```
  </Step>
</Steps>
