> ## 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>;
};

After completing this workflow, you can export Channel data from any Run into the tool or pipeline your team uses for analysis.

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.

## Before you export

* You have [a Sift API key and your base URLs](/documentation/manage/set-up-api-access).
* You have the ID of the Run and the IDs of the Channels you want to export. One option is to find these in the Sift UI or by querying the [`ListRuns`](/api-reference/runservice/listruns) and [`ListChannels`](/api-reference/channelservice/listchannels) endpoints.

## How does programmatic export work

Sift provides two programmatic export methods, **data querying** and **export data to file**, each available through the REST API and official client libraries (Python, Rust, and Go). Clients for other languages can be generated using [Buf](/api/clients/generate-a-client-with-buf).

<MintTable
  columns={['', '[Query Channel data](#query-channel-data)', '[Export data to file](#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'],
]}
/>

## Query Channel data

### Use the REST API

To query Channel data, call the [`GetData`](/api-reference/dataservice/getdata-1) endpoint.

1. 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": 0,
       "pageSize": 100000,
       "pageToken": ""
     }'
   ```

   <Note>
     **endTime**: This field is exclusive, so a sample that falls exactly at or after `endTime` is not included in the response. If a value you expect to see is missing, try extending `endTime` slightly past the boundary you actually want.
   </Note>

   <Note>
     **sampleMs**: Set `sampleMs` to `0`, or omit it, to return the full, unsampled dataset, recommended for external data analysis. Any other value downsamples the response using LTTB, a shape-preserving algorithm intended for plotting rather than analysis; it returns approximately one representative point per that many milliseconds rather than sampling at a literal fixed interval.
   </Note>

   <Note>
     **Timeseries Panel**: The Sift app's Timeseries Panel offers a choice of sampling methods (LTTB, Min/Max, and Changed Only), but [`GetData`](/api-reference/dataservice/getdata-1) only supports LTTB; the other methods are not currently available through this endpoint.
   </Note>

2. 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": 0,
       "pageSize": 100000,
       "pageToken": "TOKEN_FROM_PREVIOUS_RESPONSE"
     }'
   ```

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

1. 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"),
   )
   ```

2. 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"]
   ```

## Export data to file

### Use the REST API

To export data to a file, call the [`ExportData`](/api-reference/exportservice/exportdata) and [`GetDownloadUrl`](/api-reference/exportservice/getdownloadurl) endpoints.

1. 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"
     }'
   ```

   <Note>
     **outputFormat**: This is a fixed value to select, not a placeholder to replace with your own text.
   </Note>

2. 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"
   ```

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

1. 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"),
   )
   ```

2. Submit the export job and download the result

   ```python theme={null}
   job = client.data_export.export(
       output_format=ExportOutputFormat.PARQUET,
       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
   ```
