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

# Building an Auto-Reply Chatbot

> Build a chatbot that reads a creator's unread chats, drafts an on-brand reply for each one, and sends it back through the Fanvue API.

Fans expect fast replies, but a creator cannot sit in the inbox all day. In this tutorial you build a chatbot that does the first pass for them: it finds every conversation with unread messages, reads the recent history, drafts a reply, and sends it. You can run it once or on a schedule (a cron job, a worker, a serverless function) to keep conversations warm around the clock.

By the end you will have a single script that calls three Fanvue endpoints in sequence and posts a real reply to every unread chat.

<Note>
  This tutorial assumes you already have an access token. If you do not, the [First Call](/docs/introduction/first-call) guide walks you from zero to one authenticated request in about five minutes.
</Note>

## What you'll build

A command-line chatbot that, on each run:

<CardGroup cols={2}>
  <Card title="Finds unread chats" icon="inbox">
    Lists every conversation that has unread messages, along with the fan's `userUuid` you need to reply.
  </Card>

  <Card title="Reads the conversation" icon="comments">
    Pulls the most recent messages so the reply has context, and checks the last message actually came from the fan.
  </Card>

  <Card title="Drafts a reply" icon="pen">
    Turns the conversation into a short, on-brand reply. Start with a template, then swap in an LLM when you are ready.
  </Card>

  <Card title="Sends the reply" icon="paper-plane">
    Posts the drafted text back into the chat and confirms with the returned `messageUuid`.
  </Card>
</CardGroup>

## How it works

Three endpoints do the work. Everything is served from `https://api.fanvue.com`, authenticated with a Bearer token, and every request must carry the `X-Fanvue-API-Version` header.

```mermaid theme={null}
sequenceDiagram
    participant Bot
    participant API as Fanvue API

    Bot->>API: GET /users/me
    API-->>Bot: your own uuid (to tell who sent what)

    Bot->>API: GET /chats?filter=unread
    API-->>Bot: list of unread chats (each has user.uuid)

    loop For each unread chat
        Bot->>API: GET /chats/{userUuid}/messages
        API-->>Bot: recent messages (newest first)
        Note over Bot: Draft a reply from the history
        Bot->>API: POST /chats/{userUuid}/message
        API-->>Bot: { messageUuid }
    end
```

| Step              | Endpoint                         | Why                                                                                               |
| ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------- |
| Identify yourself | `GET /users/me`                  | Returns your own `uuid` so you can tell whether the latest message came from the fan or from you. |
| Find unread chats | `GET /chats?filter=unread`       | Returns one entry per unread conversation, each with the fan's `user.uuid`.                       |
| Read the history  | `GET /chats/{userUuid}/messages` | Returns recent messages (newest first) so the reply has context.                                  |
| Send the reply    | `POST /chats/{userUuid}/message` | Posts your drafted text into the chat.                                                            |

<Note>
  There is also a `GET /chats/unread` endpoint, but it returns only **counts** (how many unread chats and messages you have), not the chats themselves. To get the list with each fan's `userUuid`, use `GET /chats` with `filter=unread`, as shown below.
</Note>

## Prerequisites

<Steps>
  <Step title="An access token">
    A Bearer access token for the creator's account. Get one with the [First Call](/docs/introduction/first-call) guide if you do not have it yet. Access tokens are short-lived (typically one hour), so refresh yours if it has expired.
  </Step>

  <Step title="The right scopes">
    The token must be granted these OAuth scopes:

    | Scope        | Used for                                          |
    | ------------ | ------------------------------------------------- |
    | `read:self`  | `GET /users/me`                                   |
    | `read:chat`  | `GET /chats` and `GET /chats/{userUuid}/messages` |
    | `write:chat` | `POST /chats/{userUuid}/message`                  |
  </Step>

  <Step title="A runtime">
    Python 3.9+ (the example uses the `requests` library) or Node.js 18+ (which has `fetch` built in). Pick one tab below.
  </Step>
</Steps>

<Tip>
  Store the token in an environment variable rather than hard-coding it. The examples read it from `FANVUE_TOKEN`.
</Tip>

## Step 1: Set up the API client

Every call shares the same base URL, Bearer token, and version header, so wrap them once. The client below exposes one helper per endpoint we need.

<Tabs>
  <Tab title="Python">
    Install the one dependency, then create `fanvue.py`:

    ```bash theme={null}
    pip install requests
    ```

    ```python fanvue.py theme={null}
    import os
    import requests

    API_BASE = "https://api.fanvue.com"
    API_VERSION = "2025-06-26"  # pins the API version so behaviour is stable


    class FanvueClient:
        def __init__(self, token: str):
            self.session = requests.Session()
            self.session.headers.update({
                "Authorization": f"Bearer {token}",
                "X-Fanvue-API-Version": API_VERSION,
                "Content-Type": "application/json",
            })

        def _get(self, path: str, params: dict | None = None) -> dict:
            res = self.session.get(f"{API_BASE}{path}", params=params)
            res.raise_for_status()
            return res.json()

        def get_me(self) -> dict:
            # GET /users/me -> your own account, including your uuid
            return self._get("/users/me")

        def get_unread_chats(self, page: int = 1, size: int = 50) -> dict:
            # GET /chats?filter=unread -> one entry per unread conversation
            return self._get("/chats", params={
                "filter": "unread",
                "page": page,
                "size": size,
            })

        def get_messages(self, user_uuid: str, mark_as_read: bool = False) -> dict:
            # GET /chats/{userUuid}/messages -> recent messages, newest first.
            # markAsRead=false so we don't clear the unread flag before we reply.
            return self._get(f"/chats/{user_uuid}/messages", params={
                "size": 15,
                "markAsRead": "true" if mark_as_read else "false",
            })

        def send_message(self, user_uuid: str, text: str) -> dict:
            # POST /chats/{userUuid}/message -> { "messageUuid": "..." }
            res = self.session.post(
                f"{API_BASE}/chats/{user_uuid}/message",
                json={"text": text},
            )
            res.raise_for_status()
            return res.json()
    ```
  </Tab>

  <Tab title="TypeScript (Node.js)">
    No dependencies needed on Node.js 18+. Create `fanvue.ts`:

    ```typescript fanvue.ts theme={null}
    const API_BASE = "https://api.fanvue.com";
    const API_VERSION = "2025-06-26"; // pins the API version so behaviour is stable

    export class FanvueClient {
      constructor(private token: string) {}

      private async request(
        path: string,
        init: RequestInit = {},
      ): Promise<any> {
        const res = await fetch(`${API_BASE}${path}`, {
          ...init,
          headers: {
            Authorization: `Bearer ${this.token}`,
            "X-Fanvue-API-Version": API_VERSION,
            "Content-Type": "application/json",
            ...init.headers,
          },
        });
        if (!res.ok) {
          throw new Error(`${res.status} ${res.statusText} on ${path}`);
        }
        return res.json();
      }

      // GET /users/me -> your own account, including your uuid
      getMe() {
        return this.request("/users/me");
      }

      // GET /chats?filter=unread -> one entry per unread conversation
      getUnreadChats(page = 1, size = 50) {
        const q = new URLSearchParams({
          filter: "unread",
          page: String(page),
          size: String(size),
        });
        return this.request(`/chats?${q}`);
      }

      // GET /chats/{userUuid}/messages -> recent messages, newest first.
      // markAsRead=false so we don't clear the unread flag before we reply.
      getMessages(userUuid: string, markAsRead = false) {
        const q = new URLSearchParams({
          size: "15",
          markAsRead: markAsRead ? "true" : "false",
        });
        return this.request(`/chats/${userUuid}/messages?${q}`);
      }

      // POST /chats/{userUuid}/message -> { messageUuid }
      sendMessage(userUuid: string, text: string) {
        return this.request(`/chats/${userUuid}/message`, {
          method: "POST",
          body: JSON.stringify({ text }),
        });
      }
    }
    ```
  </Tab>
</Tabs>

## Step 2: Find the unread chats

`GET /chats?filter=unread` returns a paginated list. Each item describes one conversation: the fan (`user`, including the `user.uuid` you reply to), how many messages are unread (`unreadMessagesCount`), and a preview of the `lastMessage`.

A trimmed response looks like this:

```json theme={null}
{
  "data": [
    {
      "isRead": false,
      "unreadMessagesCount": 3,
      "user": {
        "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "handle": "sarah-jones",
        "displayName": "Sarah Jones"
      },
      "lastMessage": {
        "text": "Hey there! How are you doing?",
        "type": "SINGLE_RECIPIENT",
        "senderUuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
      }
    }
  ],
  "pagination": { "page": 1, "size": 15, "hasMore": false }
}
```

The `user.uuid` field is the `userUuid` path parameter for the next two endpoints. Keep it.

## Step 3: Read the conversation and draft a reply

For each unread chat, call `GET /chats/{userUuid}/messages` to pull recent history. Messages come back **newest first**, and every message carries a `sender.uuid`. Compare that against your own `uuid` from `GET /users/me` to confirm the latest message is from the fan and not a reply you already sent.

A trimmed messages response:

```json theme={null}
{
  "data": [
    {
      "uuid": "a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
      "text": "Hey there! How are you doing?",
      "sender": { "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "handle": "sarah-jones" },
      "type": "SINGLE_RECIPIENT",
      "isRead": false
    }
  ],
  "pagination": { "page": 1, "size": 15, "hasMore": true }
}
```

Now turn that history into a reply. The function below starts with a simple template so the script runs end to end today. When you are ready, replace its body with a call to your LLM of choice: pass the recent messages as context and ask for a short, on-brand reply.

<Tabs>
  <Tab title="Python">
    ```python draft.py theme={null}
    def draft_reply(fan_name: str, messages: list[dict]) -> str:
        """Turn recent messages into a reply.

        messages are newest-first, as returned by GET /chats/{userUuid}/messages.
        """
        latest = messages[0]["text"] if messages else ""

        # TODO: Replace this template with a real LLM call. Feed it the recent
        # `messages` as context and the creator's tone/persona, and return the
        # generated text. The Fanvue spec does not generate replies for you:
        # the reply text is whatever your code decides to send.
        return (
            f"Hey {fan_name}! Thanks so much for your message "
            f'("{latest[:60]}"). I will get back to you properly very soon. 💕'
        )
    ```
  </Tab>

  <Tab title="TypeScript (Node.js)">
    ```typescript draft.ts theme={null}
    interface Message {
      text: string | null;
      sender: { uuid: string; handle: string };
    }

    // messages are newest-first, as returned by GET /chats/{userUuid}/messages.
    export function draftReply(fanName: string, messages: Message[]): string {
      const latest = messages[0]?.text ?? "";

      // TODO: Replace this template with a real LLM call. Feed it the recent
      // `messages` as context and the creator's tone/persona, and return the
      // generated text. The Fanvue spec does not generate replies for you:
      // the reply text is whatever your code decides to send.
      return (
        `Hey ${fanName}! Thanks so much for your message ` +
        `("${(latest ?? "").slice(0, 60)}"). I will get back to you properly very soon. 💕`
      );
    }
    ```
  </Tab>
</Tabs>

## Step 4: Send the reply

`POST /chats/{userUuid}/message` posts your drafted text. The body needs a single `text` field (1 to 5000 characters). On success the API returns `201` with the new message's UUID:

```json theme={null}
{ "messageUuid": "a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6" }
```

<Note>
  A send can fail with a `400` and a contactability error if the fan cannot currently be messaged (for example, they are not subscribed or have blocked messages). Catch that case per chat so one un-messageable fan does not stop the whole run.
</Note>

The request body also accepts optional `mediaUuids`, a `price` (in cents, minimum 300) to make the message pay-to-view, and a `templateUuid`. This tutorial sends plain text only.

## Step 5: Put it together

Now wire the four calls into one loop: identify yourself, fetch unread chats, and for each one read the history, draft a reply, and send it. Set `FANVUE_TOKEN` in your environment first.

<Tabs>
  <Tab title="Python">
    ```python bot.py theme={null}
    import os
    from fanvue import FanvueClient
    from draft import draft_reply


    def run() -> None:
        token = os.environ["FANVUE_TOKEN"]
        client = FanvueClient(token)

        # Who am I? Needed to tell my own messages apart from the fan's.
        me = client.get_me()
        my_uuid = me["uuid"]
        print(f"Running as @{me['handle']} ({my_uuid})")

        # Find unread conversations.
        unread = client.get_unread_chats()
        chats = unread["data"]
        print(f"Found {len(chats)} unread chat(s)")

        for chat in chats:
            fan = chat["user"]
            user_uuid = fan["uuid"]
            fan_name = fan.get("displayName") or fan["handle"]

            # Read recent history. markAsRead=False keeps the chat unread until
            # we have actually replied.
            history = client.get_messages(user_uuid, mark_as_read=False)
            messages = history["data"]

            # Skip if the most recent message is one we sent: nothing to reply to.
            if messages and messages[0]["sender"]["uuid"] == my_uuid:
                print(f"  {fan_name}: latest message is mine, skipping")
                continue

            reply = draft_reply(fan_name, messages)

            try:
                result = client.send_message(user_uuid, reply)
                print(f"  {fan_name}: sent {result['messageUuid']}")
            except Exception as err:  # e.g. 400 contactability error
                print(f"  {fan_name}: could not send ({err})")


    if __name__ == "__main__":
        run()
    ```

    Run it:

    ```bash theme={null}
    export FANVUE_TOKEN="your-access-token"
    python bot.py
    ```
  </Tab>

  <Tab title="TypeScript (Node.js)">
    ```typescript bot.ts theme={null}
    import { FanvueClient } from "./fanvue";
    import { draftReply } from "./draft";

    async function run(): Promise<void> {
      const token = process.env.FANVUE_TOKEN;
      if (!token) throw new Error("Set FANVUE_TOKEN");
      const client = new FanvueClient(token);

      // Who am I? Needed to tell my own messages apart from the fan's.
      const me = await client.getMe();
      const myUuid = me.uuid;
      console.log(`Running as @${me.handle} (${myUuid})`);

      // Find unread conversations.
      const unread = await client.getUnreadChats();
      const chats = unread.data;
      console.log(`Found ${chats.length} unread chat(s)`);

      for (const chat of chats) {
        const fan = chat.user;
        const userUuid = fan.uuid;
        const fanName = fan.displayName || fan.handle;

        // Read recent history. markAsRead=false keeps the chat unread until
        // we have actually replied.
        const history = await client.getMessages(userUuid, false);
        const messages = history.data;

        // Skip if the most recent message is one we sent: nothing to reply to.
        if (messages.length && messages[0].sender.uuid === myUuid) {
          console.log(`  ${fanName}: latest message is mine, skipping`);
          continue;
        }

        const reply = draftReply(fanName, messages);

        try {
          const result = await client.sendMessage(userUuid, reply);
          console.log(`  ${fanName}: sent ${result.messageUuid}`);
        } catch (err) {
          // e.g. 400 contactability error
          console.log(`  ${fanName}: could not send (${err})`);
        }
      }
    }

    run().catch(console.error);
    ```

    Run it:

    ```bash theme={null}
    export FANVUE_TOKEN="your-access-token"
    npx tsx bot.ts
    ```
  </Tab>
</Tabs>

## Expected result

With three unread chats, a run prints something like:

```text theme={null}
Running as @johnny-doey (3bbe6394-2830-4646-a8ba-4a0a05426947)
Found 3 unread chat(s)
  Sarah Jones: sent a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6
  Mike Smith: latest message is mine, skipping
  Alex Doe: could not send (400 Bad Request on /chats/.../message)
```

Each `sent ...` line is a real message now visible in the creator's chat with that fan. Open the Fanvue inbox to confirm the replies landed. Run the script again and the chats you just answered no longer appear in the unread list, because your reply marked the conversation as read.

<Tip>
  To turn this into a true co-pilot, schedule `bot.py` to run every few minutes with cron, a serverless timer, or a background worker. Pair it with the LLM draft from Step 3 so each reply is written in the creator's own voice, and add a review step if you want a human to approve drafts before they send.
</Tip>

## A note on what the spec does not cover

The Fanvue API gives you the conversation and lets you send a reply, but it does not write the reply for you. The quality of the draft is entirely up to the code in `draft_reply`. The `TODO` in Step 3 is where you plug in your own logic or an LLM. Everything else on this page (the endpoints, fields, and request shapes) is defined by the API and works as shown.

## Next steps

<CardGroup cols={2}>
  <Card title="Fanvue MCP Server" icon="robot" href="/docs/mcp-server">
    Prefer not to write code? Connect Fanvue to Claude, ChatGPT, or Cursor and ask your assistant to read and reply to chats in plain language.
  </Card>

  <Card title="API Reference" icon="book" href="/docs/api-reference/overview">
    Browse the full chat surface: mass messages, templates, media attachments, custom lists, and more.
  </Card>
</CardGroup>
