Skip to content

Full example

This is a complete iframe integration in three files: a page, the host snippet, and a server. It has no dependencies. Run it to see the iframe open on your machine.

  • A sandbox partner key. Get a sandbox key.
  • http://localhost:3000 as one of your allowed origins. Ask Flume for it with your key.
  • Node.js 20 or later, or Python 3.9 or later.

Make a folder. Save the host snippet into it:

mkdir flume-example && cd flume-example
curl -sSO "https://docs.tryflume.ai/samples/mount-flume.js"

Save this page as index.html:

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Flume iframe example</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; }
#flume { position: fixed; inset: 0 0 0 auto; width: min(470px, 100vw); height: 100dvh; border: 0; display: none; }
#flume.open { display: block; }
#status { color: gray; }
</style>
</head>
<body>
<h1>Item T-1: Calacatta look porcelain 24 x 48</h1>
<button id="open">Find alternatives</button>
<p id="status" role="status"></p>
<ul id="saved"></ul>
<iframe
id="flume"
title="Flume: find alternatives"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
></iframe>
<script type="module">
import { mountFlume } from "./mount-flume.js";
const frame = document.querySelector("#flume");
const button = document.querySelector("#open");
const status = document.querySelector("#status");
let stop = null;
const post = async (path, body) => {
const response = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
// Show what the server said: it names the Flume error code.
if (!response.ok) throw new Error(`${path} ${response.status} ${(await response.text()).slice(0, 200)}`);
return response.json();
};
// Remove the iframe and the snippet's listener and timer.
function close(message = "") {
stop?.();
stop = null;
frame.classList.remove("open");
frame.removeAttribute("src");
status.textContent = message;
button.focus();
}
async function open() {
close("Opening Flume…");
let session;
try {
session = await post("/flume/session", { itemId: "T-1" });
} catch {
return close("Could not start a session. Try again.");
}
frame.classList.add("open");
stop = mountFlume({
frame,
iframeUrl: session.iframe_url,
sessionUuid: session.session_uuid,
flumeOrigin: new URL(session.iframe_url).origin,
handlers: {
hideSpinner: () => (status.textContent = ""),
showError: () => close("Flume did not load. Try again."),
exchangeConnectCode: (connectCode) =>
// A failed exchange leaves no token: say so. The user verifies again on the next open.
post("/flume/connect", { connectCode }).catch((error) => {
console.error(error);
status.textContent = "Could not finish connecting. You will be asked to verify again next time.";
}),
saveItem: async (payload) => {
// A thrown error becomes a failure reply: the snippet adds save_uuid and a reason.
const row = await post("/flume/save", payload);
const line = document.createElement("li");
line.textContent = `${row.id}: ${payload.product.name}`;
document.querySelector("#saved").append(line);
return { save_uuid: payload.save_uuid, item_ref: row.id };
},
close: (reason) => {
close(reason === "done" ? "Saved." : "");
if (reason === "expired") open();
},
},
});
}
button.addEventListener("click", open);
</script>
</body>
</html>

Save one of these servers.

server.mjs
// server.mjs: Node.js 20 or later, no dependencies.
// FLUME_PARTNER_KEY=... node server.mjs then open http://localhost:3000
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
const API = "https://sandbox-api.tryflume.ai/partner/v1";
const KEY = process.env.FLUME_PARTNER_KEY;
const HOST_ORIGIN = process.env.HOST_ORIGIN ?? "http://localhost:3000"; // must be agreed with Flume
const USER = { external_ref: "example-user-1", email: process.env.EMAIL ?? "you@yourcompany.com" };
const tokens = new Map(); // external_ref -> user access token. Use your database, encrypted.
const saves = new Map(); // save_uuid -> your record
async function flume(path, body, userToken) {
const response = await fetch(API + path, {
method: "POST",
headers: {
"X-Flume-Partner-Key": KEY,
"Content-Type": "application/json",
...(userToken ? { "X-Flume-User-Token": userToken } : {}),
},
body: JSON.stringify(body),
});
// An error can come back without JSON (a proxy, a network-level limit). Read text first.
const text = await response.text();
let answer = {};
try {
answer = JSON.parse(text);
} catch {
answer = { error: { code: "NOT_JSON", message: text.slice(0, 200) } };
}
return { status: response.status, body: answer };
}
async function startSession() {
const body = {
user: USER,
project: { external_ref: "example-project-1", name: "Example project" },
item: { external_id: "T-1", label: "T-1", name: "Calacatta look porcelain 24 x 48", item_type: "Tile" },
host_origin: HOST_ORIGIN,
};
let answer = await flume("/start-iframe-session", body, tokens.get(USER.external_ref));
if (answer.status === 401 && answer.body.error?.code === "USER_TOKEN_INVALID") {
tokens.delete(USER.external_ref); // the stored token stopped working: open without it
answer = await flume("/start-iframe-session", body);
}
return answer;
}
async function connect({ connectCode }) {
const answer = await flume("/users/exchange-connect-code", { connect_code: connectCode });
// Store the token only for the user who is signed in to YOUR product.
if (answer.status === 200 && answer.body.external_ref === USER.external_ref) {
tokens.set(USER.external_ref, answer.body.access_token); // replaces any earlier token
}
return { status: answer.status === 200 ? 200 : 502, body: {} };
}
function save(payload) {
// save_uuid is the idempotency key: a retried save returns the first record.
if (!saves.has(payload.save_uuid)) {
saves.set(payload.save_uuid, { id: `row_${saves.size + 1}`, name: payload.product.name, url: payload.product_url });
}
return { status: 200, body: saves.get(payload.save_uuid) };
}
const routes = { "/flume/session": startSession, "/flume/connect": connect, "/flume/save": save };
const files = { "/": ["index.html", "text/html"], "/mount-flume.js": ["mount-flume.js", "text/javascript"] };
createServer(async (request, response) => {
try {
if (request.method === "GET" && files[request.url]) {
const [name, type] = files[request.url];
return response.writeHead(200, { "Content-Type": type }).end(await readFile(name));
}
if (request.method === "POST" && routes[request.url]) {
let raw = "";
for await (const chunk of request) raw += chunk;
const { status, body } = await routes[request.url](raw ? JSON.parse(raw) : {});
return response.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
}
response.writeHead(404).end();
} catch (error) {
response.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ error: String(error) }));
}
}).listen(3000, () => console.log("http://localhost:3000"));
export FLUME_PARTNER_KEY="paste-your-sandbox-key"
export EMAIL="you@yourcompany.com"
node server.mjs

Open http://localhost:3000 and press Find alternatives.

  1. The iframe opens in a drawer on the right.
  2. The first time, it asks you to verify EMAIL with a code. The sandbox sends a real email.
  3. You search by photo, pick a product, and save it.
  4. The page lists the saved item, and the drawer closes.
  5. Press the button again. The iframe opens without the email step, because the server kept your token.
PartShows
startSessionThe body of POST /start-iframe-session, and the retry without a token after 401 USER_TOKEN_INVALID
connectThe exchange, the check on external_ref, and storing the new token over the old one
savesave_uuid as the idempotency key
open in the pageflumeOrigin read from iframe_url, and the five handlers
close in the pagestop(), removing src, and reopening on expired
  • The example keeps tokens and saves in memory. Use your database, and encrypt tokens.
  • It has one hard-coded user. Use the user who is signed in to your product.
  • It trusts the payload the browser sends to /flume/save. See what to trust.
  • It copies no image bytes. See Save payload.
You seeCause
"Could not start a session"The key is wrong, or http://localhost:3000 is not one of your allowed origins. The server's response says which
A blank drawer, then "Flume did not load"Your origin is not allowed to frame the iframe. Check the browser console
"Flume didn't open." inside the drawerThe iframe_url was used late or twice. Press the button again

More causes: Timeouts and expiry.