Skip to main content

Integrate audiences into a content creation platform

This guide is for teams building a platform that generates or edits content, such as newsletters, social posts, web stories, teases and marketing briefs, who want their users to see how a piece will land with its audiences before it goes out. It walks through where each Kumkuat call sits in an editorial workflow and what to build around it.

It assumes you have read the Quickstart and Score a draft against your audiences; the calls are only summarised here.

The loop​

StepCallNotes
Connect a workspaceGET /api/v1/workspaceOne Kumkuat workspace per customer or brand; one key per workspace
Build the pickerGET /api/v1/audience-groups, GET /api/v1/personas?summary=1Cache; filter out voices and personas without a profile
Check before publishingPOST /api/v1/personas/reaction-insightsInline draft, skip_save: true, the editor's panel
Explain a judgmentPOST /api/v1/personas/interactOptional: let the editor ask the persona directly
PublishPOST /api/v1/documentsFinal text with its published URL
Record the receptionPOST /api/v1/personas/reaction-insights/async, GET /api/v1/jobs/{job_id}Full panel on the stored document
Show audience healthGET /api/v1/audience-summaries/grades, GET /api/v1/audience-summariesGrade and trend per category, read from cache

1. Connect a workspace​

Each of your customers, or each brand inside one customer, maps to a Kumkuat workspace: its personas, documents and dashboards are all scoped there. Store one API key per workspace on your server, encrypted at rest, and verify it with GET /api/v1/workspace when it is entered. Show the returned name and company in your settings screen so the person connecting can see they picked the right workspace.

Use a workspace key for the scoring calls, so the scorecards your platform generates are visible to the whole team in Kumkuat. Listing personas and creating documents currently need a personal key from an admin or owner account, so a connection that does both holds two keys; see the note on key kinds in the Quickstart.

2. Build the audience picker​

Fetch the categories and the persona summaries, cache them for five to fifteen minutes, and render a grouped picker:

  • Group by category, in the sort_order of GET /api/v1/audience-groups, showing display_name when set.
  • Show name, description and avatar_url, and badge entity_kind_label so editors can tell a named journalist from an outlet or a segment.
  • Leave out personas with include_in_audience: false (writing voices) or has_template: false (no profile yet).
  • Let editors save a default panel per content type. Store the persona ids with your content type; a newsletter's default panel might be two consumer segments and a critic, a press release's might be trade press and analysts.

Persona ids are stable for the life of a workspace, so a saved panel keeps working. If a persona is deleted in Kumkuat its id disappears from the listing; drop it from the saved panel when you next refresh.

3. Check before publishing​

When the editor asks for an audience check, or on a pause in typing if you want it to feel live, post the current draft with the panel's ids and skip_save: true. The draft is not added to the workspace; only a seven-day cache entry is kept so that an unchanged draft returns instantly.

What to render from each insight:

  • The grade and the three counts from status_counts, labelled Resonates, Mixed and Doesn't land.
  • overall_take as the one-paragraph summary.
  • Each entry of key_judgments, coloured by status, with its judgment. Follow evidence_and_signals[].maps_to back to the quote and highlight that passage in the editor; that is what makes the check actionable.
  • comms as an Apply suggestion action that replaces the quoted passage with the persona's rewrite, so the editor can accept it, edit it, or ignore it.
  • counterpoints_and_risks as a Potential pushback list under the judgments.

Run the call from a background worker in your platform rather than from the request that handles the editor's click: a panel takes on the order of half a minute, and personas are generated up to four at a time. Show a per-persona progress state and fill cards in as the response arrives. Set the HTTP timeout to at least two minutes.

If a persona comes back with status: "failed" or "timed_out", show it as unavailable with its error_message and let the editor retry that run; the completed personas are cached, so the retry only regenerates the missing ones.

4. Revise and check again​

Every edit changes the document hash, so the next check scores the whole panel afresh. Keep the previous response on your side and show the movement: grade before and after per persona, and which judgments disappeared. Because the panel is the same, the comparison is fair.

For editors who want to understand a judgment or test a fix before rewriting, wire a Ask this audience action to POST /api/v1/personas/interact with interaction_type: "conversation", sending the passage and the question. The reply comes back in about five seconds in the persona's own voice.

5. Publish​

When the piece goes out, create it as a stored document with POST /api/v1/documents (admin or owner key): the final text, a title, the content_type, the published source_url, and media_provenance: "owned". This takes the piece out of draft mode. It now appears in the workspace's library, and every later reaction to it is kept as history.

Then queue the full panel with POST /api/v1/personas/reaction-insights/async on the new document id, using the workspace key so the results are shared, and poll GET /api/v1/jobs/{job_id} until status is completed. The rows land in Audience Scorecards and feed the Audience Dashboard, so the communications team sees in Kumkuat what shipped and how it was received, alongside everything else the company publishes.

Two things to decide with your customer:

  • Which panel runs on publication. The editor's panel is a fine default, but a wider standing panel, every persona in two or three categories, gives the dashboard a consistent read over time.
  • Whether unpublished pieces are ever stored. The default here is no: drafts stay ephemeral, published pieces are stored. Some teams also want abandoned drafts kept for review; if so, store them with a distinguishing content_type such as draft.

6. Show audience health in your platform​

Once published pieces are being stored and scored, you can surface the workspace's overall standing with each audience, the same numbers the Audience Dashboard shows.

GET /api/v1/audience-summaries/grades?categories=Consumer|Journalists returns the cached grade and trend per category in one call and never triggers generation. A category with no reactions yet reads as null:

{ "grades": { "Consumer": { "grade": "D", "trend": "down", "is_stale": false }, "Journalists": null } }

GET /api/v1/audience-summaries?category=Consumer returns the full summary for one category: the grade, the trend, the counts behind it, and a synthesised read of the recent reactions. It is served from cache and refreshed in the background when stale, and it returns 404 when the category has no reactions in the window (days, default 30).

{
"cache_hit": true,
"summary": {
"category": "Consumer",
"grade": "D",
"grade_trend": "down",
"reaction_count": 192,
"status_counts": { "good": 58, "warn": 94, "bad": 34 },
"generated_at": "2026-03-19T17:23:37.832859+00:00",
"is_stale": false,
"summary_json": {
"headline": "Consumers are highly negative about theatrical events that misrepresent value and fail to enhance the core streaming subscription.",
"overall_sentiment": "negative",
"context": "…",
"top_themes": ["…"],
"top_actions": ["…"],
"top_risks": ["…"],
"pillar_reception": ["…"]
}
}
}

A small "audience standing" panel in your platform, with the grade and trend per category and the headline, gives editors context before they write: if consumers are already reading price changes badly, the next newsletter about pricing deserves a check.

Production checklist​

  • Keys. One per workspace, server-side, encrypted at rest, rotated when a connecting user leaves. Never in a browser or a mobile build.
  • Timeouts and retries. At least two minutes on scoring calls. Retry on network errors and 5xx; identical requests are served from cache, so retries are cheap. Do not fire the same request twice in parallel.
  • Panel size. Up to fifty personas per synchronous request; switch to the background job for panels beyond a handful, and always for the publication run.
  • Budget. Every fresh reaction is a model call against the workspace's run budget. Debounce live checks, and let editors run a check on demand rather than on every keystroke.
  • What you store. Keep insight_id, grade, status_counts and the document_hash of each check with your revision history; the full output_json if you want to show past judgments offline.
  • Language. Personas answer in the workspace's market language; response_language on each insight tells you which. Do not assume English.
  • Data handling. Drafts sent with skip_save are not added to the workspace and are not used to train anything. See Data flow for what is retained and where.

A minimal end-to-end example​

import os, time, requests

BASE = "https://api.kumkuat.ai"
H = {"X-API-Key": os.environ["KUMKUAT_API_KEY"]}
PANEL = ["8e049a2e-92dd-4077-b6ca-e7154edc7de4", "afd6efb0-90f2-4fae-99a4-4c366bb57897"]

def check(title, text):
"""Pre-publish audience check on an ephemeral draft."""
r = requests.post(f"{BASE}/api/v1/personas/reaction-insights", headers=H, timeout=300, json={
"document": {"title": title, "content": text, "content_type": "newsletter"},
"personas": [{"id": p} for p in PANEL],
"skip_save": True,
})
r.raise_for_status()
return {i["persona"]["name"]: i for i in r.json()["insights"]}

def publish(title, text, url):
"""Store the published piece and queue the panel on it."""
doc = requests.post(f"{BASE}/api/v1/documents", headers=H, timeout=60, json={
"title": title, "content": text, "content_type": "newsletter",
"source_url": url, "media_provenance": "owned",
}).json()["document"]
job = requests.post(f"{BASE}/api/v1/personas/reaction-insights/async", headers=H, timeout=60, json={
"document": {"id": doc["id"]}, "personas": [{"id": p} for p in PANEL],
}).json()
while True:
status = requests.get(f"{BASE}/api/v1/jobs/{job['job_id']}", headers=H, timeout=60).json()
if status["status"] in ("completed", "failed"):
return doc["id"], status
time.sleep(5)

results = check("Tudum Weekly", open("draft.txt").read())
for name, insight in results.items():
print(name, insight["grade"], insight["status_counts"])

if all(i["grade"] in "ABC" for i in results.values()):
doc_id, job = publish("Tudum Weekly", open("draft.txt").read(), "https://example.com/tudum/2026-09-23")
print("published as", doc_id, "panel job", job["status"])