Betahunter API
One REST endpoint takes the conversation so far and returns the next message to send, human-paced, on-persona, and ready to deliver. 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 in an X-API-Key header instead. 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 produces a reply in a conversation. Every later reply in that same conversation is free, even after your balance reaches zero.
- Turns that generate nothing (not your turn, capped, out of credits, or blocked) are never charged.
- 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.
Request body
| Field | Type | Description | |
|---|---|---|---|
| accountId | string | required | Your account or sub-account id. Together with recipient.id it identifies the conversation. |
| recipient | object | required | The lead you are talking to. See Recipient. |
| chatHistory | array | required | The recent conversation, oldest first. Each item is a Message (up to 200). Send [] for a brand-new thread. |
| persona | object | The creator the bot is playing. See Persona. | |
| isFollowUp | boolean | Mark the turn as a follow-up to a lead who went quiet. Default false. |
Recipient object
| Field | Type | Description | |
|---|---|---|---|
| id | string | required | Stable unique id for the lead. Part of the conversation key. |
| 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, "assistant" for the creator. |
| content | string | required | The message text. |
| timestamp | integer | required | Unix time in seconds. Used to order the thread. |
| 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. | |
| 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[] | The media catalog she can send. Each item is a Photo. |
Photo object (each persona.photos item)
| Field | Type | Description | |
|---|---|---|---|
| url | string | required | The media URL. |
| type | string | Catalog bucket: "Primary" or "Sexy". Default "Primary". |
Response
The endpoint always returns 200. The body tells you what to do through content plus a set of outcome flags. When any flag is true, content is empty and you were not charged.
| 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 | Opaque id for this conversation. 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. |
| 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. |
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 | For an image, which catalog bucket it came from: "Primary" or "Sexy". Absent for text. |
Example
{
"accountId": "acct_42",
"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" },
{ "type": "text", "content": "i could think of a few better ways to relax tho 😉" }
],
"converted": false,
"notOurTurn": false,
"timewaste": false,
"aiCreditOver": false,
"underage": false,
"conversationId": "k3p9x2: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.
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 | integer | Conversation credits left on your key. |
Errors
Authentication, validation, and capacity problems use standard HTTP status codes. Business 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, or a business-outcome 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, or a message without a timestamp). |
429 | Too Many Requests | Over 6,000 requests/min, or over 3,000 media-bearing requests/min, on one key. |
503 | Service Unavailable | A temporary global capacity guard. Back off and retry. |
Error bodies are JSON: { "error": "unauthorized", "message": "..." }. Validation errors (422) also include a details array of { field, message } entries.
Rate limits
- 6,000 requests per minute, per key.
- 3,000 media-bearing requests per minute, per key.
Exceeding either limit returns 429. Need higher limits? Ask on Telegram.