ChatPersona.

API reference

Everything you need to build a persona and start calling /v1/chat — the request/response shape, every persona field, status codes, and how credits work.

Quickstart

The real request, against the real host. Swap in your key (from your dashboard) and go.

curl
curl -X POST https://api.chatpersona.ai/v1/chat \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"persona":{"name":"Nova","coreVoice":"playful, flirty, lowercase texting"},"messages":[{"role":"user","content":"hey, you up?"}],"allow_explicit":false}'
JavaScript (fetch)
const res = await fetch('https://api.chatpersona.ai/v1/chat', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + apiKey,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    persona: { name: 'Nova', coreVoice: 'playful, flirty, lowercase texting' },
    messages: [{ role: 'user', content: 'hey, you up?' }],
    allow_explicit: false,
  }),
});

const data = await res.json();
console.log(data.reply);
Python (requests)
import requests

res = requests.post(
    "https://api.chatpersona.ai/v1/chat",
    headers={"Authorization": f"Bearer {api_key}"},
    json={
        "persona": {"name": "Nova", "coreVoice": "playful, flirty, lowercase texting"},
        "messages": [{"role": "user", "content": "hey, you up?"}],
        "allow_explicit": False,
    },
)

data = res.json()
print(data["reply"])

Authentication

Every request to /v1/chat carries your API key as a bearer token:

Authorization: Bearer cp_live_...

Get a key by creating an account — one is issued immediately and shown to you once on your dashboard. Access is invite-only: every new account starts pending and is reviewed by a human before it can call the API. Your key already works the moment you're approved — you never need to regenerate it.

POST /v1/chat

Request body

FieldTypeNotes
personaobject, requiredThe full persona object — see the Persona object section below. Not validated for exact shape beyond a few optional fields; unrecognized fields pass through.
messagesarray, required (max 200)Conversation so far, oldest first. Each item is {role: 'user'|'assistant', content: string}; content is capped at 16000 characters. The ENTIRE request body — persona + messages + everything else combined, as JSON-serialized bytes — is capped at 1,048,576 bytes (1MB), enforced before any field is even parsed. This is SMALLER than the theoretical maximum implied by the per-field limits above (200 messages × 16,000 chars ≈ 3.2MB) — a conversation approaching that theoretical ceiling will not fit through a single call; trim history (or send fewer/shorter messages) to stay under the 1MB total.
allow_explicitboolean, default falsePermits explicit adult content for this conversation.
languagestring, default 'auto''auto' detects the reply language from the last message. Any other value (an ISO 639-1 code, e.g. 'es') is an EXPLICIT OVERRIDE that locks the reply to that language, regardless of what was actually written.
style.explicitnessinteger 0-3, optionalHeat level once `allow_explicit` is true (0 = none .. 3 = full).
style.max_bubblesinteger 1-3, optional (default 3)Maximum number of chat-style bubbles the reply is split into.
streamboolean, default falseAccepted for forward compatibility. Currently ALWAYS ignored — every response is a complete, non-streaming JSON object.

Response body (200)

FieldTypeNotes
replystringThe full reply text (all bubbles joined with newlines).
bubblesstring[]The reply split into individual chat-style messages, at most `style.max_bubbles` of them.
usage.input_tokensnumberInput tokens consumed by this turn.
usage.output_tokensnumberOutput tokens generated by this turn.
usage.creditsnumberCredits charged for this call — 1 on any served reply, including a refusal.
flaggedbooleantrue when the request was refused by the safety floor. `reply`/`bubbles` still contain an in-character decline — the call is still charged.

The persona object

Personas are not stored anywhere. You pass the full persona object inline on every request — the API is completely stateless and retains nothing about your conversation between calls. Continuity comes entirely from the messages array you send: resend as much of the conversation history as you want the model to remember on every call.

Every field below is optional (an empty {} persona is valid), but the more you give the model, the more convincing it is. examples is the single highest-leverage field — a handful of real message pairs teaches voice far better than any amount of adjectives in coreVoice.

Schema fields

FieldDescriptionExample
name
string
Display name the model role-plays as — read directly off the persona, not part of the formal schema but universally set. "Nova"
bio
string | string[]
Free-form "about you" background paragraph(s), folded into the identity block verbatim. "photographer, loves hiking"
coreVoice
string
Free-text tone/voice description. The single most direct lever on how the persona sounds. "playful, flirty, lowercase texting"
openers
string[]
Example conversation-opening lines for this persona. ["hey stranger 😏", "morning! sleep okay?"]
endearments
string[]
Affectionate terms the persona uses ("babe", "liebling", …). ["babe", "love", "trouble"]
topKinks
string[]
Topics/kinks the persona leans into. Only surfaces explicitly when the request sets `allow_explicit:true`. ["lingerie", "praise", "roleplay"]
bannedWords
string[]
Words/phrases the persona must never say, rendered verbatim into the prompt as a hard avoid-list. ["actually", "moist"]
examples
Array<{userMessage, agentResponse}>
THE HIGHEST-LEVERAGE FIELD. Real message pairs the model mimics verbatim — casing, length, emoji choice, typo tolerance, directness. Up to 18 pairs are used; more than that is a no-op. This is what turns a flat character description into a convincing one. [{"userMessage":"morning!","agentResponse":"morning babe, still in bed lol"}]
languages
string[] (ISO 639-1)
Languages the persona actually speaks. Clamps auto-detection so a reply never drifts into a language the persona was never meant to know. ["en", "es"]
langDefault
string
Legacy single-language default, kept for backward compatibility. Prefer `languages`. "en"
locationGuard
string | null
Per-persona "never disclose location" rule text, inlined verbatim as a hard constraint. "never say what city or country you're in"
bioFacts
object
Fixed, canonical answers for age / hometown / currentCity / pets / siblings / relationshipStatus, so the model gives the SAME answer every time instead of improvising a different one per session. Any fact not listed here is deflected, never invented. {"age":27,"hometown":"Portland, OR","pets":"one cat"}

Additional voice-signature fields

Not part of the formal schema — passed straight through, and genuinely read by the agent's prompt renderer if present.

FieldDescriptionExample
emojiFrequency
'none' | 'rare' | 'moderate' | 'frequent'
How often the model reaches for an emoji. Defaults to a moderate rate if unset. "moderate"
textingStyle.abbreviations
string[]
Texting abbreviations the persona uses naturally. {"abbreviations":["lol","omg","rn"]}
styleRules
string[]
Extra freeform writing-style rules, appended verbatim to the writing-style block. ["never use exclamation points"]
doNotSay
string[]
A second avoid-list (distinct from `bannedWords`), rendered verbatim as "NEVER SAY THESE WORDS". ["as an AI"]
age / location / timezone / appearance
number | string
Light identity details folded into the "about you" block. {"age":27,"appearance":"tall, dark hair"}
personalityDescription / adjectives / topics
string / string[] / string[]
Extra personality framing and discussion topics layered onto `coreVoice`. {"adjectives":["sarcastic","warm"]}
endearmentBudget
{ terms: [{word}], rate: number }
Finer-grained pacing control over endearment usage than the plain `endearments` list — a target rate plus a ranked term list. {"terms":[{"word":"babe"}],"rate":0.2}
openerVariety
string[]
The persona's actual observed opening lines, quoted verbatim as voice-signature guidance (distinct from `openers`). ["hey you 😏", "guess what"]
sentenceLengthDistribution
{ p50: number, p90: number }
Typical (p50) and occasional-max (p90) per-message character counts. {"p50":40,"p90":90}
multiSendRate / questionRate
number (0-1)
How often the persona sends multiple bubbles in a row / ends a turn with a question. {"multiSendRate":0.3,"questionRate":0.4}

A complete, realistic persona

Most fields together, ready to copy into the sandbox or your own request:

persona.json
{
  "name": "Nova",
  "bio": "27, photographer based on the west coast. Loves hiking, film cameras, and late-night diner runs. Sarcastic but warm once she likes you.",
  "coreVoice": "playful, flirty, a little sarcastic, lowercase texting, quick with a tease but genuinely warm underneath",
  "openers": ["hey stranger 😏", "morning! sleep okay?", "okay I need your opinion on something"],
  "endearments": ["babe", "trouble", "love"],
  "topKinks": ["praise", "lingerie", "roleplay"],
  "bannedWords": ["actually", "moist", "delve"],
  "examples": [
    { "userMessage": "hey what are you up to", "agentResponse": "just got out of the shower lol, thinking about you tbh" },
    { "userMessage": "you always know what to say", "agentResponse": "mm i just say what's true babe" },
    { "userMessage": "i had a rough day", "agentResponse": "come here, tell me what happened. i've got you" }
  ],
  "languages": ["en", "es"],
  "langDefault": "en",
  "locationGuard": "never reveal what city, state, or country you're actually in — deflect playfully if asked",
  "bioFacts": {
    "age": 27,
    "hometown": "Portland, OR",
    "currentCity": "Los Angeles, CA",
    "pets": "one cat named Biscuit",
    "siblings": "younger brother",
    "relationshipStatus": "single"
  },
  "emojiFrequency": "moderate",
  "textingStyle": { "abbreviations": ["lol", "omg", "rn", "tbh"] }
}

Status codes

StatusErrorMeaning
200OKA reply was generated — check `flagged`, a safety-floor refusal is still a 200.
400invalid request / detailMalformed JSON, or a body that fails schema validation (`detail` explains what failed).
401unauthorizedMissing/malformed `Authorization` header, or the API key is unknown or revoked.
402insufficient_creditsFree allowance and paid credit balance are both exhausted. Checked BEFORE the rate limit below — see the precedence note.
403account_pending / account_suspendedThe account has not been approved yet, or has been suspended.
429rate_limitedExceeded the per-key rate limit (60 requests/minute by default).
500internal errorSomething failed while generating the reply.

Precedence: a request that is both over-budget AND over the rate limit always gets 402, never 429 — the credit reserve is checked, and the request turned away, before the rate limit is even evaluated.

Credits & limits

  • 1 reply = 1 credit = 2¢.
  • New accounts start with 200 free replies — no card required.
  • A refusal (flagged:true) still consumes 1 credit — the model still did the work of evaluating the request.
  • Default rate limit: 60 requests/minute per API key.
  • Total request body cap: 1,048,576 bytes (1MB), across the whole JSON payload — see the request-body table above for how this interacts with the per-field messages limits.

Safety

Adult content is first-class here: set allow_explicit:true (and optionally style.explicitness) and the persona holds frank, explicit conversation without refusing or moralizing.

One hard floor is always enforced, regardless of allow_explicit: content involving minors (or any other illegal material) and bestiality are refused — both on what you send AND on what the model generates (an input check plus an output backstop). A request that trips this returns flagged:true and an in-character decline, never the requested content.