> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onlyfansapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to build an AI chatbot for OnlyFans (/onlyfans-ai/build-ai-chatbot-for-onlyfans)

import { Step, Steps } from "fumadocs-ui/components/steps";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";

Learn how to receive a fan message, send it to your AI, and reply through OnlyFansAPI.com.

By the end of this guide, you will have the basic chatbot working. A fan sends a message. Your webhook receives it. Your AI writes a reply. OnlyFansAPI.com sends it back.

<Callout title="Your AI is up to you">
  OnlyFansAPI.com gives you the messages and lets you send replies. You choose the AI service, prompts, chat history, and approval rules that fit your product.
</Callout>

## What you need

* An OnlyFansAPI.com account
* A connected OnlyFans account
* A server with an HTTPS webhook URL
* An AI service or your own model
* A background queue for AI jobs

## Build the chatbot

<Steps>
  <Step>
    ### Create your API key

    <a href="https://app.onlyfansapi.com" target="_blank" rel="noreferrer">Sign up for OnlyFansAPI.com</a>. Open **API Keys**, then click **Create API Key**.

        <img alt="Open API Keys in the OnlyFansAPI.com dashboard" src={__img0} placeholder="blur" />

        <img alt="Click the Create API Key button" src={__img1} placeholder="blur" />

    Give the key a name such as `My AI chatbot`. Copy it when it appears. You will only see it once.

        <img alt="Copy and save your new API key" src={__img2} placeholder="blur" />

    Save the key in an environment variable:

    ```bash
    ONLYFANS_API_KEY=your_api_key
    ```

    <Callout type="warn">
      Keep your API key on your server. Never put it in browser code, commit it to Git, or share it with your AI model.
    </Callout>
  </Step>

  <Step>
    ### Add your webhook

    Open **Webhooks** in the OnlyFansAPI.com dashboard and add your HTTPS endpoint. Add a signing secret, then subscribe to these events:

    * [`messages.received`](/webhooks/available-events#messagesreceived) starts a new AI reply.
    * [`users.typing`](/webhooks/available-events#userstyping) helps your bot wait until the fan finishes typing.
    * [`messages.ppv.unlocked`](/webhooks/available-events#messagesppvunlocked) lets your bot follow up after a fan buys a paid message.

    You only need `messages.received` for a basic chatbot. The other two events make the conversation feel more natural.

    When a fan sends a message, OnlyFansAPI.com sends the message to this endpoint.

    Your code may look like this:

    ```js
    verifyWebhookSignature(request);

    if (webhook.event === "users.typing") {
      await saveLastTypingTime(webhook);
    }

    if (
      webhook.event === "messages.received" ||
      webhook.event === "messages.ppv.unlocked"
    ) {
      await queue.add(webhook);
    }

    return 200;
    ```

    See [Subscribing to webhooks](/webhooks/subscribing-to-webhooks) for the dashboard steps and [Protecting your webhooks](/webhooks/protecting-your-webhooks) for signature checks.

    <Callout title="Reply to the webhook first">
      Return `200` right away. Let a background job handle the AI reply. Otherwise, OnlyFansAPI.com may think the webhook failed and send it again.
    </Callout>
  </Step>

  <Step>
    ### Generate the reply

    Your background job sends the fan's message to your AI:

    ```js
    const reply = await generateReply({
      message: webhook.payload.text,
      fan: webhook.payload.fromUser,
      creatorStyle,
      recentMessages,
    });
    ```

    The `generateReply()` function is your part. You can use <a href="https://platform.openai.com/docs/api-reference/responses" target="_blank" rel="noreferrer">OpenAI</a>, <a href="https://docs.anthropic.com/en/api/messages" target="_blank" rel="noreferrer">Anthropic</a>, <a href="https://ai.google.dev/gemini-api/docs/text-generation" target="_blank" rel="noreferrer">Gemini</a>, a local model, or your own system.

    You decide:

    * How the creator should sound
    * How much chat history to include
    * When to suggest content
    * When to ask a human for help
    * Which messages should never be sent automatically
  </Step>

  <Step>
    ### Show the typing indicator

    Call [Start Typing Indicator](/api-reference/chats/start-typing-indicator) while your AI writes the reply:

    ```http
    POST /api/{account}/chats/{chat_id}/typing
    ```

    The typing indicator only stays visible for a few seconds. Call the endpoint again if your AI is still working.
  </Step>

  <Step>
    ### Send the reply

    Send the generated text with [Send Message](/api-reference/chat-messages/send-message):

    ```http
    POST /api/{account}/chats/{chat_id}/messages
    ```

    ```json
    {
      "text": "Your generated reply"
    }
    ```

    The webhook gives you both IDs you need:

    * Use `account_id` as the account.
    * Use `payload.fromUser.id` as the `chat_id`.

    Add a unique `Idempotency-Key` header when you send the message. If the request times out and you retry it, the same reply will not be sent twice.
  </Step>
</Steps>

## Wait until the fan finishes typing

A fan may send several short messages in a row. Without a small delay, your bot may answer the first message while the fan is still writing the second one.

The [`users.typing`](/webhooks/available-events#userstyping) webhook gives you:

* `account_id` — the creator's connected account
* `payload.id` — the fan's ID and chat ID

Save the current time whenever this event arrives. Save it using both IDs, so typing in one creator's chat does not affect another chat.

```js
if (webhook.event === "users.typing") {
  await saveLastTypingTime({
    accountId: webhook.account_id,
    chatId: webhook.payload.id,
    time: Date.now(),
  });

  return 200;
}
```

Before the reply job calls your AI, check that the fan has been quiet for a few seconds:

```js
const lastTypingAt = await getLastTypingTime(accountId, chatId);

if (lastTypingAt && Date.now() - lastTypingAt < 8_000) {
  return runThisJobAgainIn(8_000);
}

const reply = await generateReply({ recentMessages });
```

The function names are only examples. Use the database, cache, and queue already in your app.

Start the reply job when `messages.received` arrives. If `users.typing` arrives after that, move the job back. Once the fan has been quiet for about 5 to 10 seconds, combine their recent messages and generate one reply.

<Callout title="Do not wait forever">
  Some fans will not trigger a typing event. Always start a reply timer from `messages.received`, then use `users.typing` only to extend that timer.
</Callout>

Typing events are short-lived signals and do not include an `X-OFAPI-Idempotency-Key`. You do not need to save every event forever. Keeping the latest typing time for each chat is enough.

## Follow up after a PPV purchase

The [`messages.ppv.unlocked`](/webhooks/available-events#messagesppvunlocked) webhook fires when a fan buys a paid message that the creator sent.

You can use it to:

* Thank the fan for the purchase
* Continue the conversation without asking them to buy the same content again
* Stop any reminder for that paid message
* Save the purchase in the fan's chat history or profile
* Ask your AI for a natural follow-up based on the creator's style

The useful fields are:

* `account_id` — the creator's connected account
* `payload.user_id` — the fan's ID and chat ID
* `payload.user` — basic fan details, including their name
* `payload.replacePairs["{AMOUNT}"]` — the purchase amount
* `payload.replacePairs["{MESSAGE_LINK}"]` — a link to the purchased message

Queue the event just like a received message:

```js
if (webhook.event === "messages.ppv.unlocked") {
  await queue.add({
    type: "ppv-follow-up",
    accountId: webhook.account_id,
    chatId: webhook.payload.user_id,
    fan: webhook.payload.user,
    amount: webhook.payload.replacePairs["{AMOUNT}"],
  });

  return 200;
}
```

Then give the purchase to your AI as an event, not as a message written by the fan:

```js
const reply = await generateReply({
  event: "ppv_unlocked",
  fan,
  amount,
  creatorStyle,
  recentMessages,
  instruction: "Thank the fan naturally. Do not sell another PPV right away.",
});
```

Send the result with the same [Send Message](/api-reference/chat-messages/send-message) endpoint used for normal replies.

<Callout title="Match the purchase to the original message">
  The `{MESSAGE_LINK}` value contains a `firstId` value for the purchased message. Save the PPV messages your app sends, then use that value when you need to mark the exact offer as purchased.
</Callout>

Use the webhook's `X-OFAPI-Idempotency-Key` to make sure one purchase only creates one follow-up. It stays the same if the webhook is delivered again.

## Add photos or videos

<Callout>
  Start with text replies. Add media after the basic chatbot works.
</Callout>

* Use [List Vault Media](/api-reference/media-vault/list-vault-media) to get the creator's existing content.
* Use [Upload Media to Vault](/api-reference/media-vault/upload-media-to-vault) to add new content.
* Pass the chosen media ID in `mediaFiles` when you [send the message](/api-reference/chat-messages/send-message).
* Need the actual photo or video file? Use [Download Media](/api-reference/media/download-media-from-the-only-fans-cdn).
* For protected videos, use [Download DRM-protected Media](/api-reference/media/download-drm-protected-media).

For a paid message, add `price` to the request. Paid messages must include at least one media file.

## Check for messages your bot missed

The [`messages.received`](/webhooks/available-events#messagesreceived) webhook should handle new messages. These two endpoints are useful as a backup:

* [List Chats](/api-reference/chats/list-chats) with `filter=unread` or `filter=unread_with_tips` finds chats that still need a reply.
* [List Chat Messages](/api-reference/chat-messages/list-chat-messages) loads earlier messages when your AI needs more context.

<Callout type="warn" title="Loading messages marks the chat as read">
  Do not keep calling List Chat Messages to look for new messages. Use the `messages.received` webhook instead.
</Callout>

## Other webhooks you can add later

The [webhook event list](/webhooks/available-events) has more events you can add later:

* `messages.sent` keeps your chat history up to date when a human sends a reply.
* `media_uploads.completed` and `media_uploads.failed` tell you when a background upload finishes.
* `tips.received` and `subscriptions.new` let you add separate flows for tips and new subscribers.

## Before you go live

* Check every webhook signature.
* Keep your OnlyFansAPI.com and AI keys on the server.
* Save each incoming message ID so you do not queue it twice.
* Test with one creator first.
* Start with human approval before sending replies automatically.
* Only let the AI choose from media and prices you approve.
* Check the current rules of the AI service you use.

## FAQ

<Accordions type="multiple">
  <Accordion title="Does OnlyFansAPI.com include the AI?">
    No. OnlyFansAPI.com receives messages and sends replies. You connect the AI model you want to use.
  </Accordion>

  <Accordion title="Which AI model should I use?">
    Use the model that fits your language, speed, cost, and content needs. The OnlyFansAPI.com steps stay the same whichever model you choose.
  </Accordion>

  <Accordion title="Do I need to keep checking for new chats?">
    No. Use the `messages.received` webhook for new messages. Use List Chats only as a backup.
  </Accordion>

  <Accordion title="Why did a chat become read?">
    OnlyFans marks a chat as read when you load it with List Chat Messages. Use the webhook to spot new messages without polling that endpoint.
  </Accordion>

  <Accordion title="How do I stop duplicate replies?">
    Save the incoming message ID and ignore it if you have already handled it. For webhook events such as `messages.ppv.unlocked`, also save the `X-OFAPI-Idempotency-Key`. Use an `Idempotency-Key` when sending the reply too.
  </Accordion>

  <Accordion title="How long should my bot wait after a typing event?">
    Start with 5 to 10 seconds after the latest `users.typing` event. Reset the wait each time another typing event arrives. Keep the delay short enough that the chat still feels responsive.
  </Accordion>

  <Accordion title="Should every PPV unlock get an automatic reply?">
    Not necessarily. You can send a simple thank-you, ask for human approval, or only update the AI's chat context. Your rules should depend on the creator's style and how recently they spoke with the fan.
  </Accordion>

  <Accordion title="How do I send a photo or paid message?">
    Add the vault media ID to `mediaFiles`. Add `price` for a paid message. Paid messages need at least one media file.
  </Accordion>

  <Accordion title="Can a human take over the chat?">
    Yes. Add an on or off switch for each chat in your app. Listen for `messages.sent` so replies sent by a human also appear in your saved chat history.
  </Accordion>

  <Accordion title="Do I need the DRM download endpoint?">
    Only when you need the actual file from a protected video in the creator's own vault. You do not need it to attach an existing vault item to a message.
  </Accordion>
</Accordions>

## Next steps

* <a href="https://app.onlyfansapi.com/api-keys" target="_blank" rel="noreferrer">
    Create your API key
  </a>
* [Set up the `messages.received` webhook](/webhooks/subscribing-to-webhooks)
* [Send your first message](/api-reference/chat-messages/send-message)