Programmatic access to stories, feeds, and the headline debrief tool. All endpoints return JSON and are served at http://localhost:8001.
The POST /api/debrief endpoint requires an API key. Pass it via the X-API-Key header or the api_key field in the request body:
# Option A — X-API-Key header
curl -X POST http://localhost:8001/api/debrief \
-H "Content-Type: application/json" \
-H "X-API-Key: pd_your_key_here" \
-d '{"headline": "You Won't BELIEVE What Happened!"}'
# Option B — api_key in JSON body
curl -X POST http://localhost:8001/api/debrief \
-H "Content-Type: application/json" \
-d '{"headline": "You Won't BELIEVE What Happened!", "api_key": "pd_your_key_here"}'pd_aBcDeFg...) is shown.The debrief endpoint requires an API key. All other read endpoints are public.
| Auth Type | Method | Used By | Rate Limited |
|---|---|---|---|
| API Key | X-API-Key: pd_...or body: {"api_key":"pd_..."} | POST /api/debrief | 200 req/min |
| None | — | Stories, Feeds, Health | No |
429 Too Many Requests. The window slides continuously — no need to wait a full minute for it to reset./api/debrief/promptGet the default system prompt used for headline analysis. Useful for reference before overriding with a custom prompt.
Response (200)
{
"system_prompt": "You are a media literacy analyst. Identify every manipulative tactic used, could be a varying number. Generate exactly 5 alternative neutral headlines (different factual angles, 5-15 words each). Output JSON: {\"variations\": [...], \"clickbait_tactics_found\": [...]}"
}cURL example
curl "http://localhost:8001/api/debrief/prompt"
/api/debriefSubmit a clickbait headline and receive neutralized, factual alternatives plus an analysis of the manipulation tactics used.
X-API-Key header or the api_key field in the JSON body. Rate limited to 200 req/min per key. Header takes priority if both are provided.Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| headline | string | Yes | The clickbait headline to analyze (5-500 characters) |
| system_prompt | string | No | Custom system prompt (max 5000 chars). Overrides default; bypasses cache. |
| num_variations | int | No | Number of neutral variations to generate, 1-5 (default: 5) |
| api_key | string | No | API key (alternative to X-API-Key header). Useful when headers can't be set. |
Request body (JSON)
{
"headline": "You Won't BELIEVE What This Senator Just Did!",
"system_prompt": "(optional) Custom system prompt override",
"num_variations": 5,
"api_key": "(optional) pd_your_key_here — alternative to X-API-Key header"
}Response (200)
{
"original": "You Won't BELIEVE What This Senator Just Did!",
"variations": [
"Senator [Name] Introduces New Policy on [Topic]",
"Senate Committee Reviews Proposed Legislation",
"Senator Takes Action on [Policy Area]",
"New Senate Proposal Addresses [Issue]",
"Senator Announces Legislative Initiative"
],
"analysis": {
"clickbait_tactics_found": [
"Curiosity gap / vague cliffhanger",
"ALL CAPS for emotional emphasis",
"Second-person address to create urgency"
]
}
}cURL examples
# With X-API-Key header
curl -X POST http://localhost:8001/api/debrief \
-H "Content-Type: application/json" \
-H "X-API-Key: pd_your_key_here" \
-d '{"headline": "You Won't BELIEVE What This Senator Just Did!"}'
# With api_key in request body (no header needed)
curl -X POST http://localhost:8001/api/debrief \
-H "Content-Type: application/json" \
-d '{"headline": "You Won't BELIEVE What This Senator Just Did!", "api_key": "pd_your_key_here"}'Error responses
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, or revoked API key |
| 422 | Invalid request body (headline too short/long, etc.) |
| 429 | Rate limit exceeded (200 req/min) |
| 502 | AI service unavailable — try again |
/api/storiesGet paginated story groups with nested articles. Each story group clusters related articles from multiple sources.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| page | int | No | Page number (default: 1) |
| per_page | int | No | Items per page, 1-100 (default: 30) |
| sort | string | No | "latest" (by date) or "sources" (by article count) |
| search | string | No | Full-text search across headlines and summaries (max 200 chars) |
| source | string | No | Filter by single source slug (e.g. bbc) |
| sources | string | No | Comma-separated source slugs (e.g. bbc,nyt,fox) |
| title_format | string | No | "both" (default), "neutral" (only AI-neutralized titles), or "original" (only source titles with bias) |
Response (200)
{
"stories": [
{
"id": 42,
"canonical_title": "Senate Passes Infrastructure Bill",
"canonical_summary": "The Senate voted 68-29 to pass...",
"article_count": 5,
"latest_published_at": "2025-01-15T14:30:00Z",
"articles": [
{
"id": 101,
"source": { "name": "BBC News", "slug": "bbc", "bias_label": "center" },
"original_title": "US Senate approves major infrastructure package",
"neutral_title": "Senate Passes Infrastructure Spending Bill",
"original_summary": "The US Senate has approved...",
"link": "https://bbc.com/news/...",
"published_at": "2025-01-15T14:30:00Z",
"image_url": "https://..."
}
]
}
],
"total": 150,
"page": 1,
"per_page": 30,
"has_more": true
}cURL examples
curl "http://localhost:8001/api/stories?page=1&per_page=10&sort=latest" # Search curl "http://localhost:8001/api/stories?search=infrastructure" # Filter by sources curl "http://localhost:8001/api/stories?sources=bbc,nyt,fox" # Only neutralized titles (hides original biased titles) curl "http://localhost:8001/api/stories?title_format=neutral" # Only original titles (shows source bias) curl "http://localhost:8001/api/stories?title_format=original"
/api/stories/{story_group_id}Get a single story group with all its articles. Supports the same title_format parameter.
cURL example
curl "http://localhost:8001/api/stories/42" # With original titles only curl "http://localhost:8001/api/stories/42?title_format=original"
/api/feedsList all configured RSS feed sources with their name, slug, URL, bias label, and category.
Response (200)
{
"feeds": [
{
"id": 1,
"name": "BBC News",
"slug": "bbc",
"rss_url": "https://feeds.bbci.co.uk/news/world/rss.xml",
"bias_label": "center",
"category": "original",
"is_active": true
}
]
}cURL example
curl "http://localhost:8001/api/feeds"
/api/feeds/refreshTrigger a background RSS feed ingestion. Returns immediately — articles are fetched, grouped, and neutralized asynchronously.
Response (202)
{
"status": "ingestion_started",
"message": "RSS feed refresh initiated"
}cURL example
curl -X POST "http://localhost:8001/api/feeds/refresh"
/api/healthCheck backend status, database connectivity, article/story counts, and last ingestion time. Used by Docker healthchecks.
Response (200)
{
"status": "healthy",
"db": "connected",
"last_ingestion": "2025-01-15T14:00:00Z",
"article_count": 2847,
"story_group_count": 483,
"timestamp": "2025-01-15T14:30:00Z"
}cURL example
curl "http://localhost:8001/api/health"
All errors return JSON with a detail field describing what went wrong.
| Code | Meaning |
|---|---|
| 200 | Success |
| 202 | Accepted — feed refresh queued in background |
| 400 | Bad request — validation failed |
| 401 | Missing, invalid, or revoked API key |
| 404 | Resource not found |
| 422 | Unprocessable entity — invalid field values |
| 429 | Rate limit exceeded — 200 requests/min per API key |
| 502 | Backend or AI service unavailable |
| 504 | Request timed out |
Example error response
{
"detail": "Rate limit exceeded (200 requests/min). Try again shortly."
}