Kumkuat API guide
A quick introduction to APIs
An API (application programming interface) lets your own software do what you would otherwise do by clicking in the Kumkuat web app. Anything that can send an HTTPS request can use it: a Python or JavaScript script, a spreadsheet macro, Postman, Zapier, or an AI assistant such as Claude or ChatGPT.
Five ideas cover almost everything in this guide:
- Base URL — every request goes to
https://api.kumkuat.ai. - Endpoint — the path after the base URL that names the thing you are working with, such as
/api/v1/personas. - Method — the verb.
GETreads,POSTcreates or runs something,PUTreplaces a record,PATCHchanges part of one,DELETEremoves it. - Authentication — every request carries your API key in a header called
X-API-Key. The key decides which workspace you are acting in. - JSON — requests and responses are plain-text JSON objects, for example
{"name": "Jane Doe", "category": "Journalists"}.
In Kumkuat, an audience is a set of personas (synthetic versions of real stakeholders such as journalists, investors, or legislators) grouped by category. The three workflows below map to three parts of the API:
| Workflow | What you do | Main endpoints |
|---|---|---|
| 1. Add or edit audiences | Create personas, tune their voice, sources, and behavior | /api/v1/personas, /api/v1/personas/{id}/… |
| 2. Audience scorecards | Grade how personas react to a document or message | /api/v1/personas/reaction-insights, /api/v1/audience-summaries |
| 3. Audience chat | Ask personas questions and get replies | /api/v1/personas/interact |
The full, always-current reference is the interactive Swagger page at api.kumkuat.ai/docs, with the machine-readable spec at api.kumkuat.ai/openapi.json. The same operations are documented on this site in the API reference.
Getting set up (5 minutes)
- Create an API key. In Kumkuat, go to Workspace → API → Manage API Keys and create a key. Copy it once; treat it like a password.
- Pick the right workspace first. Each workspace has its own personas and its own keys. A key from the wrong workspace is the most common reason nothing matches.
- Store the key outside your code. Put it in an environment variable, for example
export KUMKUAT_KEY="<your-key>"on Mac/Linux orsetx KUMKUAT_KEY "<your-key>"on Windows. - Test the connection. A 200 response with your workspace name means you are ready.
curl -s https://api.kumkuat.ai/api/v1/workspace \
-H "X-API-Key: $KUMKUAT_KEY"
The same call in Python, which the rest of this guide builds on:
import os, requests
BASE = "https://api.kumkuat.ai"
HEADERS = {"X-API-Key": os.environ["KUMKUAT_KEY"], "Content-Type": "application/json"}
def kk(method, path, **kwargs):
r = requests.request(method, BASE + path, headers=HEADERS, timeout=120, **kwargs)
r.raise_for_status()
return r.json() if r.content else None
print(kk("GET", "/api/v1/workspace"))
You can revoke or rotate keys at any time in the app or through DELETE /api/v1/auth/keys/{key_id} and POST /api/v1/auth/keys/{key_id}/rotate.
Kumkuat has two kinds of key, and both are sent the same way. Two calls in this guide, listing personas (GET /api/v1/personas) and creating a document (POST /api/v1/documents), currently need a personal key; every other call accepts either kind. The quickstart explains where each kind is created and when to use which.
Use case 1: Add or edit audiences
Personas are the building blocks of an audience; the category field decides which audience a persona belongs to. Most teams follow the same loop: find what exists, create what is missing, then tune.
Step 1 — List the personas you already have
page = kk("GET", "/api/v1/personas", params={"limit": 500, "offset": 0})
print(page["total"], "personas")
for p in page["personas"]:
print(p["id"], p["name"], p.get("category"))
- The list is paged:
limitis 200 by default and 500 at most. Raiseoffsetuntil you havetotalrows. - Add
summary=1for a lightweight list (id, name, category) when you only need to match names. - Large workspaces return several MB; send
Accept-Encoding: gzip(most HTTP libraries do this for you).
Step 2 — Create new personas
Pick the option that matches how much you already know about each person.
| You have | Endpoint | Behavior |
|---|---|---|
| A full profile for one persona | POST /api/v1/personas | Creates it immediately |
| Full profiles for many personas | POST /api/v1/personas/bulk | Creates them in one call; skips names that already exist |
| Just names (plus organization and context) | POST /api/v1/personas/build | Kumkuat researches and writes each profile in the background (1–100 per call) |
| A name and a few fields | POST /api/v1/personas/generate-draft | Returns AI-drafted fields for you to review, then save with POST /api/v1/personas |
The fastest route from a list of names to a working audience is build:
jobs = kk("POST", "/api/v1/personas/build", json={"personas": [
{"name": "Jane Doe", "organisation": "Financial Times", "context": "Covers EU fintech regulation"},
{"name": "John Roe", "organisation": "Bloomberg", "context": "Banking and payments reporter"},
]})
# -> 202 {"jobs": [{"job_id": "...", "name": "Jane Doe"}, ...], "requested": 2}
for job in jobs["jobs"]:
print(kk("GET", f"/api/v1/jobs/{job['job_id']}")) # poll until complete
Note the British spelling organisation. Sending the same name twice shares one job, so re-running a list is safe.
Step 3 — Edit a persona
PUT /api/v1/personas/{id} replaces the whole record. Always read the persona first, change only the fields you mean to change, and send the full object back; a partial body can blank out other fields.
p = next(x for x in kk("GET", "/api/v1/personas", params={"limit": 500})["personas"]
if x["name"] == "Jane Doe (Financial Times)")
p["category"] = "EU Fintech Press"
p["description"] += " Increasingly skeptical of crypto-asset rules."
kk("PUT", f"/api/v1/personas/{p['id']}", json=p)
Use the object from the list endpoint. GET /api/v1/personas/{id} wraps it as {"persona": {…}}, so unwrap it before sending it back.
Step 4 — Tune how a persona thinks and sounds
Tuning is where personas go from generic to accurate. Each lever has its own endpoint, so you never have to rewrite the whole record.
| Lever | Endpoint | Body |
|---|---|---|
| Voice (tone, vocabulary, rhetoric) from real writing | POST /api/v1/personas/{id}/analyze-voice | {"samples": ["…", "…"], "profile_name": "Professional Voice"} — up to 7 samples |
| Knowledge from a website or publication page | POST /api/v1/personas/{id}/link-external-source | {"source_url": "https://…", "label": "Financial Times"} |
| Knowledge from LinkedIn | POST /api/v1/personas/{id}/link-linkedin | {"linkedin_url": "https://www.linkedin.com/in/…"} |
| Profile links (X, Bluesky, LinkedIn) | POST /api/v1/personas/{id}/quick-link | {"url": "https://x.com/…", "platform": "x"} |
| A tuning note ("always asks about timelines") | POST /api/v1/personas/{id}/refinements | the note text — see Swagger |
| A trusted example answer | POST /api/v1/personas/{id}/golden-responses | {"prompt": "…", "golden_response": "…"} |
| Profile photo | POST /api/v1/personas/avatar/upload, then PUT | multipart field file; then set avatar_url and avatar_source: "upload" on the persona |
| Re-crawl linked sources now | POST /api/v1/personas/{id}/refresh | none (runs in the background) |
Two cautions:
- Each
analyze-voicecall creates a new voice profile. Checkvoice_profileson the persona first so a retry does not leave duplicates. link-external-sourcereplaces the persona's current source. Skip it whenexternal_source_kb_idis already set, unless you mean to replace it.
Step 5 — Apply one tuning change to many personas
POST /api/v1/personas/bulk-update takes one explicit entry per persona and returns a per-persona report. Send it with dry_run: true first to see the before/after without writing anything, review it, then send the same body with dry_run: false.
Organize audiences
GET /api/v1/personas/categories lists your audience categories. POST /api/v1/audience-groups adds one, and PUT /api/v1/audience-groups sets the order they appear in the app.
Use case 2: Create audience scorecards
A scorecard is Kumkuat's graded read of how each persona would react to a piece of content: a press release, a pitch, talking points, or a draft an AI assistant just wrote for you. You get one scorecard per persona, then a roll-up for the whole audience.
The flow has four steps:
- Put the content into Kumkuat as a document.
- Choose the personas who should react.
- Generate their scorecards.
- Read the audience-level summary and grade.
Step 1 — Add the content as a document
Upload a file (PDF, DOCX, TXT) and Kumkuat ingests it:
with open("press_release.docx", "rb") as f:
doc = requests.post(f"{BASE}/api/v1/documents/upload",
headers={"X-API-Key": os.environ["KUMKUAT_KEY"]}, # no Content-Type: requests sets multipart
files={"file": f}, timeout=300).json()
doc_id = doc["id"] # check the response for the exact id field
If the content was written by an AI assistant or a script, skip the file and send the text directly. title and content are both required:
doc = kk("POST", "/api/v1/documents", json={
"title": "Q3 launch press release — draft B",
"content": draft_text,
})
For a web page or video, use POST /api/v1/ingest/auto-detect or POST /api/v1/ingest/youtube.
Step 2 — Choose the personas
Use the persona list from Use case 1 and filter by category to score a whole audience, or pick individual ids.
all_personas = kk("GET", "/api/v1/personas", params={"limit": 500})["personas"]
audience = [p for p in all_personas if p.get("category") == "EU Fintech Press"]
Step 3 — Generate the scorecards
| Audience size | Endpoint | How it returns |
|---|---|---|
| Fewer than 10 personas | POST /api/v1/personas/reaction-insights | Waits and returns the scorecards in the response |
| 10 or more personas | POST /api/v1/personas/reaction-insights/async | Returns a job_id right away; poll GET /api/v1/jobs/{job_id} |
The body takes the full objects, not just ids: personas is a list of persona objects (at least id, name, description) and document is a document object (id, title, content). The easiest way to get both is to fetch them:
document = kk("GET", f"/api/v1/documents/{doc_id}")
document = document.get("document", document) # unwrap if needed
body = {
"personas": [{"id": p["id"], "name": p["name"], "description": p.get("description", "")} for p in audience],
"document": {"id": document["id"], "title": document["title"], "content": document["content"]},
"force_refresh": False,
}
if len(audience) < 10:
result = kk("POST", "/api/v1/personas/reaction-insights", json=body)
for card in result["insights"]:
print(card["persona"]["name"], card["grade"], card["status_counts"])
else:
job = kk("POST", "/api/v1/personas/reaction-insights/async", json=body)
# poll kk("GET", f"/api/v1/jobs/{job['job_id']}") until it reports complete
Each scorecard in insights has:
| Field | What it holds |
|---|---|
grade | Letter grade, A to F |
status_counts | How many judgments landed well, mixed, or badly: {"good": 5, "warn": 0, "bad": 0} |
output_json.overall_take | The persona's one-paragraph verdict |
output_json.key_judgments | Individual judgments, each with a status, tag, and 1–5 confidence |
output_json.evidence_and_signals | Quotes from your content and why they matter to this persona |
output_json.implications_and_actions | Recommended next steps |
output_json.counterpoints_and_risks | Where the reaction could go wrong |
Scorecards are cached per persona and document text. Calling again with the same content returns the saved result instantly (cache_hit: true); set force_refresh: true only after you re-tune a persona. Send real content: an empty document still gets graded, and grades an F.
To read scorecards back later, including ones created in the app:
rows = kk("GET", "/api/v1/personas/reaction-insights", params={"latest_only": "true"})
latest_only=true keeps the newest scorecard for each persona–document pair. Leave it off only when you want run history; GET /api/v1/personas/reaction-insights/history gives the history for one pair.
Step 4 — Get the audience grade and summary
GET /api/v1/audience-summaries?category=EU%20Fintech%20Press— an overall grade plus themes, recommended actions, and risks, synthesized from that audience's recent scorecards. Addforce_refresh=trueto regenerate.GET /api/v1/audience-summaries/grades— cached grade and trend for every audience in one fast call (no regeneration). Good for dashboards.GET /api/v1/persona-summaries— the same summary for a single persona.GET /api/v1/personas/grade-timeline— grades over time, for trend charts.
Using an AI assistant to drive scorecards
The API does not care who wrote the content, so a common pattern is: ask your assistant for three versions of a message, upload each as a document, score each against the same audience, and keep the version with the best grade. To improve a weak draft, POST /api/v1/content-creation/rewrite-segments and POST /api/v1/content-creation/rewrite-phrases suggest edits driven by the audience's feedback. Using AI assistants with the Kumkuat API has copy-paste prompts for this.
Use case 3: Set up an audience chat
Audience chat means asking a persona a question and getting an in-character reply. You do this with POST /api/v1/personas/interact, one persona per call. To chat with a whole audience, loop over its personas.
Step 1 — Ask one persona a question
reply = kk("POST", "/api/v1/personas/interact", json={
"persona_id": persona["id"],
"input_content": "What is the first question you would ask our CFO about these results?",
"interaction_type": "conversation",
})
print(reply["response"])
| Field | Required | Notes |
|---|---|---|
persona_id | Yes | From the persona list (Use case 1) |
input_content | Yes | Your question, or the full text you want the persona to react to |
interaction_type | No | "conversation" for chat-style answers; the default, "communication", treats the input as content to react to |
voice_profile_id | No | Answer in a specific voice profile instead of the persona's default |
response_language | No | Reply language, e.g. "en", "de" |
The reply comes back in the same response, usually within a few seconds, in the response field (also returned as persona_response and output_content).
Step 2 — Keep a conversation going
Each interact call stands alone; the persona does not remember earlier calls. For a follow-up, include the earlier exchange in input_content:
history = []
def ask(persona_id, question):
transcript = "\n".join(f"{who}: {text}" for who, text in history)
prompt = f"Conversation so far:\n{transcript}\n\nNew question: {question}" if history else question
answer = kk("POST", "/api/v1/personas/interact", json={
"persona_id": persona_id, "input_content": prompt, "interaction_type": "conversation",
})["response"]
history.extend([("Us", question), ("Persona", answer)])
return answer
ask(persona["id"], "Would you cover this announcement?")
ask(persona["id"], "What would change your mind?")
Step 3 — Ask a whole audience the same question
import csv
question = "In one sentence, what is your biggest concern about this launch?"
with open("audience_answers.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["persona", "answer"])
for p in audience: # from Use case 2, Step 2
r = kk("POST", "/api/v1/personas/interact", json={
"persona_id": p["id"], "input_content": question, "interaction_type": "conversation"})
w.writerow([p["name"], r["response"]])
Each reply calls a language model, so set a timeout of 60–120 seconds. For large audiences, run a few requests in parallel.
About /api/chat/sessions
The chat-session endpoints power the chat screen in the app, and you can read them through the API. GET /api/chat/sessions lists past sessions, and GET /api/chat/sessions/{session_id} returns the full transcript, including persona replies people had in the app. Use them to pull app conversations into your own reports.
Sending a message through the API (POST /api/chat/sessions/{session_id}/messages with {"message": "…"}) reaches Kumkuat's general workspace assistant, not a persona. For persona replies from your own code, use interact as shown above.
Close the loop
If a reply is off, say so: POST /api/v1/personas/{id}/feedback records a rating. Strong answers can become golden responses (Use case 1, Step 4) that anchor the persona in future chats.
Troubleshooting and good habits
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 "API key or bearer token required" | Missing or misspelled header | Send X-API-Key: <key> on every request |
| Names you expect are not found | Key belongs to a different workspace | Check GET /api/v1/workspace; use that workspace's key |
| A name almost matches | Persona names often carry a suffix, e.g. Jane Doe (Financial Times) | Match on the name with any trailing ( … ) removed |
| Fields went blank after an edit | PUT was sent with a partial body | Read the full persona, change it, send it all back |
| Duplicate voice profiles | analyze-voice retried after a timeout | Check voice_profiles before calling; delete extras with DELETE /api/v1/personas/{id}/voice-profiles/{profile_id} |
| 422 Unprocessable Entity | Body field missing or misnamed | The response lists the field; compare with Swagger |
| Request times out | Model-backed calls take seconds each | Raise the timeout, use the async / build endpoints, run a few in parallel |
Habits that save time:
- Dry-run first. Print what you would change before writing, and use
dry_run: truewhere offered. - Make scripts resumable. Write each finished persona id to a file and skip it on the next run, so a crash never means starting over.
- Keep keys secret. Never paste a key into code you share, a spreadsheet, or a chat you do not control. Rotate it when a project ends.
Making it easier with Claude skills
Everything above works from any language or tool. If your team uses Claude (in the Claude desktop app, Cowork, or Claude Code), skills remove most of the typing. A skill is a packaged set of instructions and scripts that teaches Claude a repeatable workflow: which endpoints to call, in what order, and which mistakes to avoid. Once installed, you describe the goal in plain English and Claude runs the calls.
Kumkuat workflows that already exist as skills:
| Skill | Say something like | What it does |
|---|---|---|
| Persona voices | "Tune the voices for these personas from this sheet" | Reads writing samples from a spreadsheet, calls analyze-voice per persona, skips and removes duplicates |
| Persona links | "Add these X and LinkedIn handles and ingest the publication pages" | Adds social_links, links sources for ingest, skips curated sources |
| Persona photos | "Upload these headshots to my Senators audience" | Extracts images from a spreadsheet or folder, uploads, and attaches them |
How to use them:
- Create a workspace API key (see Getting set up) and give it to Claude when asked. Revoke it when the job is done.
- Attach the spreadsheet or folder and name the audience or workspace.
- Review the dry-run match report Claude shows you, fix any unmatched names, then approve the write.
To build your own, ask Claude to "create a skill" from a workflow you have just run together, such as scoring every new press release against your investor audience. The same patterns in this guide (read first, dry-run, resumable, verify) make good skill instructions. Other assistants that support tools or function calling can use the same endpoints; point them at api.kumkuat.ai/openapi.json.