Skip to main content
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.
This tutorial assumes you already have an access token. If you do not, the First Call guide walks you from zero to one authenticated request in about five minutes.

What you’ll build

A command-line chatbot that, on each run:

Finds unread chats

Lists every conversation that has unread messages, along with the fan’s userUuid you need to reply.

Reads the conversation

Pulls the most recent messages so the reply has context, and checks the last message actually came from the fan.

Drafts a reply

Turns the conversation into a short, on-brand reply. Start with a template, then swap in an LLM when you are ready.

Sends the reply

Posts the drafted text back into the chat and confirms with the returned messageUuid.

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.
StepEndpointWhy
Identify yourselfGET /users/meReturns your own uuid so you can tell whether the latest message came from the fan or from you.
Find unread chatsGET /chats?filter=unreadReturns one entry per unread conversation, each with the fan’s user.uuid.
Read the historyGET /chats/{userUuid}/messagesReturns recent messages (newest first) so the reply has context.
Send the replyPOST /chats/{userUuid}/messagePosts your drafted text into the chat.
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.

Prerequisites

1

An access token

A Bearer access token for the creator’s account. Get one with the 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.
2

The right scopes

The token must be granted these OAuth scopes:
ScopeUsed for
read:selfGET /users/me
read:chatGET /chats and GET /chats/{userUuid}/messages
write:chatPOST /chats/{userUuid}/message
3

A runtime

Python 3.9+ (the example uses the requests library) or Node.js 18+ (which has fetch built in). Pick one tab below.
Store the token in an environment variable rather than hard-coding it. The examples read it from FANVUE_TOKEN.

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.
Install the one dependency, then create fanvue.py:
pip install requests
fanvue.py
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()

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:
{
  "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:
{
  "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.
draft.py
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. 💕'
    )

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:
{ "messageUuid": "a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6" }
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.
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.
bot.py
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:
export FANVUE_TOKEN="your-access-token"
python bot.py

Expected result

With three unread chats, a run prints something like:
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.
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.

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

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

API Reference

Browse the full chat surface: mass messages, templates, media attachments, custom lists, and more.