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 event.origin against your allowlist first.
Resize events: The embed sends streamline:resize when its content size changes. Events are debounced (≈150 ms), so you may see a brief delay between content changes and the event. Each payload includes width and height (numbers, pixels). The handleResize example only applies height to the iframe; set iframe.style.width from payload.width when you need to match or constrain width.
You can use streamline:session:completed and streamline:session:failed to e.g. show a thank-you message, redirect, or track analytics - see Handle session completion and failure.
See Event reference for the full list of events and payloads.
Examples
Each example shows the server-side flow: sign → POST → read resumeUrl from the response.
JavaScript (Node.js)
const crypto = require('crypto');
const axios = require('axios');
const webhookId = '00000000-0000-0000-0000-000000000000';
const secret = 'your_secret_here';
const payload = JSON.stringify({
event: 'patient.created',
patientId: '123',
});
const signature = crypto
.createHmac('sha256', secret)
.update(Buffer.from(payload, 'utf8'))
.digest('hex');
const response = await axios.post(
`https://us.streamline.intellistack.ai/v1/webhooks/incoming/${webhookId}`,
payload,
{
headers: {
'Content-Type': 'application/json',
'Streamline-Signature': `sha256=${signature}`,
},
},
);
// response.data.resumeUrl — append ?source=embed and pass to your iframe
const resumeUrl = response.data.resumeUrl;
const embedUrl = `${resumeUrl}?source=embed`;Python
import hashlib
import hmac
import json
import requests
webhook_id = "00000000-0000-0000-0000-000000000000"
secret = b"your_secret_here"
payload = json.dumps(
{"event": "patient.created", "patientId": "123"},
separators=(",", ":"),
)
signature = hmac.new(secret, payload.encode("utf-8"), hashlib.sha256).hexdigest()
response = requests.post(
f"https://us.streamline.intellistack.ai/v1/webhooks/incoming/{webhook_id}",
data=payload,
headers={
"Content-Type": "application/json",
"Streamline-Signature": f"sha256={signature}",
},
timeout=10,
)
resume_url = response.json()["resumeUrl"]
embed_url = f"{resume_url}?source=embed"Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
String webhookId = "00000000-0000-0000-0000-000000000000";
String secret = "your_secret_here";
String payload = "{\"event\":\"patient.created\",\"patientId\":\"123\"}";
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String signature = Hex.encodeHexString(digest);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://us.streamline.intellistack.ai/v1/webhooks/incoming/" + webhookId))
.header("Content-Type", "application/json")
.header("Streamline-Signature", "sha256=" + signature)
.POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON and read resumeUrl, append ?source=embed for the iframecURL (Quick Testing)
WEBHOOK_ID="00000000-0000-0000-0000-000000000000"
SECRET="your_secret_here"
PAYLOAD='{"event":"patient.created","patientId":"123"}'
SIG=$(printf "%s" "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST "https://us.streamline.intellistack.ai/v1/webhooks/incoming/${WEBHOOK_ID}" \
-H "Content-Type: application/json" \
-H "Streamline-Signature: sha256=${SIG}" \
-d "$PAYLOAD"Troubleshooting
Signature mismatch: body changed after signing
Symptom: 401 Unauthorized - authentication failed.
Cause: The payload string you signed differs from the bytes actually sent over HTTP.
Fix: Sign the exact serialized payload string that you send. Do not modify the body after signing.
Signature mismatch: whitespace differences
Symptom: Valid logic but still 401.
Cause: Signing pretty-printed JSON but sending minified JSON, or vice versa.
Fix: Serialize once and reuse that exact string for both signing and the request body.
Signature mismatch: encoding differences
Symptom: Intermittent 401, especially with non-ASCII characters.
Cause: Signing UTF-16 bytes but sending UTF-8 bytes (common in some .NET and Java environments).
Fix: Explicitly encode the payload as UTF-8 before signing and sending.
Missing header
Symptom: 401 Unauthorized.
Cause: Streamline-Signature header not included in the request.
Fix: Include Streamline-Signature: sha256={hex} on every request.
Malformed header
Symptom: 400 Bad Request.
Cause: Missing sha256= prefix or invalid hex digest.
Fix: Ensure the header format is exactly sha256={hex} - lowercase hex, no spaces.
Testing During Development
- Use
POST /v1/webhooks/incoming/{webhookId}?test=trueduring implementation. Test mode still validates HMAC when auth is configured. - Start from a known-good payload and a fixed secret, then compare digests.
- Log both the payload string and computed signature before sending.
- If test mode returns
401, verify the raw bytes, encoding, and header format first.
Security
- Shared secret management: Treat the secret like a password. Store it in environment variables or a secrets manager - never commit it to source control or expose it to the client.
- Server-side only: HMAC signing must happen on your backend. Never include the shared secret in client-side JavaScript.
- Origin validation: When listening for
postMessageevents from the iframe, always checkevent.originagainst a fixed allowlist of Streamline origins. - HTTPS: Serve your page and communicate with Streamline over HTTPS in production.
- Replay protection: HMAC alone does not prevent replay attacks. If replay protection is critical, include a timestamp or nonce in your payload and validate it server-side.
Updated about 4 hours ago
