Skip to main content
By the end of this page your creator surface loads inside Fanvue, signs the creator in without a login screen, and calls the Fanvue API from your server. Fanvue opens the surface in an iframe with a one-time session token in the URL, and the SDK trades that token for OAuth tokens on your server. The page fans see is a separate surface; for that, follow Build the fan surface. You need:
  • an app in the Developer Area with the On-platform type available on your account (Choose your app type)
  • a Next.js App Router project with React 18 or later, served over https with a browser-trusted certificate
  • a domain you control, if you configure the app through an App Manifest

Register your app

1

Create the app and save its credentials

In the Developer Area, click Create app. The dialog that opens shows the Client ID and the Client Secret, and the secret is shown only this once. Save it now; after the dialog closes, your only option is Reset secret on the Authentication tab.
2

Set the creator surface URL

Enter it in Embed settings on App details, or serve an App Manifest with an on-platform access block.
app-manifest.json
access.url is the creator surface URL Fanvue loads in the iframe. No browser ever visits oauth.redirectUris[0], but it still matters. Fanvue binds the authorisation code to the first registered redirect URI, and the SDK sends OAUTH_REDIRECT_URI at exchange, so the two must be the same string. The access reference defines every field.
3

Connect the domain and submit

Enter your-app.com in App domain on the App details tab and confirm the Versions tab shows Up to date. Complete the listing and click Submit for review. The access block takes effect on submission, and from then on the Debug tab previews the surface for you alone.

Hosting

Serve the creator surface over https with a browser-trusted certificate, and serve the manifest at https://<app domain>/app-manifest.json with no redirect. The client secret stays on your server. Fanvue can only frame a page whose Content-Security-Policy allows it. withFanvueHeaders(nextConfig, options?) adds the headers every surface needs and merges them with the rules your next.config already declares. Your own rules come after Fanvue’s, so a Content-Security-Policy you set yourself wins.
next.config.mjs
The frame-ancestors wildcard covers every Fanvue host that embeds apps, including the Debug tab preview and the non-www shell, so one value serves production and development. The CORS block on /api/* answers cross-origin calls to your API routes. A wildcard origin is safe here because sessions travel in the Authorization header, never in cookies. Pass { includeApiCors: false, apiPathPattern: null, extraAllowedHeaders: null, extraExposedHeaders: null } only if your app answers CORS itself. fanvueHeaderRules(options?) returns the same rules as an array for a headers() you compose by hand.

Authenticate with the SDK

Install the package, then add one server route and one client component.
1

Add the session exchange route

createConfig() from the on-platform entrypoint reads the OAUTH_* variables plus FANVUE_PLATFORM_URL, which defaults to https://www.fanvue.com.
app/api/fanvue/session/route.ts
The route accepts POST { "token": "<session token>" }, runs the exchange, fetches the creator’s profile and answers { "jwt": "<session JWT>" }. The JWT is signed with SESSION_SECRET and holds the Fanvue tokens.
2

Exchange the token on the client

useEmbeddedAuth() reads ?token= on mount, posts it to /api/fanvue/session, and stores the returned JWT in sessionStorage under fanvue:jwt. useAuth() exposes the JWT and authFetch, which adds Authorization: Bearer <jwt> to your own API calls and stores any X-Updated-Session header it receives.
app/embedded/page.tsx
status is one of idle (no token in the URL and no stored JWT), exchanging, authenticated or error. On error, the error field holds one of:
  • the route’s error string
  • exchange_failed, when the response carried neither error nor jwt
  • network_error, when the request never completed
The hook runs the exchange once per page load, because the session token is single-use.
3

Call the Fanvue API from your own routes

getAuthenticatedClient({ sessionSecret, config }) from the on-platform entrypoint reads the Bearer JWT, verifies it and returns { client, refreshedJwt }. When the access token was within 30 seconds of expiry, the SDK refreshes it and returns the re-signed JWT as refreshedJwt. Set it on the response as X-Updated-Session (HEADER_UPDATED_SESSION) so authFetch stores it.
app/api/me/route.ts

Outside Next.js

useEmbeddedAuth posts to /api/fanvue/session and expects { jwt } back. On another server framework, implement that route with exchangeSessionToken(config, token) and createSessionJwt(secret, payload) from the core entrypoint, and pass exchangePath if the route lives elsewhere.
Without React, read the token with getSessionTokenFromUrl(window.location.href) and post it yourself. For a server that can’t run the SDK at all, follow the manual flow.

Session routes for apps with a token store

createCreatorSessionHandler(deps) and createFanSessionHandler(deps) are alternative bootstrap routes for apps that keep an encrypted token store and issue their own app sessions. Tokens, encryption and logging covers them alongside the token store. Both apply a per-IP rate limit through the RateLimiter you inject. The defaults are SDK defaults, not platform limits, and deps.rateLimit overrides them. createInMemoryRateLimiter() is the process-local default and tracks at most 10,000 keys (MAX_TRACKED_RATE_LIMIT_KEYS). On serverless hosts each instance counts separately, so implement RateLimiter over a shared store when the limit must hold. createSessionExchangeHandler applies no limiter.

Work on the creator’s behalf in the background

getAuthenticatedClient refreshes tokens only inside a live Bearer session. To call the API after the iframe closes, persist the refresh token when the exchange completes by passing onTokens to createSessionExchangeHandler.
app/api/fanvue/session/route.ts
onTokens receives the raw TokenResponse and the FanvueUser. If it throws, the exchange fails with 502, so the creator never gets a session whose tokens weren’t stored. Encrypt the tokens at rest and refresh them with compare-and-swap through the SDK’s token store.

Match the creator’s theme

Fanvue appends the creator’s resolved colour scheme to both the creator and fan surface URLs as ?theme=light or ?theme=dark. useEmbeddedAuth returns it as theme, typed FanvueTheme | null. It’s null when the page is opened outside Fanvue, so fall back to a default.
Outside React, getThemeFromUrl(window.location.href) from the core entrypoint returns the same value. Unlike the session token, the theme parameter is not single-use, so any client code can read it from the URL.

How authentication works

Fanvue opens your creator surface URL with two query parameters. ?token= carries a short-lived, single-use session token, which proves that this creator is using your app inside Fanvue right now. ?theme= carries the creator’s colour scheme. Your server trades the session token for OAuth access and refresh tokens through the delegated authorise-on-behalf flow:
  1. Your server generates a PKCE verifier and state, then calls POST /api/v1/app-integrations/authorize-on-behalf on the platform with the session token as a Bearer token and the PKCE challenge.
  2. The platform runs the OAuth authorisation as the creator and returns an authorisation code bound to your challenge.
  3. Your server exchanges the code at the token endpoint with your client secret and verifier.
Fanvue never sees your client secret, verifier or tokens, and your app never forges a creator’s identity. The SDK’s exchangeSessionToken runs all three steps, and createSessionExchangeHandler wraps it in a route. The platform adds openid and offline_access to the grant itself, so the tokens you receive include a refresh token. The grant carries the scopes registered on your app in the Developer Area. The authorize-on-behalf request sends no scopes, and OAUTH_SCOPES applies only to off-platform login.

What your iframe can and cannot do

Fanvue renders every creator surface and fan surface in a sandboxed iframe. The sandbox is platform policy, and no app can widen it.

Environments

Set the base URLs through the variables createConfig reads. Production values are the defaults.

Checklist

Check these before you go live.
  • PKCE is mandatory. An authorise-on-behalf request without an S256 code_challenge is rejected. The SDK generates it.
  • OAUTH_REDIRECT_URI equals the first redirect URI registered on your app. The redirect URI is never visited.
  • A 403 consent_required means the creator has not approved the in-platform consent screen, or revoked it. Only the Fanvue UI can grant consent.
  • The session token is short-lived and single-use. Exchange it immediately, send it only to your own server, and never store it. If a creator leaves the surface open and acts much later, reopening the surface issues a fresh token.
  • state is verified by the SDK on every exchange. The client secret and PKCE verifier never leave your server.

Appendix: the manual flow

This is everything exchangeSessionToken does, written out for a server that can’t run the SDK.

Endpoints

{PLATFORM}, {AUTH} and {API} are the base URLs in the Environments table. redirect_uri at the token exchange must equal the first redirect URI registered on your app, because the platform binds the code to that value. The authorise-on-behalf endpoint answers these errors:

Frontend

Read token and theme from the URL. Send the token to your own server and nowhere else.
Serve the page with the framing header.

Backend

The server generates PKCE and state, asks the platform for a code, then exchanges it. Node 18 or later.
A refresh response can carry a rotated refresh_token. Store the new value when it is present, and keep the old one when it is absent; the SDK’s session helpers do exactly that. Refresh tokens rotate and are single-use has the rotation rules.

See also