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.
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.
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.
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.
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.
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:
Scope
Used for
read:self
GET /users/me
read:chat
GET /chats and GET /chats/{userUuid}/messages
write:chat
POST /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.
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:
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:
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.
Python
TypeScript (Node.js)
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. 💕' )
draft.ts
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. 💕` );}
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:
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.
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.
Python
TypeScript (Node.js)
bot.py
import osfrom fanvue import FanvueClientfrom draft import draft_replydef 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()
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);
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.
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.