- 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
httpswith 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 overhttps 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
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 neithererrornorjwtnetwork_error, when the request never completed
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.
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.
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:
- Your server generates a PKCE verifier and
state, then callsPOST /api/v1/app-integrations/authorize-on-behalfon the platform with the session token as a Bearer token and the PKCE challenge. - The platform runs the OAuth authorisation as the creator and returns an authorisation code bound to your challenge.
- Your server exchanges the code at the token endpoint with your client secret and verifier.
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 variablescreateConfig 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_challengeis rejected. The SDK generates it. OAUTH_REDIRECT_URIequals the first redirect URI registered on your app. The redirect URI is never visited.- A
403 consent_requiredmeans 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.
stateis verified by the SDK on every exchange. The client secret and PKCE verifier never leave your server.
Appendix: the manual flow
This is everythingexchangeSessionToken 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
Readtoken and theme from the URL. Send the token to your own server and nowhere else.
Backend
The server generates PKCE andstate, asks the platform for a code, then exchanges it. Node 18 or later.
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.