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.
https://api.betahunter.appAuthentication
Every request authenticates with your secret API key, sent as a Bearer token in the Authorization header:
Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxxKeys 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 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 itsconversationId. - 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 (
tierFilteredorgenderFiltered), 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,
creditsRemainingcan be fractional (e.g.919.8). - Check your remaining credits any time with
GET /v1/balance.
Generate a reply
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
| Field | Type | Description | |
|---|---|---|---|
| accountId | string | required | Your account or sub-account id. Alias accountID is accepted. Together with recipient.id it identifies the conversation. |
| recipient | object | required | The lead you are talking to. Alias userInfos is accepted. See Recipient. |
| chatHistory | array | The recent conversation in send order. Each item is a Message (max 200). Defaults to [] for a brand-new thread. | |
| persona | object | The creator the bot is playing. See Persona. | |
| presetId | string | Stored 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. | |
| modelId | string | Optional model id from GET /v1/getModelsAndPresets. Today the default is betahunter-v3. | |
| isFollowUp | boolean | Mark the turn as a follow-up to a lead who went quiet. Default false. | |
| direction | string | Who 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. | |
| generateOpener | boolean | When true and chatHistory is empty, generate the first message (opener) instead of returning notOurTurn. Default false. |
Recipient object
| Field | Type | Description | |
|---|---|---|---|
| id | string | required | Stable unique id for the lead. Alias useridentifier is accepted inside userInfos. |
| name | string | The lead's display name. | |
| username | string | The lead's handle. | |
| bio | string | The lead's bio, used to personalize. | |
| location | string | The lead's location, used to personalize. |
Message object (each chatHistory item)
| Field | Type | Description | |
|---|---|---|---|
| role | string | required | "user" for the lead, "bot" or "assistant" for the creator. |
| content | string | The message text. Missing or null is treated as an empty string. | |
| timestamp | integer | required | Unix time in seconds. Used for validation and de-duplication. |
| id | string | Your own id for the message, if you have one. Used to de-duplicate when you resend the window. |
Persona object
| Field | Type | Description | |
|---|---|---|---|
| name | string | The creator's name. Default "her". | |
| age | integer | The creator's age. Default 22. | |
| city | string | The creator's city. | |
| userInfo | string | Short self-description for the persona (look, vibe, backstory). | |
| ctaInfo | string | What you are selling, used when she pitches the link. | |
| ctaLink | string | The link to close the lead to. Leave blank to disable pitching. | |
| ctaMinExchanges | integer | How many lead replies before she may pitch the link. Default 8. | |
| photos | Photo[] | Inline media catalog she can send. Dashboard-managed media is normally stored on a preset instead. Each item is a Photo. | |
| phases | object[] | Optional engine phase config, passed through as provided. | |
| matchLocation | boolean | When 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. | |
| tierFilter | string | Country-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. | |
| filterFemales | boolean | When true, female leads are skipped before generation. Default false. See Lead filters. |
Photo object (each persona.photos item)
| Field | Type | Description | |
|---|---|---|---|
| url | string | required | The media URL. Alias photoUrl is accepted. |
| type | string | Catalog bucket. Accepts "Primary"/"main" and "Sexy"/"sexy"/"specific". Alias photoType is accepted. Default "Primary". | |
| outfitDay | string | Optional outfit bucket, such as "outfit-day-3". Dashboard uploads normalize plain numbers like "3". | |
| photoTime | string | Optional lighting bucket: "day-time", "night-time", or "either". | |
| priority | integer | Optional 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.
| Field | Type | Description |
|---|---|---|
| content | array | The messages to send, in order. Each item is a Content item. May be empty. |
| converted | boolean | true if the lead was closed to your link this turn. |
| conversationId | string | customerId:accountId:recipient.id conversation key. Use it to correlate turns. |
| notOurTurn | boolean | true 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. |
| timewaste | boolean | true when the conversation hit the message cap (50 lead messages) and was stopped. |
| aiCreditOver | boolean | true when you are out of credits on a new conversation, so nothing was generated. Top up to continue. |
| underage | boolean | true when the lead tripped the age-safety check. The thread is blocked permanently. Nothing to send. |
| tierFiltered | boolean | true when the lead was skipped by the tierFilter country-tier filter. Nothing to send; billed 0.2 credit once. |
| genderFiltered | boolean | true when the lead was skipped by the filterFemales filter. Nothing to send; billed 0.2 credit once. |
| messagedAlready | boolean | true when preset lead controls found the same lead recently engaged by another account. Nothing to send. |
| engagedUser | object | null | Details about the existing engagement when messagedAlready is true. |
| internalAccount | boolean | true when preset lead controls identify the recipient as one of your own accounts. Nothing to send. |
| statistics | object | Capital-style phase metadata, including currentPhase, currentCycle, and stage. |
Content item
| Field | Type | Description |
|---|---|---|
| type | string | "text" or "image". |
| content | string | The message text (for "text") or the media URL to send (for "image"). |
| mediaPool | string | null | For an image, which catalog bucket it came from: "Primary" or "Sexy". null for text. |
Example
{
"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" }
}{
"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.
Create a preset from your dashboard or through the API.
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 }
]
}
}'Upload media as JSON base64. Betahunter stores it under the preset and usestype, outfitDay, photoTime, and priority when choosing which image to send.
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.
Returns the callable model ids and your saved presets, useful for a Capital-style dropdown before sending modelId and presetId.
curl https://api.betahunter.app/v1/getModelsAndPresets \
-H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"Returns dashboard metrics by period, account, or preset, including follow-ups, CTA phase, conversions, duplicate skips, internal-account skips, and lead filters.
curl "https://api.betahunter.app/v1/dashboard/analytics?period=30days&presetId=pre_7f2a9c" \
-H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"Returns recent conversation summaries for the dashboard activity table and for operators who want to inspect why a lead was skipped or converted.
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
Returns the remaining conversation credits for your key.
curl https://api.betahunter.app/v1/balance \
-H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx"{ "creditsRemaining": 920 }| Field | Type | Description |
|---|---|---|
| creditsRemaining | number | Conversation 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
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 https://api.betahunter.app/v1/statistics \
-H "Authorization: Bearer bh_live_xxxxxxxxxxxxxxxxxxxx" \
-H "content-type: application/json" \
-d '{ "period": "7days", "account": "acct_42" }'{
"conversations": 10,
"messages": 134,
"ctaShared": 6,
"conversions": 3,
"conversionRate": 0.3,
"blocked": 1
}Request body
| Field | Type | Description | |
|---|---|---|---|
| account | string | Optional accountId to scope the rollup to one account. | |
| period | string | "today", "7days", "30days", or "all". Default "all". |
Response
| Field | Type | Description |
|---|---|---|
| conversations | integer | Conversation count in the selected scope. |
| messages | integer | Total stored lead-message exchanges in the selected scope. |
| ctaShared | integer | Conversations where the CTA link has been sent at least once. |
| conversions | integer | Conversations marked converted. |
| conversionRate | number | Conversions divided by conversations. Returns 0.0 when there are no conversations. |
| blocked | integer | Underage-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.
| Status | Meaning | When |
|---|---|---|
200 | OK | A reply was generated, an empty turn was abandoned, or a business-stop flag is set. |
401 | Unauthorized | Missing, unknown, or disabled API key. |
422 | Unprocessable Entity | A required field is missing or malformed (e.g. no recipient.id, a message without a timestamp, or an invalid period). |
429 | Too Many Requests | Over 6,000 generate requests/min, or over 3,000 media-bearing generate requests/min, on one key. |
503 | Service Unavailable | A temporary global capacity guard. Back off and retry. |
500 | Internal Error | An 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.