Build a media pitching workflow
A pitch works when it reaches the right reporter with an angle that fits what they cover. Kumkuat can model the journalists and outlets that matter to you as personas, grounded in what they have written and said. This guide shows how to put those personas to work in a pitching tool: find the reporters, understand them, draft a pitch for each one, and test it against the reporter before anyone hits send.
There is no single "pitch" endpoint. The workflow combines the persona, content creation and scoring calls, and this guide shows where each one fits. It assumes you have a key and know how to list personas from the Quickstart, and have read Score a draft against your audiences.
The workflow
| Step | Call | Key |
|---|---|---|
| Find the reporters | GET /api/v1/personas?summary=1&category=… | Personal |
| Understand each one | GET /api/v1/personas/{persona_id}, GET /api/v1/persona-summaries?persona_id=… | Personal for the first; any for the second |
| Ground them in their work | POST /api/v1/personas/{persona_id}/link-file, /discover-linkedin, /link-linkedin, /quick-link | Personal, member or above |
| Ask for the angle | POST /api/v1/personas/interact | Any |
| Draft the pitch | POST /api/v1/content-creation/generate | Any |
| Test the pitch | POST /api/v1/personas/reaction-insights | Any |
| Ask a follow-up | POST /api/v1/personas/interact | Any |
Kumkuat does not send email. The pitch leaves from your own outreach tool; Kumkuat's part ends when the pitch is ready.
1. Find the reporters
Journalists are ordinary personas. Most workspaces file them under a category such as Journalists or Media; check the category list from GET /api/v1/audience-groups for the names your workspace uses. Then list the category:
curl "https://api.kumkuat.ai/api/v1/personas?summary=1&category=Journalists" \
-H "X-API-Key: $KUMKUAT_PERSONAL_KEY"
The category filter is an exact, case-sensitive match. There is no text search parameter; filter the list on your side.
Each persona says what kind of entity it stands for. For pitching, this is the field that separates a named reporter from a segment like "Trade press":
entity_kind_resolved | entity_kind_label | Stands for | Pitch it? |
|---|---|---|---|
person | Individual | A named reporter, columnist or editor | Yes: one pitch per person |
organization | Organization | An outlet or desk | Yes: the desk's view, before choosing a reporter |
archetype | Segment | A type of journalist, such as "Energy trade reporters" | Use it to test an angle, not to send to |
reporters = [p for p in personas
if p["entity_kind_resolved"] in ("person", "organization")
and p["include_in_audience"] and p["has_template"]]
If a reporter you need is missing, create a persona for them; see Build and manage an audience library. Name individuals with their outlet in parentheses, such as "Dana Okafor (Grid & Power Weekly)", so they are recognised as people rather than segments.
2. Understand each reporter
GET /api/v1/personas/{persona_id} returns the full persona. The fields that matter for a pitch:
| Field | What it tells you |
|---|---|
description | Who they are and what they cover |
interests, concerns | What they look for in a story, and what makes them sceptical |
communication_style, tone | How they like to be approached |
grounded_statements[] | Things this person has said or written, each with text, title, source_url and date, newest first |
social_links[], external_sources[] | Where their work lives |
linked_files[] | Articles and clips attached to the persona |
interests and concerns are only in the full object, not in the summary=1 listing.
How the reporter has reacted to your own content so far is in the persona summary. It is built from the reactions already recorded for that persona, read from cache and refreshed in the background:
curl "https://api.kumkuat.ai/api/v1/persona-summaries?persona_id=5c1e9f0a-3b7d-4e2a-9c61-2f8d4a7b0e13&days=90" \
-H "X-API-Key: $KUMKUAT_API_KEY"
{
"cache_hit": true,
"summary": {
"grade": "C",
"grade_trend": "up",
"reaction_count": 14,
"status_counts": { "good": 22, "warn": 31, "bad": 9 },
"summary_json": {
"headline": "Engages with grid-reliability data but discounts announcements that lack independent numbers.",
"overall_sentiment": "mixed",
"top_themes": [{ "theme": "Reliability metrics", "status": "good", "detail": "…" }],
"top_actions": [{ "title": "Lead with third-party data", "detail": "…", "severity": "high" }],
"top_risks": ["…"]
}
}
}
A persona with no recorded reactions returns {"summary": null, "cache_hit": false}. Values here are illustrative.
For named people and outlets, the persona's profile page in Kumkuat can also show a stance gauge and a How to pitch them playbook, where your workspace has persona intelligence switched on. Those sections are not yet part of the public API.
3. Ground them in their work
A reporter persona is only as good as what it knows about the reporter. Attach their recent articles so that its answers rest on their actual coverage.
| Call | What it does | Feeds the persona's answers |
|---|---|---|
POST /api/v1/personas/{persona_id}/link-file | Upload an article, clip or transcript (multipart field file: PDF, Word, PowerPoint, HTML, text or Markdown). Ingested at once, enriched in the background. | Yes |
POST /api/v1/personas/{persona_id}/discover-linkedin | Search for the person's LinkedIn profile by name. Returns candidates[] with url, name, headline and match. Changes nothing. | No |
POST /api/v1/personas/{persona_id}/link-linkedin | Link a LinkedIn profile, {"linkedin_url": "…"}. Its posts are synced daily in the background. | Yes |
POST /api/v1/personas/{persona_id}/quick-link | Record a link to the reporter's author page or social profile, {"url": "…"}. Stored for reference; not crawled. | No |
curl -X POST https://api.kumkuat.ai/api/v1/personas/5c1e9f0a-3b7d-4e2a-9c61-2f8d4a7b0e13/link-file \
-H "X-API-Key: $KUMKUAT_PERSONAL_KEY" \
-F "file=@okafor-grid-capacity-feature.pdf"
{
"message": "File linked to persona",
"document_id": "e7a2c0d4-…",
"knowledgebase_id": "3b9f6e21-…",
"linked_file": { "document_id": "e7a2c0d4-…", "name": "okafor-grid-capacity-feature.pdf", "uploaded_at": "2026-10-02T15:11:40+00:00" },
"linked_files": ["…"]
}
These calls change the persona, so they need a personal key from a member, admin or owner (persona.write, and documents.uploads for files). Three to five recent pieces per reporter is a good start; refresh them as the reporter publishes.
4. Ask for the angle
Before drafting, ask the reporter persona what would make the story worth their time. A conversation call answers in the reporter's voice in a few seconds:
curl -X POST https://api.kumkuat.ai/api/v1/personas/interact \
-H "X-API-Key: $KUMKUAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"persona_id": "5c1e9f0a-3b7d-4e2a-9c61-2f8d4a7b0e13",
"interaction_type": "conversation",
"input_content": "We are announcing a 400 MW battery storage project with a regional utility, online in 2028. What would make this a story for you rather than a press release? What would you need from us, and what would make you pass?",
"ground_with_google_search": true
}'
{
"message": "Persona interaction completed",
"persona_response": "Another storage announcement doesn't get me out of bed. What would: how many hours of discharge, what it displaces on the peak, and whether the utility will put its own reliability numbers on the record …",
"response_language": "en"
}
Values are illustrative. ground_with_google_search: true lets the persona bring in recent public context, such as what the outlet has covered lately. Ask the same question of the outlet persona, if you have one, to see whether the desk would take the story at all.
If the Why this matters feature is switched on for your workspace, POST /api/v1/personas/why-it-matters returns a short, web-sourced explanation of why the topic matters to a given persona, with sources[]. It is a quick way to find the stake a reporter's readers have in your news. Where the feature is off, the call returns 403.
5. Draft a pitch for each reporter
POST /api/v1/content-creation/generate writes the draft. Use content_type: "email", which asks for a subject line, a greeting, short paragraphs and a specific call to action. There is no dedicated pitch type.
The generator does not look up the reporter by itself. Put what you learned in steps 2 to 4 into prompt:
def draft_pitch(reporter, angle_notes, news):
prompt = (
f"Write a media pitch email to {reporter['name']}.\n"
f"They cover: {reporter['description']}\n"
f"They care about: {reporter.get('interests', '')}\n"
f"They are sceptical of: {reporter.get('concerns', '')}\n"
f"What they told us would make it a story: {angle_notes}\n\n"
f"The news: {news}\n\n"
"Under 150 words. Lead with the angle that fits their beat, not with our company. "
"Offer one concrete thing they can use: data, access or an interview. No hype words."
)
resp = requests.post(f"{BASE}/api/v1/content-creation/generate", headers=HEADERS, timeout=180, json={
"prompt": prompt,
"content_type": "email",
"content_type_label": "Media pitch",
"source_mode": "freeform",
"audiences": [reporter["name"]],
})
resp.raise_for_status()
return resp.json()["content"]
{
"content": "**Subject:** 400 MW, four hours, and the utility's own peak numbers\n\nHi Dana,\n\nYour piece on …",
"model": "gemini/gemini-2.5-flash",
"truncated": false
}
To ground the pitch in the announcement itself, set source_mode to "communication" and pass the press release's stored document id as source_document_id. To write in a spokesperson's voice, see Build a message creation flow. Every option of this call is covered there.
6. Test the pitch against the reporter
Score each pitch against its reporter, and against the outlet persona if you have one, with the same call a scorecard uses. Send the pitch inline with skip_save: true so drafts stay out of the workspace library:
{
"document": {
"title": "Pitch: 400 MW storage project (Dana Okafor)",
"content_type": "pitch",
"content": "Subject: 400 MW, four hours, and the utility's own peak numbers\n\nHi Dana, …"
},
"personas": [
{ "id": "5c1e9f0a-3b7d-4e2a-9c61-2f8d4a7b0e13" },
{ "id": "9a40d2b7-61e5-4c8f-b3a2-7d0e5f1c6b98" }
],
"skip_save": true
}
Post it to POST /api/v1/personas/reaction-insights. What to read for a pitch:
gradeas a send/revise gate. A pitch the reporter persona grades C or below is worth another pass.key_judgmentswith statusbadorwarn: these are the reasons the reporter would pass.evidence_and_signals.items[].quote: the exact line that triggered each judgment. Highlight it in your editor.commson each judgment: the persona's own rewrite of that line.counterpoints_and_risks: the questions the reporter would come back with. Have the answers ready before you send.
content_type is free text and is passed to the persona as the type of document it is reading; pitch tells it to read the text as a pitch, not as a press release.
Because each pitch is written for one reporter, test it against that reporter. Testing a pitch written for Dana against a different reporter tells you little.
7. Refine and follow up
Revise the pitch with the judgments in hand and score it again; an unchanged pitch returns the cached result instantly, and any edit produces a fresh read. To probe a judgment before rewriting, ask the reporter directly:
{
"persona_id": "5c1e9f0a-3b7d-4e2a-9c61-2f8d4a7b0e13",
"interaction_type": "conversation",
"input_content": "Here is our pitch:\n\n… pitch text …\n\nYou said the second paragraph reads like marketing. Which phrase, and what would you rather see there?"
}
The conversation call is stateless; include the pitch and any earlier exchange in input_content. See Build an audience reaction experience for a chat loop that keeps the history for you.
Putting it together
import os, requests
BASE = "https://api.kumkuat.ai"
HEADERS = {"X-API-Key": os.environ["KUMKUAT_API_KEY"]} # workspace key
PERSONAL = {"X-API-Key": os.environ["KUMKUAT_PERSONAL_KEY"]} # personal key, for persona reads
def get(path, headers=HEADERS, **params):
r = requests.get(f"{BASE}{path}", headers=headers, params=params, timeout=60)
r.raise_for_status()
return r.json()
def post(path, body, timeout=180):
r = requests.post(f"{BASE}{path}", headers=HEADERS, json=body, timeout=timeout)
r.raise_for_status()
return r.json()
NEWS = open("announcement.txt").read()
listing = get("/api/v1/personas", PERSONAL, summary=1, category="Journalists")["personas"]
reporters = [p for p in listing if p["entity_kind_resolved"] == "person" and p["has_template"]]
for summary in reporters[:5]:
reporter = get(f"/api/v1/personas/{summary['id']}", PERSONAL)["persona"]
angle = post("/api/v1/personas/interact", {
"persona_id": reporter["id"], "interaction_type": "conversation",
"input_content": f"Here is our news:\n\n{NEWS}\n\nWhat would make this a story for you, and what would make you pass?",
"ground_with_google_search": True,
}, timeout=120)["persona_response"]
pitch = draft_pitch(reporter, angle, NEWS) # from step 5
result = post("/api/v1/personas/reaction-insights", {
"document": {"title": f"Pitch for {reporter['name']}", "content_type": "pitch", "content": pitch},
"personas": [{"id": reporter["id"]}],
"skip_save": True,
}, timeout=300)
insight = result["insights"][0]
blockers = [j["judgment"] for j in insight["output_json"]["key_judgments"] if j["status"] == "bad"]
print(f"{reporter['name']}: grade {insight['grade']}")
for b in blockers:
print(" would pass because:", b)
Good practice
- One reporter, one pitch, one test. The value is in the tailoring. Keep the reporter's id with the pitch so the test and any follow-up go to the same persona.
- Keep reporter personas current. Attach new articles as they are published; a persona grounded in last year's coverage pitches last year's beat.
- Use segments to choose the angle, individuals to tailor it. Test two or three angles against an archetype such as "Energy trade reporters" first, then tailor the winner per reporter.
- Keep it a rehearsal. A persona is a model of a reporter built from their public work, not the reporter. Use it to sharpen the pitch, never as a stand-in for the relationship or as a quote.
- Drafts stay ephemeral. Score pitches with
skip_save: true. Store the press release itself as a document when it goes out, so its reception shows on the Audience Dashboard.