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. It works right away, with free replies already on it, so you can make your first call before adding any billing details.

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.
conversation_notesstring, optional (max 4000 chars)Freeform background about the person being talked to — what the persona already knows about them from earlier exchanges. Used for continuity and tone; never quoted back, and never treated as facts about the persona herself. Send it fresh on every request, since it is per conversation rather than per persona.
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"]
notes
string | string[]
Freeform background about the persona that should <strong>shape</strong> how she behaves without being recited as a list. Use this for operator notes that do not split cleanly into individual facts. Distinct from <code>bioFacts</code>, which is the set of answers she is expected to give when asked directly. "grew up around horses, hates small talk, close to her sister"
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. Set this only if the persona is meant to be multilingual or to speak a language other than the one it is written to in: it pins each reply to whichever of these languages the incoming message is in, so a persona with its own strong foreign voice cannot drift back into it. Leave it out for a single-language persona and the reply simply follows the language it was written in, which is the right default and needs no configuration. ["en", "de"]
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"]}
capitalization
'lowercase' | 'standard' | 'none'
How the persona capitalises. Defaults to 'lowercase' (all-lowercase texting) when unset, so existing personas are unchanged. Use 'standard' for normal sentence-case and proper nouns, or 'none' to state no rule and let styleRules decide. "standard"
styleRules
string[]
Extra freeform writing-style rules, rendered in their own block. These take precedence over the built-in writing-style defaults wherever the two conflict. ["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 10 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.