---
title: "Security and CSP"
description: "The Flume iframe frames only on origins you agreed with Flume. This page covers allowed origins, the headers Flume sets, your CSP and what to trust."
url: https://docs.tryflume.ai/embed/security
updated: 2026-11-11
---

# Security and CSP

> The Flume iframe frames only on origins you agreed with Flume. This page covers allowed origins, the headers Flume sets, your CSP and what to trust.

The Flume iframe loads only inside pages on origins you agreed with Flume. Your partner key and user access tokens stay on your server. This page lists what Flume enforces, what your page must allow, and what to trust.

## What do you agree with Flume?

Flume holds three lists per partner. Flume sets them. You cannot read or change them yourself today.

| List | Format | Used for |
|---|---|---|
| Allowed origins | Exact origins: scheme, host and port, no path, no trailing slash | Which pages may frame the iframe. Checked against `host_origin` on every session start |
| Image hosts | Exact host names | Which hosts Flume may fetch `item.image_urls` from |
| Redirect hosts | Exact host names | Which hosts a deep link's `redirect_uri` may name |

- To add or change an entry, [ask Flume](https://docs.tryflume.ai/support.md).
- A change to allowed origins can take up to 60 seconds to apply.
- Sandbox and live each have their own lists.
- A local origin such as `http://localhost:3000` can be an allowed origin in the sandbox. See [Develop on localhost](https://docs.tryflume.ai/embed/host-page.md#develop-on-localhost).

## What does Flume enforce on the iframe?

Flume sends these headers with the iframe document.

| Header | Value | Effect |
|---|---|---|
| `Content-Security-Policy` | `frame-ancestors` followed by your allowed origins | Only your pages can frame it |
| `Content-Security-Policy` | `frame-ancestors 'none'` for an unknown or inactive partner | The frame stays blank |
| `Cache-Control` | `no-store` | The document is never cached |
| `Referrer-Policy` | `no-referrer` | The iframe URL is not leaked in a referrer |
| `X-Content-Type-Options` | `nosniff` | The browser does not guess content types |

Also:

- The iframe runs only when framed. Opened in its own tab, it does not start.
- The iframe's own policy allows scripts only from Flume's own hosts and its analytics host, and blocks nested frames.
- The launch code travels in the URL fragment, which browsers do not send to a server.
- The iframe removes the launch code from its URL as soon as it has read it.

## What must your page's CSP allow?

If your page sends a `Content-Security-Policy`, add these.

| Directive | Add | Why |
|---|---|---|
| `frame-src` | The iframe origin | To frame the iframe |
| `img-src` | The origin of the image links | Only if your page shows Flume image links directly |
| `connect-src` | The origin of the image links | Only if your page fetches image bytes with `fetch` |

- The iframe origin is `new URL(iframe_url).origin`. Read it once per environment from a real response, then add it to your policy.
- The origin of the image links is the origin of a link in a real payload, for example `new URL(payload.product.gallery[0].urls[key]).origin`. Read it once per environment.
- Do not assume either origin equals the host your server calls. In the sandbox they can differ.
- Ask Flume before go-live whether those origins are about to change.
- If your page sends `Cross-Origin-Embedder-Policy: require-corp`, the browser blocks the iframe.

## What must always hold?

Twelve rules. An integration that breaks one is not finished.

| # | Rule | Detail |
|---|---|---|
| 1 | The partner key and every user access token stay on your server. Never in a browser, a client bundle, a log or a commit | [Keys and tokens](https://docs.tryflume.ai/concepts/authentication.md) |
| 2 | Only your server calls Flume. A server call cannot be made from a browser | [Server calls](https://docs.tryflume.ai/embed/server-calls.md) |
| 3 | Every server call sends `X-Flume-Partner-Key`. `POST /start-iframe-session` also sends `X-Flume-User-Token` whenever you hold a token for the user | [Server calls](https://docs.tryflume.ai/embed/server-calls.md#start-a-session) |
| 4 | `iframe_url`, its launch code and the connect code are single use and short lived. Use each at once. Never store, log, rebuild or edit them | [Timeouts and expiry](https://docs.tryflume.ai/embed/timeouts.md) |
| 5 | Read the iframe's origin from `iframe_url`. Never hardcode it | [Set up your page](https://docs.tryflume.ai/embed/host-page.md) |
| 6 | Drop a message unless its origin, source, `version` and `session_uuid` are right. Ignore types you do not know | [Messages](https://docs.tryflume.ai/embed/messages.md#checks-your-page-must-make) |
| 7 | Reply to the iframe with its origin as the target origin, never `"*"` | [Messages](https://docs.tryflume.ai/embed/messages.md#from-your-page-to-the-iframe) |
| 8 | Answer every `flume:save_item` within 60 seconds, with `flume:item_saved` or `flume:item_save_failed`, carrying its `save_uuid`. A repeated `save_uuid` writes nothing twice | [Messages](https://docs.tryflume.ai/embed/messages.md#flumesave_item) |
| 9 | Exchange every connect code at once. Store the new token over the old one | [Users and verification](https://docs.tryflume.ai/concepts/users.md) |
| 10 | Run a ready timer of 15 seconds. When it fires, remove the iframe and show an error with a retry | [Timeouts and expiry](https://docs.tryflume.ai/embed/timeouts.md) |
| 11 | On `401` `USER_TOKEN_INVALID`, delete the stored token and start again without it. On `429`, wait for `Retry-After`. Branch on `error.code`, never on `error.message` | [Errors](https://docs.tryflume.ai/api/errors.md) |
| 12 | The save payload came through the browser. Render its text as text, and do not treat a saved price as a commitment | [Below](#can-you-trust-the-save-payload) |

The [host snippet](https://docs.tryflume.ai/embed/host-page.md#the-host-snippet) keeps rules 5 to 8 and 10 for you when you copy it unchanged.

## What must your page check?

Your `message` listener receives every message sent to your window. Make the [four checks](https://docs.tryflume.ai/embed/messages.md#checks-your-page-must-make) on each one, and reply with the iframe's origin as the target, never `"*"`. The [host snippet](https://docs.tryflume.ai/embed/host-page.md#the-host-snippet) does both.

## Can you trust the save payload?

The save payload reaches your backend through the user's browser. Treat it like any other browser input.

- The four message checks prove the message came from the Flume iframe. They do not stop a user from changing a request to your own backend.
- Flume offers no server call to read the payload again. So do not treat a saved price as an order or a commitment. It is what the user saw, valid until `pricing.valid_until`.
- Check on your server that the item being written belongs to the signed-in user.
- Render every string as text. See [Text rules](https://docs.tryflume.ai/concepts/save-payload.md#text-rules).

## Which user does a token belong to?

`POST /users/exchange-connect-code` returns `external_ref`: your own id for the user who verified.

1. Compare it with the user who is signed in to your product.
2. Store the token only when they match.
3. If they differ, discard the token.

## Where do credentials live?

**Diagram: Where each secret lives and travels.** Long-lived secrets stay on servers. Only single-use, short-lived codes pass through the browser.

```mermaid
flowchart LR
  page["Your page / checks origin, source, / version, session_uuid"]
  backend["Your backend / partner key / user access tokens"]
  iframe["Flume iframe / posts only to host_origin / its own short credential"]
  flume["Flume servers / allowed origins / per partner"]
  backend -->|"iframe_url (launch code)"| page
  page -->|"connect_code"| backend
  backend -->|"partner key, user access token"| flume
  flume -->|"iframe_url, access_token"| backend
  page -.->|"save replies"| iframe
  iframe -.->|"connect_code, save payload"| page
  iframe -->|"launch code, once"| flume
```

1. Your backend holds the partner key and each user access token. It sends them only to Flume, over HTTPS.
2. Flume returns iframe_url, which carries a launch code. The launch code works once, within 120 seconds.
3. Your backend hands iframe_url to your page. Your page sets it as the iframe src and stores it nowhere.
4. The iframe spends the launch code with Flume and then uses its own short-lived credential.
5. When a user verifies, the iframe posts a connect code to your page. It works once, within 120 seconds.
6. Your page passes the connect code to your backend, which exchanges it for the user access token.
7. Your page accepts a message only when its origin, source, version and session_uuid are right. The iframe posts only to the host_origin you sent.

| Credential | Where it lives | Reaches the browser |
|---|---|---|
| Partner key | Your server | Never |
| User access token | Your server | Never |
| Connect code | Passes through your page once | Yes. Single use, 120 seconds |
| Launch code, in `iframe_url` | Passes through your page once | Yes. Single use, 120 seconds |
| The iframe's own credential | Inside the iframe | Yes, inside the iframe only. Your page cannot read it |

Flume stores every key, token and code as a hash only.

## How does Flume fetch your item photos?

When you send `item.image_urls`, Flume's server fetches the first photo.

- `https` only.
- The host must be on your image host list.
- Redirects are not followed.
- 10 second timeout, 25 MB limit.
- The links must stay reachable for the session's life.

An entry that breaks a rule is a `422` `VALIDATION_FAILED` naming `item.image_urls.<index>`.

## What should you treat as untrusted?

- Every string in the [save payload](https://docs.tryflume.ai/concepts/save-payload.md). Flume removes control characters and angle brackets only. Quotes and `&` remain.
- Render strings as text. Escape them on output. Never build HTML or an HTML attribute from them by joining strings.
- Every message, until it passes the four checks above.

## How do you report a security problem?

[Contact Flume](https://docs.tryflume.ai/support.md). Send the key prefix and `X-Request-Id` values. Never send a key or a token.
