> ## 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 to MATLAB

> Get telemetry data from Sift into MATLAB for analysis using the official Python client or the REST API.

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 bring Channel data from Sift into MATLAB and load it into a timetable or table ready for analysis.

After data is stored in Sift, it can be exported into MATLAB for analysis. Sift recommends using the official Python [client](https://pypi.org/project/sift-stack-py/) via MATLAB's built-in Python interface. If your team cannot use Python, the REST API is also supported natively from MATLAB.

## Before you begin

* 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 exporting to MATLAB works

Sift provides two programmatic export methods: **data querying**, which returns Channel data immediately in the response, and **export data to file**, which processes the export in the background and produces a downloadable file. Both methods are available through either the **Python client** or the **REST API**, the two options for importing data into MATLAB.

<MintTable
  columns={['', '[Use the Python client via MATLAB\'s Python interface](#use-the-python-client-via-matlabs-python-interface)', '[Use the REST API via webread](#use-the-rest-api-via-webread)']}
  columnWidths={['20%', '40%', '40%']}
  rows={[
['**What it does**', 'Uses Sift\'s official Python client via MATLAB\'s built-in Python interface', 'Calls the Sift REST API directly using MATLAB\'s native `webread` and `webwrite` functions'],
['**Best for**', 'Teams that have Python available and want to use the full Sift Python client feature set', 'Teams that cannot or prefer not to use Python'],
['**Requirements**', 'Python 3.8 or later, `sift-stack-py`, and `pyarrow` installed on the same machine as MATLAB', 'MATLAB only, no additional dependencies'],
]}
/>

## Use the Python client via MATLAB's Python interface

### Set up the Python environment

The Python client requires Python 3.8 or later installed on the same machine as MATLAB. Both methods below rely on the Sift Python client running inside a Python virtual environment that MATLAB can access.

Complete the following steps once before running either method. This sets up the Python environment, points MATLAB to it, and stores your Sift credentials.

1. Create a virtual environment

   ```bash theme={null}
   python3 -m venv env
   source env/bin/activate
   ```

2. Install the required packages

   Create a file named `requirements.txt` in your working directory with the following contents:

   ```
   sift-stack-py==0.18.0
   pyarrow==21.0.0
   ```

   <Note>
     **Versions**: `sift-stack-py` 0.18.0 is the latest release as of this guide's last update. If a newer version is available by the time you're reading this, it likely works as well, but hasn't been verified against this guide.
   </Note>

   Creating a `requirements.txt` file as shown above, rather than installing packages individually, is recommended to avoid version mismatches as new releases of `sift-stack-py` become available.
   With the virtual environment active, install from the file:

   ```bash theme={null}
   pip install -r requirements.txt
   ```

3. Point MATLAB to your virtual environment

   In the MATLAB Command Window, run:

   ```matlab theme={null}
   pyenv(Version="/path/to/env/bin/python")
   ```

   Replace `/path/to/env` with the full path to the virtual environment you created. To find it, run `which python` in your terminal while the virtual environment is active.

4. Verify your Python environment

   In MATLAB, confirm that the correct Python environment is being used:

   ```matlab theme={null}
   pyenv
   ```

   If the wrong environment is selected, point MATLAB at the correct one:

   ```matlab theme={null}
   pyenv(Version="/path/to/your/python")
   ```

5. Set your Sift credentials

   Replace the placeholder values below with your actual credentials, then run the following in the MATLAB Command Window. This creates a `.env` file in your current working directory.

   ```matlab theme={null}
   lines = [
       "SIFT_API_KEY=your-api-key-here"
       "SIFT_GRPC_URI=your-grpc-url-here"
       "SIFT_REST_URI=your-rest-url-here"
   ];
   writelines(lines, '.env')
   ```

   To confirm where the file was saved, run `pwd`.

   Then load the credentials:

   ```matlab theme={null}
   loadenv('.env')
   disp(getenv('SIFT_API_KEY'))  % confirms the key was loaded
   ```

### Query Channel data

1. Initialize the client

   Run the following in the MATLAB Command Window to connect to Sift:

   ```matlab theme={null}
   sift_mod = py.importlib.import_module('sift_client');
   client = sift_mod.SiftClient(pyargs( ...
       'api_key',  getenv('SIFT_API_KEY'), ...
       'grpc_url', getenv('SIFT_GRPC_URI'), ...
       'rest_url', getenv('SIFT_REST_URI')));

   disp(client)  % confirms the client connected
   ```

2. List Channels and query data

   Use [client.runs.find](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.RunsAPI.find) to find a single Run by name, or [client.runs.list\_](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.RunsAPI.list_) to search across multiple Runs. When a Run is provided to [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 Run's time range is used automatically so `start_time` and `end_time` are not needed.

   Find the Run. Use Approach 1 to find a single Run by name:

   ```matlab theme={null}
   run = client.runs.find(pyargs('name', 'YOUR_RUN_NAME'));
   disp(run)  % shows the Run name, ID and time range
   ```

   Use Approach 2 to search across multiple Runs:

   ```matlab theme={null}
   runs = client.runs.list_(pyargs('name', 'YOUR_RUN_NAME'));
   run = runs{1};
   disp(run)  % shows the Run name, ID and time range
   ```

   Find Channels scoped to the Run using [client.channels.list\_](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.list_):

   ```matlab theme={null}
   channels = client.channels.list_(pyargs( ...
       'run',  run, ...
       'name', 'YOUR_CHANNEL_NAME'));

   disp(channels)  % shows the Channel objects found
   ```

   If you already know the Channel ID, you can use [client.channels.get](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get) instead:

   ```matlab theme={null}
   channel = client.channels.get(pyargs('channel_id', 'YOUR_CHANNEL_ID'));
   channels = py.list({channel});
   disp(channels)  % confirms the Channel was found
   ```

   Query data for the full Run:

   ```matlab theme={null}
   arrow_result = client.channels.get_data_as_arrow(pyargs( ...
       'channels', channels, ...
       'run',      run));

   disp(arrow_result)  % shows the Python dict with Channel names and Arrow tables
   ```

   To query a specific time window within the Run, pass `start_time` and `end_time`:

   ```matlab theme={null}
   utc = py.datetime.timezone(py.datetime.timedelta(0));

   start_dt = py.datetime.datetime(int32(YYYY), int32(MM), int32(DD), int32(HH), int32(MM), int32(SS), pyargs('tzinfo', utc));  % e.g. int32(2026), int32(4), int32(15), int32(0), int32(17), int32(49)
   end_dt   = py.datetime.datetime(int32(YYYY), int32(MM), int32(DD), int32(HH), int32(MM), int32(SS), pyargs('tzinfo', utc));  % e.g. int32(2026), int32(4), int32(15), int32(0), int32(18), int32(26)

   arrow_result = client.channels.get_data_as_arrow(pyargs( ...
       'channels',   channels, ...
       'run',        run, ...
       'start_time', start_dt, ...
       'end_time',   end_dt));

   disp(arrow_result)  % shows the Python dict with Channel names and Arrow tables
   ```

   `arrow_result` is a Python dict where each key is a Channel name and the value is an Apache Arrow table.

3. Write to Parquet and load into MATLAB

   Data returned by [get\_data\_as\_arrow](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data_as_arrow) comes back as a Python object that MATLAB cannot consume directly. Writing it to a Parquet file and loading it with `parquetread` converts it into a format MATLAB understands.

   Replace `YOUR_CHANNEL_NAME` with the exact name of your Channel as it appears in Sift, for example `temperature`.

   ```matlab theme={null}
   pq = py.importlib.import_module('pyarrow.parquet');
   pq.write_table(arrow_result{'YOUR_CHANNEL_NAME'}, 'sift_export.parquet');

   tt = parquetread('sift_export.parquet', ...
       'OutputType', 'timetable', ...
       'RowTimes',   'x__index_level_0__');

   % Rename the time dimension to something readable
   tt.Properties.DimensionNames{1} = 'time';

   disp(tt)
   ```

   The output will look similar to this:

   ```
               time            temperature
       ____________________    ___________

       15-Apr-2026 00:17:49      25.152
       15-Apr-2026 00:17:50      29.854
       15-Apr-2026 00:17:50      28.995
       15-Apr-2026 00:17:51      31.947
       15-Apr-2026 00:17:51      23.975
   ```

   You will see a warning about table variable names being modified. This is expected. MATLAB automatically renames the internal timestamp column from `__index_level_0__` to `x__index_level_0__` to comply with MATLAB identifier rules. The timetable is created correctly and the warning can be safely ignored.

### Export data to file

1. Submit the export job

   Submit the job using [client.data\_export.export](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.DataExportAPI.export). See the [`ExportOutputFormat`](https://sift-stack.github.io/sift/python/latest/reference/sift_client/sift_types/export/#sift_client.sift_types.export.ExportOutputFormat) reference for the accepted output format values.

   ```matlab theme={null}
   export_mod = py.importlib.import_module('sift_client.sift_types.export');
   ExportOutputFormat = export_mod.ExportOutputFormat;

   job = client.data_export.export(pyargs( ...
       'output_format', ExportOutputFormat.CSV, ...
       'runs',          py.list({'YOUR_RUN_ID'}), ...
       'channels',      py.list({'YOUR_CHANNEL_ID'})));

   % Extract the job ID from the Python job object
   jobId = char(job.id_);
   disp(jobId)  % confirms the job was submitted
   ```

2. Wait for the job and download the result

   Use `client.jobs.wait_and_download` with `show_progress` set to `false` to avoid the progress bar conflict with MATLAB's Command Window:

   ```matlab theme={null}
   client.jobs.wait_and_download(pyargs( ...
       'job',           job, ...
       'show_progress', false, ...
       'output_dir',    '.'));
   ```

3. Load the result into MATLAB

   The exported file is saved to MATLAB's current working directory. Run `dir` to find the filename:

   ```matlab theme={null}
   dir('*.csv')
   ```

   Then load it:

   ```matlab theme={null}
   T = readtable('YOUR_FILENAME.csv', ...
       'Delimiter',          ',', ...
       'VariableNamingRule', 'preserve');
   disp(T)
   ```

   The column names include the full Run and Asset path, for example `runName|assetName|temperature`. To rename a column after loading:

   ```matlab theme={null}
   T.Properties.VariableNames{2} = 'temperature';
   ```

## Use the REST API via webread

### Query Channel data

1. Set your Sift credentials

   Replace the placeholder values below with your actual credentials, then run the following in the MATLAB Command Window. This creates a `.env` file in your current working directory.

   ```matlab theme={null}
   lines = [
       "SIFT_API_KEY=your-api-key-here"
       "SIFT_REST_URI=your-rest-url-here"
   ];
   writelines(lines, '.env')
   ```

   To confirm where the file was saved, run `pwd`.

   Then load the credentials:

   ```matlab theme={null}
   loadenv('.env')
   ```

2. Query Channel data

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

   ```matlab theme={null}
   opts = weboptions( ...
       'RequestMethod', 'post', ...
       'MediaType',     'application/json', ...
       'ContentType',   'json', ...
       'HeaderFields',  {'Authorization', ['Bearer ' getenv('SIFT_API_KEY')]});

   query = struct( ...
       'queries', {{struct('channel', struct( ...
           'channelId', 'YOUR_CHANNEL_ID', ...
           'runId',     'YOUR_RUN_ID'))}}, ...
       'startTime', 'YYYY-MM-DDTHH:MM:SS.sssZ', ...  % e.g. 2026-04-15T00:17:49.984Z
       'endTime',   'YYYY-MM-DDTHH:MM:SS.sssZ', ...  % e.g. 2026-04-15T00:18:26.609Z
       'sampleMs',  0, ...  % 0 returns full-fidelity data; omit or set to 0 for analysis
       'pageSize',  100000, ...
       'pageToken', '');

   allResults = {};

   while true
       result = webwrite([getenv('SIFT_REST_URI') '/api/v2/data'], query, opts);
       allResults{end+1} = result;  %#ok<AGROW>

       if isempty(result.nextPageToken)
           break
       end

       query.pageToken = result.nextPageToken;
   end

   disp(allResults)  % each cell holds one page of the raw API response
   ```

   <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, or the last row in MATLAB doesn't match what the Sift app shows for the same time range, 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>

   <Note>
     **Pagination**: `GetData` returns at most `pageSize` values per request; see the [`pageSize`](/api-reference/dataservice/getdata-1#body-page-size) reference for the exact limits and how they're applied.

     If more data exists beyond the returned page, the response's `nextPageToken` field is non-empty and the result is truncated, not an error.

     Client libraries such as `sift-stack-py` handle this pagination internally, but MATLAB's `webwrite` does not, so a manual REST call must loop on `pageToken` itself to retrieve the full dataset, as shown above. The loop continues sending the same query with an updated `pageToken` until `nextPageToken` comes back empty, which signals that all data has been retrieved.

     Skipping this loop, as in a single `webwrite` call, silently returns only the first page even when more data exists.
   </Note>

3. Query multiple Channels

   To query more than one Channel in a single request, add a `struct` to the `queries` cell array for each Channel. The nested structure can be difficult to get right by hand, so it helps to build the `struct` in MATLAB first, then use `jsonencode` to inspect the exact JSON it produces before sending it.

   ```matlab theme={null}
   query = struct( ...
       'queries', {{ ...
           struct('channel', struct('channelId', 'YOUR_CHANNEL_ID_1', 'runId', 'YOUR_RUN_ID')), ...
           struct('channel', struct('channelId', 'YOUR_CHANNEL_ID_2', 'runId', 'YOUR_RUN_ID')), ...
           struct('channel', struct('channelId', 'YOUR_CHANNEL_ID_3', 'runId', 'YOUR_RUN_ID')) ...
       }}, ...
       'startTime', 'YYYY-MM-DDTHH:MM:SS.sssZ', ...  % e.g. 2026-04-15T00:17:49.984Z
       'endTime',   'YYYY-MM-DDTHH:MM:SS.sssZ', ...  % e.g. 2026-04-15T00:18:26.609Z
       'sampleMs',  0, ...  % 0 returns full-fidelity data; omit or set to 0 for analysis
       'pageSize',  100000, ...
       'pageToken', '');
   ```

   Write the `struct` to a JSON file to confirm the nesting is correct before submitting it:

   ```matlab theme={null}
   jsonStr = jsonencode(query, 'PrettyPrint', true);

   fid = fopen('query.json', 'w');
   fprintf(fid, '%s', jsonStr);
   fclose(fid);

   disp(jsonStr)  % shows the JSON that will be sent
   ```

   If you build or edit the JSON file directly instead of constructing the `struct` in MATLAB, load it back in with `jsondecode` before submitting:

   ```matlab theme={null}
   query = jsondecode(fileread('query.json'));
   ```

   Submit the request. `webwrite` accepts the `struct` directly and encodes it to JSON internally, so the same `query` variable used to inspect the file can be sent as is. A multichannel request is subject to the same `pageSize` limit as a single-Channel request, so this uses the same `pageToken` loop shown in the previous step:

   ```matlab theme={null}
   allResults = {};

   while true
       result = webwrite([getenv('SIFT_REST_URI') '/api/v2/data'], query, opts);
       allResults{end+1} = result;  %#ok<AGROW>

       if isempty(result.nextPageToken)
           break
       end

       query.pageToken = result.nextPageToken;
   end

   disp(allResults)  % each cell holds one page of the raw API response, with data for all requested Channels
   ```

4. Load into a MATLAB timetable

   Each page in `allResults` can contain data for more than one Channel. Group the values by Channel name across all pages, then build one timetable per Channel:

   ```matlab theme={null}
   channelData = struct();

   for i = 1:numel(allResults)
       for j = 1:numel(allResults{i}.data)
           channelName = matlab.lang.makeValidName(allResults{i}.data(j).metadata.channel.name);

           if ~isfield(channelData, channelName)
               channelData.(channelName) = [];
           end

           channelData.(channelName) = [channelData.(channelName); allResults{i}.data(j).values];
       end
   end

   channelNames = fieldnames(channelData);
   timetables = struct();

   for k = 1:numel(channelNames)
       name = channelNames{k};

       dt = struct2table(channelData.(name));
       dt.time = datetime(dt.timestamp, ...
           'InputFormat', 'uuuu-MM-dd''T''HH:mm:ss.SSS''Z''', ...
           'TimeZone',    'utc');
       dt.timestamp = [];

       timetables.(name) = table2timetable(dt, 'RowTimes', 'time');
   end

   disp(timetables)  % struct with one timetable per Channel, e.g. timetables.ADCS_RW_Speed_Y
   ```

   `matlab.lang.makeValidName` converts Channel names such as `ADCS.RW_Speed_Y` into valid MATLAB field names (`ADCS_RW_Speed_Y`), since field names can't contain periods. This same code works whether `allResults` came from a single-Channel or multichannel query, since it groups by however many Channels are actually present in the response.

### Export data to file

1. Submit the export job

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

   ```matlab theme={null}
   opts = weboptions( ...
       'RequestMethod', 'post', ...
       'MediaType',     'application/json', ...
       'ContentType',   'json', ...
       'HeaderFields',  {'Authorization', ['Bearer ' getenv('SIFT_API_KEY')]});

   exportBody = struct( ...
       'runsAndTimeRange', struct('runIds', {{'YOUR_RUN_ID'}}), ...
       'channelIds',       {{'YOUR_CHANNEL_ID'}}, ...
       'outputFormat',     'EXPORT_OUTPUT_FORMAT_CSV');

   exportResult = webwrite([getenv('SIFT_REST_URI') '/api/v1/export'], exportBody, opts);
   disp(exportResult)  % shows the job ID and status
   ```

   <Note>
     **outputFormat**: `EXPORT_OUTPUT_FORMAT_CSV` is an actual value that needs to be passed as-is, not a placeholder to replace with your own text.
   </Note>

2. Retrieve the download link

   ```matlab theme={null}
   if ~isempty(exportResult.presignedUrl)
       websave('sift_export.zip', exportResult.presignedUrl);
   else
       getOpts = weboptions( ...
           'HeaderFields', {'Authorization', ['Bearer ' getenv('SIFT_API_KEY')]}, ...
           'ContentType', 'json');
       jobId = exportResult.jobId;
       presignedUrl = '';
       while isempty(presignedUrl)
           pause(5);
           urlResult    = webread([getenv('SIFT_REST_URI') '/api/v1/export/' jobId '/download-url'], getOpts);
           presignedUrl = string(urlResult.presignedUrl);
       end
       websave('sift_export.zip', presignedUrl);
   end
   ```

3. Unzip and load into MATLAB

   ```matlab theme={null}
   unzip('sift_export.zip', 'sift_export');
   ```

   The ZIP contains a file with a name generated by Sift. The format matches the `outputFormat` you specified when submitting the job, for example `sift_data_export_2026-05-19_172841.csv` for CSV. Run the following to see the exact filename:

   ```matlab theme={null}
   dir('sift_export')
   ```

   Then load it using the actual filename:

   ```matlab theme={null}
   T = readtable('sift_export/YOUR_FILENAME.csv', ...
       'Delimiter',         ',', ...
       'VariableNamingRule', 'preserve');
   disp(T)
   ```

   The column names include the full Run and Asset path, for example `runName|assetName|temperature`. To rename a column after loading:

   ```matlab theme={null}
   T.Properties.VariableNames{2} = 'temperature';
   disp(T)
   ```

## Reference

* [Export data programmatically](/api/export/export-data-programmatically)
