Skip to main content
createFanvueClient(accessToken, apiBaseUrl, options?) returns a typed client bound to one access token. Each method maps to one REST operation and returns a Result, and the tables list the scope each one needs. The client ships in the core entrypoint, @fanvue/builder-sdk, and runs anywhere fetch and WebCrypto exist; all you need is an access token.

Create a client

Pass the access token and null to use https://api.fanvue.com, or a fanvue.com base URL. options accepts apiVersion (default 2025-06-26) and timeoutMs (default 10,000).
The token is fixed for the life of the client. An app that needs two tokens, for example a creator token for chats and an app-pooled token for the experience exchange, creates two clients. Every request carries Authorization: Bearer, X-Fanvue-API-Version and cache: "no-store". The transport aborts a request after the timeout. It retries at most once, and only for a GET that answered 5xx; timeouts, network failures and non-GET methods are never retried. Inside the Next.js entrypoints, getAuthenticatedClient builds the client from the session for you, so route code rarely calls createFanvueClient directly.

Handle errors

Every method resolves to ok(value) or err(ApiError), and the error’s code tells you which arm you have. message is upstream text, so log it and keep it out of response bodies.
parseFanvueErrorBody(body) normalises the platform’s error body shapes into { code, message }. The Next.js error helpers emit three app-facing codes, exported as FANVUE_APP_ERROR_CODES and NON_SESSION_401_CODES: The first two mean the Fanvue grant is gone or lacks a scope while your app session is still valid. Don’t treat them as session expiry on the client.

Current user

Every other binding calls a /v0/... path.

Chats

sendMessage checks the body against the platform’s cross-field rules before sending, so an invalid combination comes back as an API_VALIDATION_ERROR with no round trip. text is 1 to 5,000 characters (CHAT_MESSAGE_MAX_CHARS), and price is in USD cents and at least 300 (MIN_CHAT_MESSAGE_PRICE). The cross-field rules are:
  • a message needs text, media, a GIF or a template
  • a GIF excludes media, price and template
  • a price needs media to unlock
  • a preview needs a priced message with media
listMessages marks the conversation read unless you pass markAsRead: false. Pass it on every background read, otherwise the creator’s unread badge clears on messages nobody saw. beforeMessageUuid is a keyset anchor that takes precedence over page. Passing endDate suppresses markAsRead entirely. To message several fans, send sequentially. A fan who can’t be messaged answers 400, so skip that fan; a 401 should stop the run. Prices are in USD cents. A link price is 0 or between 300 (MIN_CHECKOUT_LINK_PRICE) and 1,000,000 (MAX_CHECKOUT_LINK_PRICE). classifyCheckoutForbidden(error) splits a 403 in two:
  • not_enabled: checkout links are off for this creator, and only Fanvue can turn them on.
  • missing_scope: the stored token predates write:creator, so the creator must reconnect.
For the full flow, see Create a checkout link.

Experiences

exchangeExperienceToken maps every platform answer into the ok channel as an ExperienceExchangeResult. Its status is entitled, denied (with reason), expired, binding_mismatch, unavailable, rate_limited or upstream_error, and only an unreadable 200 body reaches the err channel. To show the right locked screen, accessModeFromDenialReason(reason) recovers the access mode from a denial.

Direct experience writes

These four methods call the experiences write API, which ships together with the server-side operations GET /v0/experiences, POST /v0/experiences, PATCH /v0/experiences/{uuid} and POST /v0/experiences/{uuid}/unpublish. Each write resolves to a result whose status is either its own success arm or one of three shared outcomes. A 403 with no known reason is an err. Either the token belongs to another app or it lacks write:experience, and the platform’s message says which. PublishExperienceParams keys price on accessMode. PAID requires priceCents in USD cents and accepts priceRecurring, while FREE, SUBSCRIPTION and HIDDEN take no price. For the publish flow and the creator’s confirmation dialog, see Publish an experience.

Subscribers

get still returns a subscriber whose subscription.status is cancelled or paused, so check the status before granting access. Handles, display names and avatar URLs are confidential, and they change. Resolve them when you render, key your own records on uuid, and never persist them.

Vault

Four helpers sit beside the namespace. Signed URLs stay valid only for a window the platform doesn’t publish, so don’t cache them.

Webhook subscriptions

To receive deliveries and manage the subscription lifecycle, see Receive webhooks with the SDK.

Paginate

paginateOffset(fetchPage, options?) walks any offset-paginated /v0 route as an async generator of Result pages. It clamps size to the platform’s 1 to 50 (default 15) and stops after the first err. On a 429 it waits the platform’s Retry-After and resumes the same page, up to 3 waits per walk (DEFAULT_MAX_RATE_LIMIT_WAITS), each capped at 60 s.

Upload a file to the vault

An upload takes four moves: open a session, PUT each part to its presigned URL, complete the upload, then poll until the platform finishes processing.
A browser can’t read the ETag header off a cross-origin fetch response, so browser uploads need XMLHttpRequest or a server-side relay. For the whole media workflow, see Upload media.

Shell URLs

creatorProfileUrl(webOrigin, handle), experienceDetailShareUrl(webOrigin, experienceUuid) and experienceShareUrl(template, substitutions) build links back into Fanvue. Each returns { ok: true, url } or { ok: false, refusal }, and none ever emits a URL with an unresolved placeholder. webOrigin is FANVUE_WEB_ORIGIN. The template is FANVUE_EXPERIENCE_URL_TEMPLATE, and it must contain {experienceUuid}.

See also