> ## 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.

# App Manifest reference

> Every key of app-manifest.json for manifestVersion 1, with types, required fields, allowed values and the URL rules each field follows.

Look up any key of `app-manifest.json` for `manifestVersion` 1: its type, whether it's required, the values it accepts and the URL rules it follows. To connect a manifest to your app, start with the [App Manifest overview](/docs/app-store/app-manifest); when a check fails, the error code leads you to [Manifest statuses and errors](/docs/app-store/app-manifest/troubleshooting).

## Complete example

This manifest uses every key. For a first manifest, start from the [minimal manifest](/docs/app-store/app-manifest#minimal-manifest) instead.

```json app-manifest.json theme={null}
{
  "manifestVersion": 1,
  "x-build": "2026-09-23T12:00:00Z",
  "access": {
    "type": "embedded",
    "url": "https://your-app.com/embedded",
    "fanExperienceUrl": "https://your-app.com/fan",
    "postInstallUrl": "https://your-app.com/welcome"
  },
  "surfaces": [
    { "surface": "creator_landing", "src": "https://your-app.com/embedded" },
    {
      "surface": "fan_experience",
      "src": "https://your-app.com/fan",
      "fullScreen": true,
      "presentation": { "type": "dialog", "desktopWidth": 480, "desktopHeight": 640 }
    }
  ],
  "oauth": {
    "scopes": ["read:self", "read:creator", "read:chat"],
    "redirectUris": ["https://your-app.com/oauth/callback"]
  },
  "webhooks": {
    "destinations": [
      { "topic": "creator.follow.created", "url": "https://your-app.com/webhooks/fanvue" },
      { "topic": "creator.message.received", "url": "https://your-app.com/webhooks/fanvue" }
    ]
  }
}
```

`x-build` is optional metadata of your own; any key prefixed `x-` is ignored.

## Rules for the whole file

* The file is a single JSON object, served at `https://<your app domain>/app-manifest.json`. See [Serving requirements](/docs/app-store/app-manifest#serving-requirements).
* Unknown keys are rejected at every level.
* Keys prefixed `x-` are ignored at every level. Use them for your own metadata, for example `"x-build"` or `"x-commit"`.
* Don't add `"$schema"`. It's an unknown key and fails the check. No JSON Schema file is published for the manifest.
* Every URL starts with lowercase `https://`, except a redirect URI on `localhost`, `127.0.0.1` or `[::1]`, which may use `http://`. See [URL rules](#url-rules) for what else each URL allows.

## Top-level keys

| Key | Type | Required | Description |
| - | - | - | - |
| `manifestVersion` | number | Yes | Must be the number `1`. |
| `access` | object | Yes | Your app type and the URLs creators and fans open. See [`access`](#access). |
| `surfaces` | array of objects | No | Where an on-platform app renders inside Fanvue. Applies when present. See [`surfaces`](#surfaces). |
| `oauth` | object | No | OAuth scopes and redirect URIs. Each key present overrides the **Authentication** tab. See [`oauth`](#oauth). |
| `webhooks` | object | No | Webhook destinations. Overrides the **Events** tab when present. See [`webhooks`](#webhooks). |

If a key is present, the manifest sets that field and the Developer Area marks it **From manifest**. Leave a key out to keep using your settings value; [Settings or manifest](/docs/app-store/app-manifest#settings-or-manifest) has the full rules. Every field has a Developer Area equivalent except `access.postInstallUrl`:

* `access` maps to the app type select, **App URL** and **Embed settings** on **App details**.
* `oauth` maps to the **Authentication** tab.
* `webhooks` maps to the **Events** tab.

The manifest contains no app identity, so no Client ID and no app UUID. Your [app domain](/docs/app-store/app-manifest#connect-your-app-domain) is what links the file to your app.

## `manifestVersion`

`manifestVersion` must be the number `1`. The string `"1"` fails with `unsupported_manifest_version`, and when this check fails it's the only issue reported.

## `access`

`access` sets your app's type and the URLs creators and fans open, and Fanvue always reads it. The Developer Area labels the two types On-platform and Off-platform.

| Field | Type | Required | Description |
| - | - | - | - |
| `type` | string | Yes | `embedded` (On-platform: renders inside Fanvue) or `off_platform` (Off-platform: runs on your own site). See [Choose your app type](/docs/get-started/choose-your-app-type). |
| `url` | string | Yes | Where creators open your app. For an `embedded` app, the page Fanvue loads on the creator's App Home page (the `creator_landing` surface). For a private app with no UI, use your homepage, which creators only see if you list the app. |
| `fanExperienceUrl` | string | No | The page Fanvue loads when a fan opens your experience. Not allowed when `type` is `off_platform`. |
| `postInstallUrl` | string | No | Where creators are sent after installing your app. |

All four fields are staged onto your store draft on every successful check and reach your live app [after review](/docs/app-store/app-manifest#when-changes-take-effect).

Leaving out `fanExperienceUrl` or `postInstallUrl` keeps the current value. Neither can be cleared from the manifest, so to change one, set a new URL. A manifest that would remove the fan experience surface is refused at check; remove the surface in **Embed settings** instead, as described in [Removing a fan experience](/docs/app-store/app-manifest#removing-a-fan-experience).

For an `off_platform` app, `url` must be a public production URL using `https`, checked when the manifest is staged. These hosts are refused:

* `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, any IP address and any host without a dot
* any host that is or ends in `local`, `localhost`, `internal`, `test` or `example`
* any host that is or ends in `ngrok.io`, `ngrok-free.app`, `ngrok.app`, `ngrok.dev`, `trycloudflare.com`, `loca.lt`, `serveo.net`, `localtunnel.me`, `tunnelto.dev`, `lhr.life`, `localhost.run`, `repl.co`, `replit.dev` or `netlify.app`
* any host that is or ends in `fanvue.com`

A `netlify.app` domain can still host your manifest; only the app URL is restricted. The error names the manifest field, `access.url`, followed by the reason.

```json theme={null}
"access": {
  "type": "off_platform",
  "url": "https://your-app.com",
  "postInstallUrl": "https://your-app.com/welcome"
}
```

## `surfaces`

`surfaces` places an on-platform app on Fanvue surfaces. Changes are staged onto your store draft on every successful check and reach your live app [after review](/docs/app-store/app-manifest#when-changes-take-effect).

`surfaces` isn't allowed when `access.type` is `off_platform`, even as `[]` (`surfaces_on_off_platform`). Changing `access.type` to `off_platform` removes all of your app's surfaces when that version is approved.

| Field | Type | Required | Description |
| - | - | - | - |
| `surface` | string | Yes | One of the four surface values. Each surface may appear once (`duplicate_surface`). |
| `src` | string | Yes | The page Fanvue loads on that surface. |
| `fullScreen` | boolean | No | Render the surface full-screen. Defaults to `false`, so leaving it out sets it to `false`. Only affects `fan_experience`. |
| `presentation` | object | No | How the fan experience opens from a chat card or the creator's profile. Only `fan_experience` may declare it (`presentation_on_unsupported_surface`). Absent means `page`. See [`presentation`](#presentation). |

| `surface` | Where it renders |
| - | - |
| `creator_landing` | The creator's App Home page for your app. |
| `fan_experience` | The experience page at `/experiences/{uuid}`, reached from the creator's profile tab, chat cards and direct links, or a dialog over chat or the profile when `presentation.type` is `dialog`. |
| `fan_chat` | Accepted. Fanvue does not show it to creators or fans; you can preview it on the **Debug** tab. |
| `fan_post` | Accepted. Fanvue does not show it to creators or fans; you can preview it on the **Debug** tab. |

`creator_landing` and `fan_experience` always follow `access`. `access.url` sets `creator_landing` and `access.fanExperienceUrl` sets `fan_experience`, so you don't need a `surfaces` block for either.

Add a `surfaces` block to set `fullScreen` or `presentation`, or to declare other surfaces. When present, the file is the whole list, and `fan_chat` or `fan_post` surfaces missing from it are removed when your next version is approved. `creator_landing` and `fan_experience` are never removed this way.

When `surfaces` is present, a `creator_landing` `src` must equal `access.url`, and a `fan_experience` `src` must equal `access.fanExperienceUrl` when that is set. Otherwise the check fails with `conflicting_surface_url`.

Fanvue sets the iframe sandbox permissions, and a top-level `sandbox` key fails with `sandbox_not_configurable`.

```json theme={null}
"surfaces": [
  { "surface": "fan_experience", "src": "https://your-app.com/fan", "fullScreen": true }
]
```

### `presentation`

`presentation` on the `fan_experience` surface decides whether the experience opens as a page or a dialog when a fan clicks it on a chat card or the creator's profile. The **Open as** field in **Embed settings** sets the same value. Direct links and the experience detail page always open as a page, and on phones a dialog is always full screen.

| Field | Type | Required | Description |
| - | - | - | - |
| `type` | string | Yes | `page` (default) or `dialog`. |
| `desktopWidth` | integer | No | Preferred dialog content width on desktop, in CSS px, 240 to 1600 inclusive. Fanvue adds a header and shrinks the dialog on smaller screens. |
| `desktopHeight` | integer | No | Preferred dialog content height on desktop, in CSS px, 240 to 1600 inclusive. |

Declare `desktopWidth` and `desktopHeight` together, or neither to open as large as possible; one without the other fails with `incomplete_dialog_size`. A `page` presentation can't declare either (`dialog_size_on_page`).

```json theme={null}
"surfaces": [
  {
    "surface": "fan_experience",
    "src": "https://your-app.com/fan",
    "presentation": { "type": "dialog", "desktopWidth": 480, "desktopHeight": 640 }
  }
]
```

## `oauth`

`oauth` sets the scopes your app can request and the redirect URIs Fanvue accepts, both of which you can also set on the **Authentication** tab. Each key you include replaces that list on every successful check and overrides the tab's value; leave a key out to keep using the tab's value.

Redirect URI changes need no review. A scope added after your app is published waits for review, and the **Authentication** tab shows it as **Pending review** until then.

| Field | Type | Required | Description |
| - | - | - | - |
| `scopes` | array of strings | No | The [scopes](/docs/authentication/scopes) your app can request. May be `[]`, which clears the list. Duplicates are ignored. |
| `redirectUris` | array of strings | No | The redirect URIs Fanvue accepts in the OAuth flow. May be `[]`, which clears the list. No `*` anywhere (`wildcard_redirect_uri`). `https://` on any host and port; `http://` only on `localhost`, `127.0.0.1` or `[::1]`. See [URL rules](#url-rules). No limit on the number of entries. |

Allowed `scopes` values: `read:self`, `read:creator`, `write:creator`, `read:chat`, `write:chat`, `read:fan`, `read:media`, `write:media`, `read:post`, `write:post`, `read:insights`, `read:tracking_links`, `write:tracking_links`, `read:experience`, `write:experience`, `read:agency`, `write:agency`.

`openid` and `offline_access` are always allowed for your app. Don't list them, because the check fails if you do. Request them in your `/oauth2/auth` URL if you need an ID token or a refresh token. The Fanvue App Starter and SDK request them by default.

Adding a scope makes every user authorise your app again, so read [When changes take effect](/docs/app-store/app-manifest#when-changes-take-effect) before you add one.

`"oauth": { "scopes": [], "redirectUris": [] }` clears both lists, whatever the **Authentication** tab holds, and stops sign-in working for your app.

To submit for review, your app needs at least one redirect URI in use, from the **Authentication** tab or the manifest. Until it has one, the submission checklist flags the missing redirect URI.

```json theme={null}
"oauth": {
  "scopes": ["read:self", "read:chat"],
  "redirectUris": [
    "https://your-app.com/api/oauth/callback",
    "https://my-fanvue-app.dev:3001/api/oauth/callback"
  ]
}
```

## `webhooks`

`webhooks.destinations` subscribes endpoints to webhook events, which you can also set on the **Events** tab; see [Webhooks](/docs/webhooks/index). When `webhooks` is present, the file's list replaces your app's destinations on every successful check.

Endpoints on the **Events** tab stay saved and editable, but the tab marks them **From manifest** and they aren't in use while the manifest sets the list. Remove the `webhooks` key to go back to the tab's list.

| Field | Type | Required | Description |
| - | - | - | - |
| `destinations` | array of objects | Yes | May be `[]`, which removes every destination in use. |
| `destinations[].topic` | string | Yes | The event type, for example `creator.follow.created`. See the [event catalogue](/docs/webhooks/event-catalog). Each topic may appear once (`duplicate_topic`). There is no all-events wildcard. |
| `destinations[].url` | string | Yes | Your webhook endpoint: public `https` only, no `localhost`, private hosts or unusual ports. See [URL rules](#url-rules). |

* A destination is created only once your app has the scope that event requires. Add the scope to `oauth.scopes` in the same file and both apply in one check, or add it on the **Authentication** tab if the manifest doesn't set scopes. Until then the destination waits, and the status shows **Out of sync**.
* Each app has a destination limit, 20 by default, counted across all your app's destinations after the check. If your file goes over it, the check shows **Invalid** with `too_many_destinations`, but `oauth` changes in the same file are already applied.
* Every destination uses your app's one signing secret, shown on the **Events** tab. See [Verify webhook signatures](/docs/webhooks/signature-verification).
* `creator.payment.pending`, `creator.payment.failed`, `checkout_link.refund.rejected`, `checkout_link.refund.failed` and `app.subscription.deactivated` pass validation but Fanvue never delivers them.

## URL rules

Every URL must parse as an absolute URL (`invalid_url`), be 2048 characters or fewer (`too_big`) and start with lowercase `https://` (`not_https`). The one exception is a redirect URI on a loopback host, which may use `http://`. No URL has to be on your app domain.

| URL | `localhost`, IP address, any port | Other rules |
| - | - | - |
| `access.url` | Allowed by the manifest | `off_platform` apps: public production URL required when the manifest is staged. See [`access`](#access). |
| `access.fanExperienceUrl`, `access.postInstallUrl`, `surfaces[].src` | Allowed by the manifest | Must work for real users to pass review. |
| `oauth.redirectUris[]` | Allowed. `https://` on any host and port. `http://` only on `localhost`, `127.0.0.1` or `[::1]`, on any port; not `*.localhost` or other private IP addresses (`not_https`). | No `*` (`wildcard_redirect_uri`). No scheme other than `http` or `https` (`not_https`). No username or password and no `#` fragment (`invalid_url`). 2048 characters or fewer (`too_big`). The same rule applies on the **Authentication** tab. No limit on the number of entries. |
| `webhooks.destinations[].url` | Not allowed | Refused: `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, `localhost.*`, `*.localhost`, `*.local` and `*.internal` (`localhost_url`). Also refused: private IP addresses and a username or password in the URL (`unsafe_url`). Port 443 or 8443 only. |

## Not configured in the manifest

| Configuration | Where |
| - | - |
| App name, tagline, icon, description, highlights, preview images | **App details** tab |
| Test credentials, "What's changed in this version" | Submit dialog on the **App details** tab |
| Client ID and Client Secret | **Authentication** tab |
| Pricing plans and one-time items | **Pricing** tab. See [App billing](/docs/payments/app-billing/overview). |
| Tracking | **Tracking** tab |

Keys for these settings (`appName`, `tagline`, `listing`, `tracking`) fail with `unknown_key`.


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