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

# Reply to unread chats automatically

> Build a Python or Node.js script that finds a creator's unread Fanvue chats, reads the recent history and sends a reply to each one.

By the end of this page you'll have a command-line script that finds a creator's unread chats, checks the latest message in each came from the fan, drafts a reply and sends it. The script prints the new `messageUuid` for each reply. It starts with a template reply so it runs end to end; you swap in your own reply logic afterwards. Run it once or on a schedule.

You need:

* a Bearer access token for the creator's account; [Build your first app](/docs/get-started/build-your-first-app) gets you one. Access tokens are short-lived, so refresh yours if it has expired.
* the `read:self`, `read:chat` and `write:chat` scopes on that token
* Python 3.9 or later (the example uses the `requests` library) or Node.js 18 or later (which has `fetch` built in). Pick one tab in each step.

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

Keep the token in an environment variable rather than in source; the examples read it from `FANVUE_TOKEN`.

## How it works

Four requests do the work. Every request goes to `https://api.fanvue.com` with `Authorization: Bearer <token>` and `X-Fanvue-API-Version: 2025-06-26`.

| Step | Endpoint | Purpose |
| - | - | - |
| Identify yourself | `GET /users/me` | Returns your own `uuid`, used to tell your messages from the fan's. |
| Find unread chats | `GET /chats?filter=unread` | Returns one entry per unread conversation with the fan's `user.uuid`. |
| Read the history | `GET /chats/{userUuid}/messages` | Returns recent messages, newest first. |
| Send the reply | `POST /chats/{userUuid}/message` | Posts your text into the chat. |

`GET /chats/unread` looks like the obvious call but returns only counts (unread chats and unread messages), not the chats. To get the list with each fan's `user.uuid`, use `GET /chats` with `filter=unread`.

## Step 1: Set up the API client

Every call shares the base URL, the Bearer token and the version header, so wrap them once in a client with one helper per endpoint.

<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 on Node.js 18 or later. 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 (`size` default 15, maximum 50). Each item describes one conversation, with the fan as `user` (including the `user.uuid` you reply to), `isRead`, `unreadMessagesCount` and a `lastMessage` preview.

A trimmed response:

```json theme={null}
{
  "data": [
    {
      "isRead": false,
      "unreadMessagesCount": 3,
      "user": {
        "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "handle": "sam-fan",
        "displayName": "Sam"
      },
      "lastMessage": {
        "text": "Hey, how are you doing?",
        "type": "SINGLE_RECIPIENT",
        "senderUuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "senderRole": "FAN"
      }
    }
  ],
  "pagination": { "page": 1, "size": 15, "hasMore": false }
}
```

Branch on `isRead`, not only on `unreadMessagesCount`. A chat the creator marked unread by hand has `isRead: false` and `unreadMessagesCount: 0`, so decide whether your script should answer those. The `user.uuid` value is the `userUuid` path parameter for the next two endpoints.

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

For each unread chat, call `GET /chats/{userUuid}/messages`. Messages come back newest first, and every message carries `sender.uuid`. Compare it with 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": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "text": "Hey, how are you doing?",
      "sender": { "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "handle": "sam-fan" },
      "type": "SINGLE_RECIPIENT",
      "isRead": false
    }
  ],
  "pagination": { "page": 1, "size": 15, "hasMore": true }
}
```

The thread includes `BROADCAST` mass messages the creator sent to this fan, interleaved by publish date, so skip them when you look for the fan's latest message. `POST /chats/messages/batch` returns direct messages only.

The draft function starts with a template so the script runs end to end. Replace its body with your own reply logic, passing the recent messages as context and returning a short 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 ""

        # Replace this template with your own reply logic. Feed it the recent
        # `messages` as context and the creator's tone, and return the text.
        # The API does not write the reply: the text is whatever you send.
        return (
            f"Hey {fan_name}, thanks for your message "
            f'("{latest[:60]}"). I will get back to you properly 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 ?? "";

      // Replace this template with your own reply logic. Feed it the recent
      // `messages` as context and the creator's tone, and return the text.
      // The API does not write the reply: the text is whatever you send.
      return (
        `Hey ${fanName}, thanks for your message ` +
        `("${(latest ?? "").slice(0, 60)}"). I will get back to you properly soon.`
      );
    }
    ```
  </Tab>
</Tabs>

## Step 4: Send the reply

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

```json theme={null}
{ "messageUuid": "8d0f7780-8536-41ef-a55c-f18fd2fa1bf8" }
```

A send fails with `400` and a contactability error when the fan can't be messaged, for example because they aren't subscribed. Catch that case per chat so one fan doesn't stop the whole run.

The body also accepts `mediaUuids`, `price` (USD cents, minimum 300) to make the message pay-to-view, `mediaPreviewUuid`, `templateUuid` and `gif`. This guide sends plain text. See [Read and send messages](/docs/tutorials/working-with-messages#send-a-message) for the full body.

## Step 5: Put it together

Wire the four calls into one loop: identify yourself, fetch unread chats, then read, draft and send for each. Set `FANVUE_TOKEN` in your environment first.

<Tabs>
  <Tab title="Python">
    ```python reply.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 = [m for m in history["data"] if m["type"] != "BROADCAST"]

            # 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 reply.py
    ```
  </Tab>

  <Tab title="TypeScript (Node.js)">
    ```typescript reply.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.filter((m: any) => m.type !== "BROADCAST");

        // 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 reply.ts
    ```
  </Tab>
</Tabs>

## Expected result

With three unread chats, a run prints:

```text theme={null}
Running as @ava-creates (3bbe6394-2830-4646-a8ba-4a0a05426947)
Found 3 unread chat(s)
  Sam: sent 8d0f7780-8536-41ef-a55c-f18fd2fa1bf8
  Jo: latest message is mine, skipping
  Alex: could not send (400 Bad Request on /chats/.../message)
```

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

## Next steps

To run the script continuously, schedule `reply.py` or `reply.ts` with cron or a background worker, and add a review step if a human should approve drafts before they send. Each run spends two requests per unread chat plus two, so check the budget in [Rate limits](/docs/authentication/rate-limits). The events you can receive about chats instead of checking for them are in [Creator message events](/docs/creator/messages).

## See also

* [Fanvue MCP server](/docs/mcp-server)


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