> ## Documentation Index
> Fetch the complete documentation index at: https://api.fanvue.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fan surface and creator surface messages

> Every fanvue:experience:* postMessage request and result between your iframes and Fanvue, with payloads, origin checks and rate limits.

Your creator surface and fan surface talk to Fanvue with `window.parent.postMessage`, using the messages in the table. Analytics events and host-rendered dialogs travel over a different channel, the [host bridge](/docs/app-store/sdk/bridge).

Fanvue accepts a message only when `event.source` is the iframe and the origin is the launched app origin. It replies to that origin, and drops malformed messages silently.

## Messages

| Request type | Surface | Payload | Result type | Result payload |
| - | - | - | - | - |
| `fanvue:experience:publish-request` | creator | `{ token }` | `fanvue:experience:publish-result` | `{ status: "published" \| "cancelled", experienceId?, experience?, reason? }` |
| `fanvue:experience:unpublish-request` | creator | `{ token }` | `fanvue:experience:unpublish-result` | `{ status: "unpublished" \| "cancelled", reason? }` |
| `fanvue:experience:price-request` | creator | `{ externalActionId }` | `fanvue:experience:price-result` | `{ status: "saved" \| "cancelled" \| "failed", externalActionId, amountMinorUnits?, currency? }` |
| `fanvue:experience:purchase-request` | fan | `{ externalActionId, clientReferenceId? }` | `fanvue:experience:purchase-result` | `{ status: "succeeded" \| "failed" \| "cancelled", externalActionId, clientReferenceId?, purchaseReference? }` |
| `fanvue:experience:topup-request` | fan | `{ amountMinorUnits, clientReferenceId? }` | `fanvue:experience:topup-result` | `{ status: "succeeded" \| "failed" \| "cancelled", clientReferenceId?, invoiceNumber? }` |
| `fanvue:experience:consent-request` | fan | `{ clientReferenceId? }` | `fanvue:experience:consent-result` | `{ status: "granted" \| "declined" \| "failed", clientReferenceId? }` |
| `fanvue:experience:close-request` | fan | `{ type }` only | none | The dialog closes; honoured only in dialog presentation |

Every message carries its `type` as a field. The other fields:

* `token` is the opaque request token from `POST /experiences/request-token`.
* `clientReferenceId` is up to 200 characters, and is echoed on the result and on the settled-purchase webhook.
* `amountMinorUnits` and `priceCents` are USD cents.
* `reason` is set on a `cancelled` publish or unpublish result when the ending was not the creator's choice; the values are on [Publish experiences](/docs/app-store/experiences/publish#request-token-and-creator-confirmation).

A `purchase-request` never carries an amount, because Fanvue resolves the price from the catalogue. A `price-result` of `saved` without `amountMinorUnits` means the creator withdrew the action.

## Rate limits

Each fan bridge has its own token bucket with a burst of 3 and a refill of 10 per minute. A purchase, top-up or consent request over budget answers `failed`. A close request over budget is ignored.

## Origin checks

Validate `event.origin` on every reply with `isFanvueOrigin`, which requires the protocol `https:` and the host `fanvue.com` or a `*.fanvue.com` subdomain.

Fanvue posts replies to the vouched origin of your registered surface URL, so the browser already refuses delivery anywhere else. The check defends against a sibling frame on a shared origin.

## SDK coverage

The SDK ships `isFanvueOrigin`, plus schemas and type guards for the four publish and unpublish messages: `PublishRequestMessageSchema`, `PublishResultMessageSchema`, `UnpublishRequestMessageSchema`, `UnpublishResultMessageSchema`, `isPublishResultMessage` and `isUnpublishResultMessage`. Price, purchase, top-up, consent and close have no SDK helpers, so post those by hand.

## Listener

One listener can validate the origin and dispatch on `type`.

```ts theme={null}
import { isFanvueOrigin } from "@fanvue/builder-sdk";

window.addEventListener("message", (event) => {
  if (!isFanvueOrigin(event.origin)) return;
  const message = event.data;
  if (!message || typeof message.type !== "string") return;

  switch (message.type) {
    case "fanvue:experience:publish-result":
    case "fanvue:experience:unpublish-result":
    case "fanvue:experience:price-result":
      handleCreatorResult(message);
      break;
    case "fanvue:experience:purchase-result":
    case "fanvue:experience:topup-result":
    case "fanvue:experience:consent-result":
      handleFanResult(message);
      break;
  }
});
```

<Accordion title="One request per type">
  ```ts theme={null}
  const post = (message: Record<string, unknown>) => window.parent.postMessage(message, "*");

  // Creator surface
  post({ type: "fanvue:experience:publish-request", token: publishToken });
  post({ type: "fanvue:experience:unpublish-request", token: unpublishToken });
  post({ type: "fanvue:experience:price-request", externalActionId: "critique" });

  // Fan surface
  post({ type: "fanvue:experience:purchase-request", externalActionId: "critique", clientReferenceId: "round-42" });
  post({ type: "fanvue:experience:topup-request", amountMinorUnits: 1000, clientReferenceId: "topup-1" });
  post({ type: "fanvue:experience:consent-request", clientReferenceId: "consent-1" });
  post({ type: "fanvue:experience:close-request" });
  ```
</Accordion>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.