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 ───► │
│ │ │- Your server builds the JSON payload and computes the HMAC-SHA256 signature using the shared secret.
- Your server POSTs to the Incoming Webhook endpoint with the
Streamline-Signatureheader. - Streamline validates the signature, creates a workflow session, and returns a
resumeUrlin the JSON body. - Your server passes that URL (with
?source=embedappended) to the client. - The client renders the URL in an iframe - all
postMessageevents work the same as the simple embed flow.
HMAC Signature Basics
| Detail | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Header | Streamline-Signature: sha256={hex} |
| Input | Exact raw request body bytes (UTF-8) |
| Secret | Your shared secret from Streamline |
Validation flow inside Streamline
- Your system computes
HMAC-SHA256(rawBodyBytes, sharedSecret). - Your system sends the hex digest as header
Streamline-Signature: sha256={digest}. - Streamline computes the same digest using the raw bytes it receives.
- 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
- Build the exact JSON payload string you will send.
- Convert the payload to raw bytes (UTF-8).
- Compute HMAC-SHA256 with your shared secret.
- Hex-encode the digest (lowercase).
- Set the
Streamline-Signatureheader.
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=embedThis 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
