Embedding with HMAC Authentication

Embed Streamline workflows that use HMAC-authenticated Incoming Webhooks - your server starts the session, signs the payload, and passes the embed URL to the client.

Overview

When a workflow's first step is an Incoming Webhook with HMAC authentication enabled, the simple /start-session embedding flow does not apply. Instead, your server creates the session by sending an HMAC-signed request to the webhook endpoint, receives a resumeUrl in the response, and passes that URL to the client-side iframe.

This approach is ideal when:

  • Payload integrity must be cryptographically verified (the request body cannot be tampered with in transit).
  • The session must be initiated server-side, not by the end user's browser.
  • You need server-to-server authentication without exposing tokens to the client.

Looking for the simple embed flow? See Embedding Workflows for public workflows that use the /start-session route.

Prerequisites

  • A workflow in Streamline whose first step is an Incoming Webhook with authentication set to HMAC.
  • The webhook's HMAC shared secret (found in Streamline connection settings).
  • Your webhook ID (the Incoming Webhook's unique identifier).
  • A backend server that can compute HMAC-SHA256 signatures before the page is served to the user.
  • Your site served over HTTPS in production.

How It Works

┌──────────────┐        ┌────────────────────────┐        ┌──────────────┐
│  Your Server │        │       Streamline       │        │  Browser     │
│              │        │                        │        │  (iframe)    │
└──────┬───────┘        └───────────┬────────────┘        └───────┬──────┘
       │                            │                             │
       │  1. Build payload          │                             │
       │  2. Compute HMAC-SHA256    │                             │
       │  3. POST {URL}             │                             │
       │  ────────────────────────► │                             │
       │                            │  4. Validate signature      │
       │                            │  5. Create session          │
       │  6. Receive resumeUrl    ◄─┤                             │
       │                            │                             │
       │  7. Pass URL to client     │                             │
       │  ──────────────────────────┼───────────────────────────► │
       │                            │                             │
       │                            │  8. iframe loads session  ◄─┤
       │                            │  9. postMessage events ───► │
       │                            │                             │
  1. Your server builds the JSON payload and computes the HMAC-SHA256 signature using the shared secret.
  2. Your server POSTs to the Incoming Webhook endpoint with the Streamline-Signature header.
  3. Streamline validates the signature, creates a workflow session, and returns a resumeUrl in the JSON body.
  4. Your server passes that URL (with ?source=embed appended) to the client.
  5. The client renders the URL in an iframe - all postMessage events work the same as the simple embed flow.

HMAC Signature Basics

DetailValue
AlgorithmHMAC-SHA256
HeaderStreamline-Signature: sha256={hex}
InputExact raw request body bytes (UTF-8)
SecretYour shared secret from Streamline

Validation flow inside Streamline

  1. Your system computes HMAC-SHA256(rawBodyBytes, sharedSecret).
  2. Your system sends the hex digest as header Streamline-Signature: sha256={digest}.
  3. Streamline computes the same digest using the raw bytes it receives.
  4. If digests match, authentication passes; otherwise the request is rejected with 401.

Step 1: Generate the HMAC Signature (Server-Side)

Requirements

  • Header format: Streamline-Signature: sha256={hex}
  • Encoding: UTF-8 bytes (unless your payload contract specifies otherwise)
  • Secret: The HMAC secret from Streamline connection settings

Steps

  1. Build the exact JSON payload string you will send.
  2. Convert the payload to raw bytes (UTF-8).
  3. Compute HMAC-SHA256 with your shared secret.
  4. Hex-encode the digest (lowercase).
  5. Set the Streamline-Signature header.

Step 2: POST to the Incoming Webhook

Send the signed payload to Streamline, for example:

POST https://us.streamline.intellistack.ai/v1/webhooks/incoming/{WEBHOOK_ID}

Include both headers:

Content-Type: application/json
Streamline-Signature: sha256={hex}

On success, the JSON response includes resumeUrl — use it in Step 3 to build the iframe URL.

Step 3: Build the Embed URL

Take resumeUrl from Step 2 and append ?source=embed (or &source=embed if the URL already has query params):

{RESUME_URL}?source=embed

This tells Streamline to render the session in embedded mode (enable in-memory routing, emit resize events, etc.).

Step 4: Serve the iframe

Pass the embed URL to your frontend and render it in an iframe. This is identical to the simple embed flow - the only difference is where the URL comes from.

<iframe
  id="streamline-embedded-workflow"
  title="Streamline Workflow"
  src=""
  width="600"
  height="800"
  style="border: none;"
></iframe>

<script>
  // The browser calls your API; your server runs Step 1–2 and returns
  // the resume URL (`resumeUrl`) with `?source=embed` (the `embedUrl`).
  (async () => {
    const response = await fetch('/api/get-embed-url', {
      method: 'GET',
    });
    if (!response.ok) throw new Error('Failed to get embed URL');

    const { embedUrl } = await response.json();
    document.getElementById('streamline-embedded-workflow').src = embedUrl;
  })();
</script>

Step 5: Listen for postMessage Events

The workflow inside the iframe sends events to the parent window via postMessage, same as the simple embed flow. Always validate the message origin before handling data.

Allowed origin: Only accept messages from your Streamline origin, e.g. https://us.streamline.intellistack.app (or your region's host).

Event format: Messages are objects with type and payload. Streamline event types use the prefix streamline:.

Minimal listener with origin check:

const ALLOWED_ORIGINS = ['https://us.streamline.intellistack.app'];

function isOriginAllowed(origin) {
  return ALLOWED_ORIGINS.some((allowed) => origin.startsWith(allowed));
}

const iframe = document.getElementById('streamline-embedded-workflow');

window.addEventListener('message', (event) => {
  if (!isOriginAllowed(event.origin)) {
    return; // Ignore messages from other origins
  }

  const { type, payload } = event.data || {};
  if (!type || !type.startsWith('streamline:')) {
    return;
  }

  switch (type) {
    case 'streamline:ready':
      // Embed has loaded and is ready
      // payload: { timestamp, sessionId }
      break;
    case 'streamline:resize':
      // payload: { width, height } - both are emitted (pixels)
      handleResize(payload);
      break;
    case 'streamline:session:completed':
      console.log('Workflow completed', payload?.sessionId);
      break;
    case 'streamline:session:failed':
      console.warn(
        'Workflow failed or expired',
        payload?.sessionId,
        payload?.error,
      );
      break;
    default:
      break;
  }
});

function handleResize(payload) {
  if (payload && payload.height > 0) {
    iframe.style.height = `${payload.height}px`;
  }
}

For security purposes, never process or trust event.data without checking