API Reference

Betahunter API

The main endpoint takes the conversation so far and returns the next message to send, human-paced, on-persona, and ready to deliver. Balance and statistics endpoints expose credits and funnel rollups. Plain JSON over HTTPS, no SDK required.

Introduction

Send the thread you have so far for a lead. Betahunter decides whether it is the creator's turn, generates a reply in her voice, and returns the message (or burst of messages) to send back.

Base URLhttps://api.betahunter.app
TransportJSON over HTTPS
AuthSecret API key as a Bearer token

Authentication

Every request authenticates with your secret API key, sent as a Bearer token in the Authorization header:

Authorization header
Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx

Keys start with bh_live_. You can also send the key as an optional JSON body field named licensekey on POST endpoints for drop-in compatibility. The Authorization header wins when both are present, and GET /v1/balance requires the header. Keep it server-side, anyone who has it can spend your credits. A missing, unknown, or disabled key returns 401.

No key yet? Get one on Telegram.

Quickstart

One call generates the next reply for a lead who just messaged you:

curl
curl https://api.betahunter.app/v1/generateResponse \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "accountId": "acct_42",
    "recipient": { "id": "u_88", "name": "Bob" },
    "chatHistory": [
      { "role": "user", "content": "hey", "timestamp": 1730000000 }
    ],
    "persona": { "name": "Ava", "ctaLink": "https://onlyfans.com/ava" }
  }'

How billing works

You are billed in conversations, not messages or tokens.

  • A conversation is one lead in one of your accounts, identified by accountId + recipient.id. The API returns its conversationId.
  • You are charged one credit the first time the engine returns a successful non-empty bot turn in a conversation. Every later reply in that same conversation is free, even after your balance reaches zero.
  • If you enable a lead filter and a conversation is skipped by it (tierFiltered or genderFiltered), you are charged 0.2 credit once for that conversation, far less than a full reply and never more than once.
  • Turns that generate nothing (not your turn, capped, out of credits, blocked, or abandoned) are never charged.
  • Because of the 0.2 filter charge, creditsRemaining can be fractional (e.g. 919.8).
  • Check your remaining credits any time with GET /v1/balance.

Generate a reply

POST/v1/generateResponse

Send the recent thread you have so far. The engine appends only the genuinely new messages, decides whether to respond, and returns the reply to send.

The engine replies only when it is the creator's turn, that is, when the last message in chatHistory is from the lead ("role": "user"). If the thread is empty or the last message is the creator's, you get notOurTurn: true and nothing to send, unless you set generateOpener: true on an empty thread, in which case it writes the first message (the opener) instead.

Request body

FieldTypeDescription
accountIdstringrequiredYour account or sub-account id. Alias accountID is accepted. Together with recipient.id it identifies the conversation.
recipientobjectrequiredThe lead you are talking to. Alias userInfos is accepted. See Recipient.
chatHistoryarrayThe recent conversation in send order. Each item is a Message (max 200). Defaults to [] for a brand-new thread.
personaobjectThe creator the bot is playing. See Persona.
presetIdstringStored dashboard preset id. When present, Betahunter loads that preset's persona, CTA rotation, lead controls, follow-up setup, and media vault; inline persona fields override the stored preset for this call.
modelIdstringOptional model id from GET /v1/getModelsAndPresets. Today the default is betahunter-v3.
isFollowUpbooleanMark the turn as a follow-up to a lead who went quiet. Default false.
directionstringWho started the conversation: "outbound" (default, you cold-messaged the lead) or "inbound" (the lead reached out first, so she never pretends she messaged first). Booleans isInbound / isOutbound are accepted as aliases.
generateOpenerbooleanWhen true and chatHistory is empty, generate the first message (opener) instead of returning notOurTurn. Default false.

Recipient object

FieldTypeDescription
idstringrequiredStable unique id for the lead. Alias useridentifier is accepted inside userInfos.
namestringThe lead's display name.
usernamestringThe lead's handle.
biostringThe lead's bio, used to personalize.
locationstringThe lead's location, used to personalize.

Message object (each chatHistory item)

FieldTypeDescription
rolestringrequired"user" for the lead, "bot" or "assistant" for the creator.
contentstringThe message text. Missing or null is treated as an empty string.
timestampintegerrequiredUnix time in seconds. Used for validation and de-duplication.
idstringYour own id for the message, if you have one. Used to de-duplicate when you resend the window.

Persona object

FieldTypeDescription
namestringThe creator's name. Default "her".
ageintegerThe creator's age. Default 22.
citystringThe creator's city.
userInfostringShort self-description for the persona (look, vibe, backstory).
ctaInfostringWhat you are selling, used when she pitches the link.
ctaLinkstringThe link to close the lead to. Leave blank to disable pitching.
ctaMinExchangesintegerHow many lead replies before she may pitch the link. Default 8.
photosPhoto[]Inline media catalog she can send. Dashboard-managed media is normally stored on a preset instead. Each item is a Photo.
phasesobject[]Optional engine phase config, passed through as provided.
matchLocationbooleanWhen true, she grounds in the lead's location and plays up being near him; if his location is unknown she asks early. Default false. Aliases matchCity / matchHisLocation accepted.
tierFilterstringCountry-tier lead filter: "off" (default), "T3" (skip lower-income / tier-3 countries), or "T2T3" (skip tier-2 and tier-3). A matched lead is skipped before generation. See Lead filters.
filterFemalesbooleanWhen true, female leads are skipped before generation. Default false. See Lead filters.

Photo object (each persona.photos item)

FieldTypeDescription
urlstringrequiredThe media URL. Alias photoUrl is accepted.
typestringCatalog bucket. Accepts "Primary"/"main" and "Sexy"/"sexy"/"specific". Alias photoType is accepted. Default "Primary".
outfitDaystringOptional outfit bucket, such as "outfit-day-3". Dashboard uploads normalize plain numbers like "3".
photoTimestringOptional lighting bucket: "day-time", "night-time", or "either".
priorityintegerOptional manual send order. Lower values are preferred first.

Lead filters (optional)

Skip leads you do not want to engage, before any reply is generated. Set persona.tierFilter (country tier) and/or persona.filterFemales (gender). A skipped lead comes back with tierFiltered / genderFiltered set and no content, and is billed the 0.2 filter charge once (see How billing works).

For drop-in Capital compatibility the API also accepts these at the top level of the request: filterMaleOnly (boolean, maps to filterFemales), filterTiers (the country tiers to keep, 1 = highest income … 3 = lowest; e.g. [1, 2] keeps tiers 1–2 and skips tier 3, equivalent to tierFilter: "T3"), and nameForFilter (a name for the classifier when recipient.name is absent). Explicit persona values win over these aliases.

Response

Successful generate calls return 200. The body tells you what to do through content plus outcome fields. When notOurTurn, timewaste, aiCreditOver, orunderage is true, content is empty and you were not charged. tierFiltered and genderFiltered also return empty content, but a filtered conversation is billed 0.2 credit once. converted can be true alongside content.

FieldTypeDescription
contentarrayThe messages to send, in order. Each item is a Content item. May be empty.
convertedbooleantrue if the lead was closed to your link this turn.
conversationIdstringcustomerId:accountId:recipient.id conversation key. Use it to correlate turns.
notOurTurnbooleantrue when it is not the creator's turn (empty thread, or the last message was already hers). Nothing to send. With generateOpener: true, an empty thread generates an opener instead.
timewastebooleantrue when the conversation hit the message cap (50 lead messages) and was stopped.
aiCreditOverbooleantrue when you are out of credits on a new conversation, so nothing was generated. Top up to continue.
underagebooleantrue when the lead tripped the age-safety check. The thread is blocked permanently. Nothing to send.
tierFilteredbooleantrue when the lead was skipped by the tierFilter country-tier filter. Nothing to send; billed 0.2 credit once.
genderFilteredbooleantrue when the lead was skipped by the filterFemales filter. Nothing to send; billed 0.2 credit once.
messagedAlreadybooleantrue when preset lead controls found the same lead recently engaged by another account. Nothing to send.
engagedUserobject | nullDetails about the existing engagement when messagedAlready is true.
internalAccountbooleantrue when preset lead controls identify the recipient as one of your own accounts. Nothing to send.
statisticsobjectCapital-style phase metadata, including currentPhase, currentCycle, and stage.

Content item

FieldTypeDescription
typestring"text" or "image".
contentstringThe message text (for "text") or the media URL to send (for "image").
mediaPoolstring | nullFor an image, which catalog bucket it came from: "Primary" or "Sexy". null for text.

Example

Request
{
  "accountId": "acct_42",
  "presetId": "pre_7f2a9c",
  "recipient": { "id": "u_88", "name": "Bob" },
  "chatHistory": [
    { "role": "assistant", "content": "heyy what are you up to", "timestamp": 1730000000 },
    { "role": "user", "content": "just relaxing, you?", "timestamp": 1730000120 }
  ],
  "persona": { "name": "Ava", "ctaLink": "https://onlyfans.com/ava" }
}
200 Response
{
  "content": [
    { "type": "text", "content": "mmm relaxing sounds nice", "mediaPool": null },
    { "type": "text", "content": "i could think of a few better ways to relax tho 😉", "mediaPool": null }
  ],
  "converted": false,
  "notOurTurn": false,
  "timewaste": false,
  "aiCreditOver": false,
  "underage": false,
  "conversationId": "cust_123:acct_42:u_88"
}

Send each content item as its own message, in order, with natural delays. That multi-message burst is the human cadence the engine is built around.

Dashboard presets

Use presets when you do not want to rebuild creator setup in your own product. A preset stores persona info, CTA rotation, follow-up messages, duplicate-lead controls, phases, and the media vault. Then your integration only passes presetId on POST /v1/generateResponse.

POST/v1/presets

Create a preset from your dashboard or through the API.

Create preset
curl https://api.betahunter.app/v1/presets \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "name": "Ava main funnel",
    "persona": {
      "name": "Ava",
      "age": 22,
      "city": "Miami",
      "userInfo": "playful gym girl",
      "ctaInfo": "VIP page with daily drops",
      "ctaMinExchanges": 8
    },
    "ctas": [
      { "platform": "OnlyFans", "cta": "https://onlyfans.com/ava" }
    ],
    "leadControls": {
      "skipRecentlyMessagedLeads": true,
      "duplicateLeadCooldownDays": 30,
      "internalUsernames": ["ava_backup"]
    },
    "followUps": {
      "enabled": true,
      "maxFollowUps": 2,
      "messages": [
        { "message": "still thinking about you", "delayHours": 24 }
      ]
    }
  }'
POST/v1/presets/{presetId}/photos/upload

Upload media as JSON base64. Betahunter stores it under the preset and usestype, outfitDay, photoTime, and priority when choosing which image to send.

Upload preset media
curl https://api.betahunter.app/v1/presets/pre_7f2a9c/photos/upload \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "filename": "outfit-3-night.jpg",
    "contentType": "image/jpeg",
    "dataBase64": "data:image/jpeg;base64,/9j/...",
    "type": "Sexy",
    "outfitDay": "outfit-day-3",
    "photoTime": "night-time",
    "priority": 2
  }'

You can also attach an already-hosted URL with POST /v1/presets/{presetId}/photos. Deleting a preset removes dashboard-uploaded local media; remote URLs are left untouched.

GET/v1/getModelsAndPresets

Returns the callable model ids and your saved presets, useful for a Capital-style dropdown before sending modelId and presetId.

Models and presets
curl https://api.betahunter.app/v1/getModelsAndPresets \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"
GET/v1/dashboard/analytics

Returns dashboard metrics by period, account, or preset, including follow-ups, CTA phase, conversions, duplicate skips, internal-account skips, and lead filters.

Dashboard analytics
curl "https://api.betahunter.app/v1/dashboard/analytics?period=30days&presetId=pre_7f2a9c" \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"
GET/v1/dashboard/conversations

Returns recent conversation summaries for the dashboard activity table and for operators who want to inspect why a lead was skipped or converted.

Recent conversations
curl "https://api.betahunter.app/v1/dashboard/conversations?period=30days&limit=50" \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"

Preset lead controls return messagedAlready or internalAccount on generateResponse when a lead should not be messaged. Those are normal 200 outcomes with empty content, so your sender can skip cleanly.

Check balance

GET/v1/balance

Returns the remaining conversation credits for your key.

curl
curl https://api.betahunter.app/v1/balance \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"
200 Response
{ "creditsRemaining": 920 }
FieldTypeDescription
creditsRemainingnumberConversation credits left on your key. Normally a whole number, but can be fractional (e.g. 919.8) once the 0.2 lead-filter charge has been applied.

Funnel statistics

POST/v1/statistics

Returns a funnel rollup for the authenticated key. Omit the body for all-time, all-account stats, or pass an account and period to narrow the result.

curl
curl https://api.betahunter.app/v1/statistics \
  -H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{ "period": "7days", "account": "acct_42" }'
200 Response
{
  "conversations": 10,
  "messages": 134,
  "ctaShared": 6,
  "conversions": 3,
  "conversionRate": 0.3,
  "blocked": 1
}

Request body

FieldTypeDescription
accountstringOptional accountId to scope the rollup to one account.
periodstring"today", "7days", "30days", or "all". Default "all".

Response

FieldTypeDescription
conversationsintegerConversation count in the selected scope.
messagesintegerTotal stored lead-message exchanges in the selected scope.
ctaSharedintegerConversations where the CTA link has been sent at least once.
conversionsintegerConversations marked converted.
conversionRatenumberConversions divided by conversations. Returns 0.0 when there are no conversations.
blockedintegerUnderage-blocked conversations.

Errors

Authentication, validation, and capacity problems use standard HTTP status codes. Generate business-stop outcomes (not your turn, capped, out of credits, blocked) are not errors; they come back as 200 with a flag set, as described above.

StatusMeaningWhen
200OKA reply was generated, an empty turn was abandoned, or a business-stop flag is set.
401UnauthorizedMissing, unknown, or disabled API key.
422Unprocessable EntityA required field is missing or malformed (e.g. no recipient.id, a message without a timestamp, or an invalid period).
429Too Many RequestsOver 6,000 generate requests/min, or over 3,000 media-bearing generate requests/min, on one key.
503Service UnavailableA temporary global capacity guard. Back off and retry.
500Internal ErrorAn unexpected server error.

Error bodies are JSON: { "error": "unauthorized", "message": "..." }. Validation errors (422) also include a details array of { field, message } entries.

Rate limits

  • POST /v1/generateResponse: 6,000 requests per minute, per key.
  • POST /v1/generateResponse: 3,000 media-bearing requests per minute, per key.

Exceeding either limit returns 429. Need higher limits? Ask on Telegram.