---
title: "Iframe messages"
description: "The Flume iframe and your page talk with six postMessage types. Four go from the iframe to your page and two go back. This page lists each payload."
url: https://docs.tryflume.ai/embed/messages
updated: 2026-11-11
---

# Iframe messages

> The Flume iframe and your page talk with six postMessage types. Four go from the iframe to your page and two go back. This page lists each payload.

The iframe and your page talk with `window.postMessage`. There are six message types: four from the iframe to your page, two from your page to the iframe.

## The envelope

Every message, in both directions, has this shape.

```json
{
  "version": 1,
  "type": "flume:iframe_ready",
  "session_uuid": "c1f0a6de-3b52-4d8e-9c47-2f8a1b6d0e93",
  "payload": {}
}
```

| Field | Type | Notes |
|---|---|---|
| `version` | number | Always `1` |
| `type` | string | One of the six types below |
| `session_uuid` | string | The session from `POST /start-iframe-session` |
| `payload` | object | Depends on `type` |

## Checks your page must make

Drop a message unless all four hold.

| Check | Why |
|---|---|
| `event.origin` equals the iframe's origin | Another frame cannot pose as Flume |
| `event.source` is your iframe's `contentWindow` | Another window on that origin cannot pose as this iframe |
| `data.version` is `1` | A future version is ignored safely |
| `data.session_uuid` is your session's | A stale iframe cannot write to a new item |

Ignore any `type` you do not know. The [host snippet](https://docs.tryflume.ai/embed/host-page.md#the-host-snippet) does all of this.

## All messages

**Diagram: Every message, and which way it goes.** All six postMessage types between the iframe and your page, in the order one open usually sends them. Four come to your page, two go back.

```mermaid
sequenceDiagram
  participant iframe as Flume iframe
  participant page as Your page
  iframe-->>page: flume:iframe_ready
  iframe-->>page: flume:user_connected (not every time)
  iframe-->>page: flume:save_item
  page-->>iframe: flume:item_saved
  page-->>iframe: flume:item_save_failed (not every time)
  iframe-->>page: flume:close_iframe
```

1. The iframe posts flume:iframe_ready once it has painted, with an empty payload. Clear the ready timer and show the iframe.
2. First open only: the iframe posts flume:user_connected with a connect_code. Send it to your backend at once. The iframe expects no reply.
3. The iframe posts flume:save_item with the save payload, which carries save_uuid. It waits 60 seconds for your reply.
4. Your page replies flume:item_saved with the same save_uuid, and item_ref and item_label when you have them.
5. Or your page replies flume:item_save_failed with the same save_uuid and a reason: permission, conflict, storage or error.
6. The iframe posts flume:close_iframe with a reason of done, closed or expired. Remove the iframe. On expired, start a new session.

| Type | Direction | Payload | When |
|---|---|---|---|
| [`flume:iframe_ready`](#flumeiframe_ready) | Iframe to page | Empty | The iframe has loaded |
| [`flume:user_connected`](#flumeuser_connected) | Iframe to page | `connect_code` | The user verified their email |
| [`flume:save_item`](#flumesave_item) | Iframe to page | The save payload | The user saved a product |
| [`flume:close_iframe`](#flumeclose_iframe) | Iframe to page | `reason` | The iframe should be removed |
| [`flume:item_saved`](#flumeitem_saved) | Page to iframe | `save_uuid`, your ids | Your write worked |
| [`flume:item_save_failed`](#flumeitem_save_failed) | Page to iframe | `save_uuid`, `reason` | Your write failed |

## From the iframe to your page

### flume:iframe_ready

The iframe has loaded and drawn its first screen.

- Payload: `{}`.
- Do this: clear your ready timer, remove your spinner, move focus into the iframe.
- It is also sent when the iframe loaded but could not start, for example with a used launch code. The iframe then shows its own error, with a retry and a close button. Your page cannot tell the two cases apart, and does not need to: clear the timer in both.
- After a failed start, the iframe's close button sends `flume:close_iframe` with `closed`, and its retry sends `expired`.

### flume:user_connected

The user verified their email inside the iframe.

| Payload field | Type | Notes |
|---|---|---|
| `connect_code` | string | Single use. Valid for 120 seconds |

```json
{
  "version": 1,
  "type": "flume:user_connected",
  "session_uuid": "c1f0a6de-3b52-4d8e-9c47-2f8a1b6d0e93",
  "payload": { "connect_code": "connect_example_code_0000" }
}
```

- Do this: send `connect_code` to your backend. Call [`POST /users/exchange-connect-code`](https://docs.tryflume.ai/embed/server-calls.md#exchange-a-connect-code). Store the `access_token` over any token you held for the user.
- Any token you held for this user stopped working when they verified. See [Users and verification](https://docs.tryflume.ai/concepts/users.md#what-happens-when-a-user-verifies-in-the-iframe).
- If you skip the exchange, you hold no working token. The user verifies again on the next open.

### flume:save_item

The user confirmed a save. The payload is the full [save payload](https://docs.tryflume.ai/concepts/save-payload.md), one JSON object.

| Payload field | Type | Notes |
|---|---|---|
| `save_uuid` | string (UUID) | Identifies this save. A retry carries the same value |
| `schema_version` | string | `"1.0"` |
| Other fields | | See [Save payload](https://docs.tryflume.ai/concepts/save-payload.md) |

- Do this: write the item into your records, then reply with `flume:item_saved` or `flume:item_save_failed`.
- Reply within 60 seconds. Silence counts as a failure.
- If the user presses "Try again", you get the same `save_uuid` again. Do not write a second record.

### flume:close_iframe

The iframe is done. Remove it from your page.

| Payload field | Type | Values |
|---|---|---|
| `reason` | string | `done`, `expired` or `closed` |

| `reason` | Meaning | Do this |
|---|---|---|
| `done` | The user finished after a save | Remove the iframe. Refresh your data |
| `expired` | The session ended, or the user pressed Try again after a failed start | Start a session again and remount |
| `closed` | The user pressed X or Escape | Remove the iframe |

```json
{
  "version": 1,
  "type": "flume:close_iframe",
  "session_uuid": "c1f0a6de-3b52-4d8e-9c47-2f8a1b6d0e93",
  "payload": { "reason": "done" }
}
```

## From your page to the iframe

Post replies to the iframe's window, with the iframe's origin as the target origin. Never use `"*"`.

```js
frame.contentWindow.postMessage(
  { version: 1, type: "flume:item_saved", session_uuid: sessionUuid, payload },
  new URL(iframeUrl).origin,
);
```

The iframe accepts a reply only when all of these hold:

- it comes from the page that framed it,
- that page's origin is the `host_origin` you sent,
- it carries the session's `session_uuid`,
- its `save_uuid` is the save the iframe is waiting on.

A reply that fails one of these is ignored, and the save waits out its 60 seconds.

### flume:item_saved

Your write worked.

| Payload field | Type | Required | Notes |
|---|---|---|---|
| `save_uuid` | string (UUID) | Yes | From the `flume:save_item` you are answering |
| `item_ref` | string | No | Your id for the record. Max 128 characters |
| `item_label` | string | No | Your label for it. Max 200 characters |

```json
{
  "version": 1,
  "type": "flume:item_saved",
  "session_uuid": "c1f0a6de-3b52-4d8e-9c47-2f8a1b6d0e93",
  "payload": { "save_uuid": "2b9d6f1c-7a34-4e58-b0c2-9d1e3f5a7b6c", "item_ref": "row_9931", "item_label": "T-1.A" }
}
```

### flume:item_save_failed

Your write failed. The iframe shows the user a retry.

| Payload field | Type | Required | Notes |
|---|---|---|---|
| `save_uuid` | string (UUID) | Yes | From the `flume:save_item` you are answering |
| `reason` | string | Yes | `permission`, `conflict`, `storage` or `error` |
| `message` | string | No | Detail for Flume's logs. Cut at 500 characters. Never shown to the user |

| `reason` | Use it when |
|---|---|
| `permission` | The user may not edit that record |
| `conflict` | The record changed or is locked |
| `storage` | Your write failed |
| `error` | Anything else |

The payload must be a plain object. A thrown `Error` cannot be sent as it is. The host snippet turns one into a proper payload.

## What does the user see around a save?

**Diagram: Every way a save can end.** Three outcomes of one flume:save_item. Two of them show the user the same message, and a retry repeats the same save_uuid.

```mermaid
flowchart LR
  request["flume:save_item / carries save_uuid"]
  ok["flume:item_saved / your write worked"]
  failed["flume:item_save_failed / permission, conflict, / storage or error"]
  silent["No reply / within 60 seconds"]
  saved["Saved screen / shows item_label"]
  retry["Could not save / Close, or Try again"]
  request --> ok
  request --> failed
  request --> silent
  ok --> saved
  failed --> retry
  silent --> retry
```

1. The iframe posts flume:save_item with a save_uuid.
2. Your page replies flume:item_saved: the user sees the saved screen, with your item_label when you send one.
3. Your page replies flume:item_save_failed with a reason of permission, conflict, storage or error: the user sees one message with Close and Try again. The reason is not shown.
4. Your page does not reply within 60 seconds: the user sees the same message.
5. Try again posts flume:save_item again with the same save_uuid. Your write must be safe to repeat.

| Case | The user sees |
|---|---|
| You reply `flume:item_saved` | A saved screen. It shows `item_label` when you send one, as "Saved as ...". Otherwise your item's own label |
| You reply `flume:item_save_failed`, with any `reason` | One message: the save to your product failed, with **Close** and **Try again**. `reason` and `message` are never shown |
| You do not reply within 60 seconds | The same message |
| Your reply arrives after that | A late reply for the same `save_uuid` can still be applied while that save is the current one. Do not rely on it: reply within the time limit |
| Flume could not build the payload | An error from Flume. Your page receives no `flume:save_item` |

- **Try again** sends `flume:save_item` again with the same `save_uuid`. A slow write of yours may have finished meanwhile, so the write must be safe to repeat.
- After a save, the user can press **Done**, which sends `flume:close_iframe` with `done`, or open the product in Flume, which keeps the iframe open.

## Rules the iframe follows

- It posts only to the `host_origin` you sent when you started the session. If your page is on another origin, no message reaches it.
- It reports each save's result to Flume itself. Your backend makes no call for it.
- One exception: when the iframe cannot start, it has no session. It then posts only `flume:iframe_ready` and `flume:close_iframe`, to the page that framed it.

## What is not sent?

- No resize message. You size the iframe.
- No error message. The iframe shows its own error screen when it cannot start. Your ready timer covers a frame that never loads.
- No message per search or per quote. Only a save reaches your page.

## Versioning

`version` is `1`. A message with another version, or a type you do not know, must be ignored.
