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

# Build your first Fanvue app

> Register a Fanvue app, get an OAuth access token with curl and make your first authenticated call to GET /users/me.

By the end of this page you'll have a registered app, an access token, and a successful call to `GET /users/me` against the live API. You need a Fanvue creator account, or an agency admin account, with a verified email, plus curl and openssl on your machine.

Fanvue has no API keys. A creator or agency admin authorises your app through OAuth 2.0 and you call the API with the access token you receive. The [authentication overview](/docs/authentication/overview) explains the flow in full; here you run it once by hand.

<Steps>
  <Step title="Create the app">
    Open the Developer Area at `https://www.fanvue.com/dev`. On your first visit, click **Create developer profile**. Then click **Create app**, give the app a name and click **Create**. Creating an app means accepting the Fanvue Developer Terms of Service and Developer Policy.

    Only create apps for a service you operate. The client secret you're about to receive acts on your account, so handing it to someone else gives them that access. If you want to connect your account to another developer's app, use that app's Connect button instead.
  </Step>

  <Step title="Save the client secret">
    The dialog that opens shows your Client ID and Client Secret. Copy the secret into your secrets manager before you close the dialog.

    <Warning>
      The client secret is shown once. Fanvue cannot show it again after the dialog closes. If you lose it, open the **Authentication** tab and click **Reset secret**: a new secret appears once, and the old one stops working.
    </Warning>

    Your app is registered as one confidential OAuth client with the `authorization_code` and `refresh_token` grants. There is no public client option, no personal access token and no client-credentials grant. Every access token comes from a user signing in.
  </Step>

  <Step title="Pick API only">
    On **App details**, open the app type select and choose **API only**. API-only apps aren't listed in the App Store; creators connect to them through OAuth. The page offers a **Get your OAuth credentials** button that opens the **Authentication** tab.

    The type isn't final. You can change it later on the same select, and [Choose your app type](/docs/get-started/choose-your-app-type) explains what the other two types add.
  </Step>

  <Step title="Set the redirect URI and scopes">
    On the **Authentication** tab, add the exact redirect URI your code will send and tick the scopes your app needs. Both apply when you save. An API-only app needs no app domain and no App Manifest.

    A redirect URI uses `https://` on any host and port, or `http://` on `localhost`, `127.0.0.1` or `[::1]`. For this walkthrough add `http://localhost:3000/callback` and tick `read:self`.

    Sign in with your creator account when you run the flow. A fan account that authorises an app receives only `read:self` and the chat, media and experience scopes, whatever the app requests.
  </Step>

  <Step title="Get a token with curl">
    | Endpoint | URL |
    | - | - |
    | Authorise | `https://auth.fanvue.com/oauth2/auth` |
    | Token | `https://auth.fanvue.com/oauth2/token` |

    PKCE is mandatory, so start by generating a verifier and its S256 challenge:

    ```bash theme={null}
    VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
    CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
    echo "$VERIFIER"
    echo "$CHALLENGE"
    ```

    Build the authorise URL. `openid`, `offline_access` and `offline` are the system scopes every authorisation request carries, and `read:self` is the scope you ticked. The `state` value is a random string you check when the browser comes back.

    ```text theme={null}
    https://auth.fanvue.com/oauth2/auth
      ?client_id=YOUR_CLIENT_ID
      &response_type=code
      &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
      &scope=openid%20offline_access%20offline%20read%3Aself
      &state=a8f5f167f44f4964e6c998dee827110c
      &code_challenge=YOUR_CHALLENGE
      &code_challenge_method=S256
    ```

    Open the URL in a browser, sign in and approve. The browser lands on `http://localhost:3000/callback?code=...&state=...`. Nothing needs to listen on that port: copy the `code` value from the address bar and check that `state` matches the value you sent.

    Now exchange the code for tokens. The code is single-use.

    ```bash theme={null}
    curl -X POST https://auth.fanvue.com/oauth2/token \
      -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
      -d grant_type=authorization_code \
      -d code=THE_CODE_FROM_THE_ADDRESS_BAR \
      -d redirect_uri=http://localhost:3000/callback \
      -d code_verifier="$VERIFIER"
    ```

    The `-u` flag sends your Client ID and Client Secret as an HTTP Basic Auth header. Every Fanvue app is registered with the `client_secret_basic` method, so credentials placed in the POST body are rejected with `invalid_client`. The `code_verifier` is the other half of PKCE: it binds the code to the browser session that started the flow, while the secret proves the request comes from your server.

    ```json theme={null}
    {
      "access_token": "<access token>",
      "refresh_token": "<refresh token>",
      "expires_in": "<integer seconds>",
      "token_type": "bearer",
      "scope": "openid offline_access offline read:self"
    }
    ```

    `access_token` is short-lived and `expires_in` is its lifetime in seconds. `refresh_token` gets you a new access token without another sign-in; see [Token refresh](/docs/authentication/implementation-guide#token-refresh) when you get that far.
  </Step>

  <Step title="Call GET /users/me">
    Send the access token as a Bearer token and pin the API version with `X-Fanvue-API-Version: 2025-06-26`. Send the header on every request: a request without it is served the current default version, which moves when a new version becomes current.

    <Tabs>
      <Tab title="curl">
        ```bash theme={null}
        curl https://api.fanvue.com/users/me \
          -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
          -H "X-Fanvue-API-Version: 2025-06-26"
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        import requests

        response = requests.get(
            "https://api.fanvue.com/users/me",
            headers={
                "Authorization": "Bearer YOUR_ACCESS_TOKEN",
                "X-Fanvue-API-Version": "2025-06-26",
            },
        )

        print(response.status_code)
        print(response.json())
        ```
      </Tab>

      <Tab title="JavaScript">
        ```javascript theme={null}
        const response = await fetch("https://api.fanvue.com/users/me", {
          headers: {
            Authorization: "Bearer YOUR_ACCESS_TOKEN",
            "X-Fanvue-API-Version": "2025-06-26",
          },
        });

        console.log(response.status);
        console.log(await response.json());
        ```
      </Tab>
    </Tabs>

    A successful call returns `200 OK` and your account:

    ```json theme={null}
    {
      "uuid": "6f1c2a3e-9b4d-4c8e-a1f2-3d4e5f6a7b8c",
      "email": "ava@example.com",
      "handle": "ava-creates",
      "bio": "Content creator and influencer",
      "displayName": "Ava Creates",
      "isCreator": true,
      "isAiCreator": false,
      "roles": ["creator"],
      "isDiscoverable": true,
      "isInCuratedSection": false,
      "createdAt": "2023-01-15T10:30:00.000Z",
      "updatedAt": "2024-12-01T14:20:00.000Z",
      "avatarUrl": "https://media.fanvue.com/user-avatar.jpg",
      "bannerUrl": "https://media.fanvue.com/user-banner.jpg",
      "likesCount": 1250,
      "fanCounts": {
        "followersCount": 3420,
        "subscribersCount": 890
      },
      "contentCounts": {
        "imageCount": 145,
        "videoCount": 67,
        "audioCount": 12,
        "postCount": 234,
        "payToViewPostCount": 45
      }
    }
    ```

    A `401 Unauthorized` means the token is missing, expired or malformed: check that the header reads `Bearer`, one space, then the token, or exchange a new code. A `403 Forbidden` means the token lacks a scope the endpoint needs.
  </Step>
</Steps>

## Next steps

You're rate limited to 200 requests per minute per user per app by default; higher limits are available to agencies on request. From here, pick the path that matches what you're building.

<CardGroup cols={2}>
  <Card title="Choose your app type" icon="layer-group" href="/docs/get-started/choose-your-app-type">
    Stay API-only, or add an App URL or embed settings to list on the App Store.
  </Card>

  <Card title="Sign in with Fanvue" icon="right-to-bracket" href="/docs/get-started/sign-in-with-fanvue">
    Add Sign in with Fanvue to a web app with the Fanvue App Starter.
  </Card>

  <Card title="Authentication overview" icon="key" href="/docs/authentication/overview">
    The OAuth 2.0 flow in full, with refresh and error handling.
  </Card>

  <Card title="Rate limits" icon="gauge" href="/docs/authentication/rate-limits">
    How the budget is counted and what a `429` carries.
  </Card>

  <Card title="What you can build" icon="store" href="/docs/app-store/introduction">
    Listing, billing, fan experiences and the Builder SDK.
  </Card>
</CardGroup>


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