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

# Build your own live telemetry display

> Poll the Sift API from a backend service to stream live Run telemetry to a custom application or display.

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 isListLine = line => (/^\s*[-*]\s+/).test(line);
  const renderCellContent = (text, keyBase) => {
    if (text === null || text === undefined) return "";
    const lines = String(text).split(/\\n|\n/);
    const hasListLine = lines.some(isListLine);
    if (!hasListLine) {
      return parseMarkdown(text);
    }
    const blocks = [];
    let paragraphLines = [];
    let listItems = [];
    const flushParagraph = () => {
      if (paragraphLines.length) {
        blocks.push({
          type: "paragraph",
          text: paragraphLines.join("\n")
        });
        paragraphLines = [];
      }
    };
    const flushList = () => {
      if (listItems.length) {
        blocks.push({
          type: "list",
          items: listItems
        });
        listItems = [];
      }
    };
    lines.forEach(line => {
      if (isListLine(line)) {
        flushParagraph();
        listItems.push(line.replace(/^\s*[-*]\s+/, ""));
      } else if (line.trim() === "") {} else {
        flushList();
        paragraphLines.push(line);
      }
    });
    flushParagraph();
    flushList();
    return blocks.map((block, bIdx) => {
      if (block.type === "list") {
        return <ul key={`${keyBase}-list-${bIdx}`} style={{
          margin: 0,
          paddingLeft: "1.1em"
        }}>
            {block.items.map((item, iIdx) => <li key={`${keyBase}-list-${bIdx}-item-${iIdx}`} style={{
          marginTop: iIdx > 0 ? "0.25em" : 0
        }}>
                {parseMarkdown(item)}
              </li>)}
          </ul>;
      }
      return <span key={`${keyBase}-p-${bIdx}`}>{parseMarkdown(block.text)}</span>;
    });
  };
  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)}>
                    {renderCellContent(cell, `r${rIdx}-c${cIdx}`)}
                  </td>)}
              </tr>;
  })}
        </tbody>
      </table>
    </div>;
};

After completing this workflow, you can show telemetry in your own application from a Run as it streams into Sift.

## Before you begin

* You have [a Sift API key and your base URLs](/documentation/manage/set-up-api-access).
* You have an Asset that is streaming live data to Sift in a Run.

## How live telemetry reaches your display

Your application can read telemetry from Sift while a Run streams. Because Sift does not push data automatically, your application must poll the API.

To protect your API key, build a backend proxy service to poll Sift and relay telemetry updates to your client display.
The service finds the latest Run for the Asset with [`ListRuns`](/api-reference/runservice/listruns) and polls [`GetData`](/api-reference/dataservice/getdata-v2) for new data.

<div align="center" className="mermaid-diagram">
  ```mermaid theme={null}
  %%{init: {"flowchart": {"htmlLabels": false}}}%%
  graph TB
    C(Sift API):::highlight
    B(Your backend service):::highlight
    A(Your display):::highlight

    C -. returns the latest Run and data .-> B
    B -- polls ListRuns and GetData --> C
    B -- sends updates --> A

    classDef highlight fill:transparent,stroke:#ee4220,stroke-width:0px,color:#ee4220;
    linkStyle default stroke:#ee4220,stroke-width:1px;
  ```
</div>

Sift does not support embedding a Sift view in another page, such as an internal dashboard or a wiki page.

## Relay live telemetry through a backend service

1. Run a backend service that stores your [API key](/documentation/manage/set-up-api-access).
   * Use an API key [created in the environment](/documentation/manage/set-up-api-access#use-api-keys-across-multiple-environments) that contains the Asset's data.
   <Warning>
     **Browser requests**: Do not call the Sift API directly from the browser. Doing so exposes your API key, and the API does not accept requests from browser pages outside Sift.
   </Warning>
2. From the service, call [`ListRuns`](/api-reference/runservice/listruns) with a `filter` on `asset_id`, set to the ID of the Asset you want to follow, and a `pageSize` of `1`.
   * Sift does not flag a Run as live. `ListRuns` orders results newest first by default, so the first result is the latest Run for the Asset.
   * This follows the latest test when your system creates a new Run for each test.
   * Do not filter on an empty `stop_time`. Sift moves a streaming Run's `stop_time` forward with each new data point, so it is set even while the Run is live.
3. Poll [`GetData`](/api-reference/dataservice/getdata-v2) about once per second for the latest window of the Channels you need.

   <MintTable
     columns={['Field', 'Value']}
     columnWidths={['25%', '80%']}
     rows={[
   ["`runId`", "The Run ID from step 2."],
   ["`channelId`", "The ID of each Channel you want to display."],
   ["`startTime`", "The start of the time window to query. This bound is *inclusive* and matches the time each point was recorded at the source, not the time Sift received it.\n\nSet it to the current time minus a lookback margin, such as 10 seconds. Base the margin on how late you expect points to arrive.\n\n**If your data timestamps lag**: Use the timestamp of the newest point you received minus the lookback margin instead."],
   ["`endTime`", "The end of the time window to query. This bound is *exclusive* and, like `startTime`, matches the time each point was recorded at the source.\n\nSet it to the current time plus a lookforward margin, such as 10 seconds. The margin picks up points as they arrive if your server's clock is slightly behind."],
   ["`sampleMs`", "`0`, to return every point without downsampling."],
   ["`includeReceivedAt`", "Optional. Set to `true` to include a `sift_received_at` timestamp for each point in `extras`. This timestamp is the time Sift received the point."]
   ]}
   />

   ```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:43:45.557Z",
       "sampleMs": 0
     }'
   ```
4. Send the updates to your display using any method your application supports, such as WebSocket or Server-Sent Events (SSE).

## Verify

Launch your display while the Asset streams a Run. Confirm the UI renders updated telemetry approximately once per second.

## Reference

* [`ListRuns` API reference](/api-reference/runservice/listruns)
* [`GetData` API reference](/api-reference/dataservice/getdata-v2)
* [Query Channel data](/api/export/export-data-programmatically#query-channel-data)
