Skip to main content

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​

StepCallKey
Find the reportersGET /api/v1/personas?summary=1&category=…Personal
Understand each oneGET /api/v1/personas/{persona_id}, GET /api/v1/persona-summaries?persona_id=…Personal for the first; any for the second
Ground them in their workPOST /api/v1/personas/{persona_id}/link-file, /discover-linkedin, /link-linkedin, /quick-linkPersonal, member or above
Ask for the anglePOST /api/v1/personas/interactAny
Draft the pitchPOST /api/v1/content-creation/generateAny
Test the pitchPOST /api/v1/personas/reaction-insightsAny
Ask a follow-upPOST /api/v1/personas/interactAny

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_resolvedentity_kind_labelStands forPitch it?
personIndividualA named reporter, columnist or editorYes: one pitch per person
organizationOrganizationAn outlet or deskYes: the desk's view, before choosing a reporter
archetypeSegmentA 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:

FieldWhat it tells you
descriptionWho they are and what they cover
interests, concernsWhat they look for in a story, and what makes them sceptical
communication_style, toneHow 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.

In the app

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.

CallWhat it doesFeeds the persona's answers
POST /api/v1/personas/{persona_id}/link-fileUpload 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-linkedinSearch 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-linkedinLink a LinkedIn profile, {"linkedin_url": "…"}. Its posts are synced daily in the background.Yes
POST /api/v1/personas/{persona_id}/quick-linkRecord 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:

  • grade as a send/revise gate. A pitch the reporter persona grades C or below is worth another pass.
  • key_judgments with status bad or warn: 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.
  • comms on 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.