Skip to main content
By the end of this page your fan surface turns a launch token into a verified fan session and renders the right screen: the experience for an entitled fan, a locked screen for everyone else. Fanvue loads your fan experience URL in an iframe with a fresh launch token in the query string, and your server exchanges it for the fan’s entitlement before rendering anything. You need:
  • an on-platform app whose creator surface already works, as in Run your app inside Fanvue
  • a stored app access token with the read:experience scope, as in Store tokens
  • a public https domain of your own for the fan surface

Declare the URL

Declare your fan experience URL before the first publish. Set it in Embed settings in the Developer Area, or in your manifest as access.fanExperienceUrl with a surfaces[] entry whose surface is fan_experience. Fans can’t open the surface until an experience exists, which is what Publish experiences covers. The URL must be https on a public domain of your own. Fanvue refuses Fanvue domains, localhost, IP addresses, dev tunnels and preview deploys.

Query parameters

The SDK reads the first two with getSessionTokenFromUrl and getThemeFromUrl. It has no reader for presentation, so read that one with URLSearchParams.

Exchange the token

Exchange the token from your server with POST /experiences/token/exchange. The call needs the read:experience scope, and the token works only with the credentials of the app that owns the experience. The SDK version below also signs a fan session for your own routes.
200 response
walletBalance is the fan’s wallet balance in USD cents, or null. null means the fan has no wallet at all, not an empty one, so don’t offer a wallet charge in that case. The balance is a snapshot, never an authorisation. HIDDEN experiences skip the entitlement check at exchange, because holding a valid token is the share-link capability. Their entitlement.reason reads hidden_token_exchange. The SDK differs from the raw endpoint in four ways:
  • createFanSessionHandler answers a denial as 200 { entitled: false, accessMode, reason }, so your page can render a locked screen.
  • It limits the route to 30 requests per minute per IP. The creator session route allows 10.
  • client.experiences.exchangeExperienceToken returns one of seven outcomes: entitled, denied, expired, binding_mismatch, unavailable, rate_limited and upstream_error.
  • The SDK schema drops walletBalance, so call the endpoint directly if you need the balance.
Protect every later request with requireFanSession(request, { sessions }), which rejects a missing or revoked fan session with 401.

Branch on entitlement

Branch on isEntitled, not on reason, because the list of reasons grows without an API version change. Fanvue reads purchases from the payment ledger on every launch, so a refund or chargeback revokes access on the fan’s next launch.

Sandbox and headers

Fanvue renders your surface with the sandbox allow-scripts allow-forms allow-popups allow-same-origin. It adds allow-same-origin at render time, after checking that your origin isn’t a Fanvue origin, so your cookies, storage and same-origin API calls work. allow-modals and allow-downloads are absent. window.alert and window.confirm do nothing, and downloads are blocked inside the frame and in any tab it opens. Your response must carry Content-Security-Policy: frame-ancestors 'self' https://fanvue.com https://*.fanvue.com. Hosting lists the full header set.

Page or dialog

When a fan clicks an experience card on a chat or a profile, presentation on your fan_experience surface decides whether it opens as a full page or in a dialog over the chat or profile.
app-manifest.json excerpt
  • type is page (the default) or dialog.
  • desktopWidth and desktopHeight are integers from 240 to 1600 CSS pixels, both or neither. Both omitted means as large as the viewport allows.
  • Phones always show a dialog full screen.
  • On desktop the sizes describe your content. Fanvue adds its own chrome and clamps the dialog to the viewport, so measure your real size from the window rather than assuming the declared one.
  • Direct links and the detail page always open the page.
To close the dialog from inside, post { type: "fanvue:experience:close-request" } to window.parent. Fanvue honours it only in a dialog, under the same origin and rate rules as the other fan bridges. Send it when the fan presses Escape inside your frame, because key events in a cross-origin iframe never reach Fanvue.
Without a manifest, set the same thing in the Developer Area. Embed settings has an Open as select with Page or Dialog, and for a dialog a choice between as large as possible and Fixed size. When your manifest manages the surface, those fields are read-only and point you to presentation in the manifest.

External delivery

EXTERNAL experiences show an Open button with a note that your app opens in a new tab outside Fanvue. The fan’s click opens externalUrl in a new tab with ?token= appended. Exchange the token the same way; nothing else differs.

Test before approval

Install your draft app from App details, open it as the creator and publish an experience. Test your app walks through the install flow, and Test before approval explains who can reach a draft experience.