Loopback HTTP API Reference
CivicNewspaper's Rust backend runs an Axum HTTP server bound strictly to 127.0.0.1:12053. It exists so local clients, including the Chromium browser extension and local assistant tools, can pair with the desktop app and exchange data over a controlled local channel.
This file is the route contract. If src-tauri/src/core/server.rs or src-tauri/src/core/auth.rs changes, update this document.
Trust Boundary
The trust boundary is the local user account, not the process. Any process running as the same OS user can reach 127.0.0.1:12053. After pairing, the bearer token is the authorization gate. See SECURITY.md for the full threat model.
Base URL
http://127.0.0.1:12053
The server binds 127.0.0.1 only. It is not intended to be reachable from another machine.
Request Prerequisites
All API routes are Host/Origin-gated.
- Host header: must be exactly
127.0.0.1:12053. Other values return403 Forbidden. - Origin header: if present, must be the bundled Civic Desk Browser Bridge origin
chrome-extension://fobahchhglbihbjfldjlnbbaoagbmjifor an explicitly configured development origin.Origin: null, arbitrary Chrome extensions, and untrusted web origins return403 Forbidden. - No-Origin callers: protected routes may omit
Originonly because they still needAuthorization: Bearer <token>. The pairing route may omitOriginonly when the request includesx-civicnews-pair: 1. - Bearer token: every route except
POST /api/pairrequiresAuthorization: Bearer <token>. Missing, malformed, revoked, or unknown tokens return401 Unauthorized.
POST /api/pair has no bearer-token requirement because it issues the bearer token. It still enforces Host/Origin rules, the explicit no-Origin pairing header, the one-time PIN check, and the rate limit.
Routes
| Method | Path | Auth | Purpose | |---|---|---|---| | POST | /api/pair | Pairing PIN, Host/Origin gate, rate limit | Exchange the one-time pairing token for a bearer token | | GET | /api/queue | Bearer | List all leads and drafts | | GET | /api/evidence/:lead_id | Bearer | List evidence items behind a lead | | POST | /api/drafts | Bearer | Create a new draft, forced to draft_generated | | POST | /api/llm/task | Bearer | Run a one-shot local-LLM prompt | | POST | /api/guardrails/check | Bearer | Run advisory guardrails over a draft |
POST /api/pair
Exchanges the one-time pairing token shown in the desktop app's Browser Pairing tab for a long-lived bearer token. The field is named pin for historical reasons; the value is the 22-character pairing token, not a numeric PIN.
No-Origin local clients must include:
x-civicnews-pair: 1
Host: 127.0.0.1:12053
Browser-extension clients may instead use the bundled Civic Desk Browser Bridge Origin chrome-extension://fobahchhglbihbjfldjlnbbaoagbmjif. Developer builds may allow extra origins only through CIVICNEWS_ALLOWED_EXTENSION_ORIGINS.
Rate limit: 5 failed attempts per client IP per 1800 seconds. A successful pair resets the counter.
Request:
{ "pin": "<22-char pairing token>" }
Response:
{ "token": "<long-lived bearer token>" }
Errors: 401 Unauthorized, 403 Forbidden, 429 Too Many Requests, 500 Internal Server Error.
GET /api/queue
Returns every lead and draft in the local database.
Response:
{
"leads": [
{
"id": 1,
"detector_name": "Money Threshold",
"why": "Why the lead was flagged",
"confidence": "medium",
"risk_level": "low",
"confirmation_checklist": "[]",
"from_scan_lead_id": null,
"story_type": "brief",
"disposition": "ready_to_draft",
"novelty_score": 4,
"novelty_reason": "New meeting agenda item",
"recurrence_count": 0,
"recurrence_note": null,
"created_at": "2026-05-28T12:00:00Z"
}
],
"drafts": [
{
"id": 1,
"lead_id": 1,
"format": "brief",
"title": "Council sets hearing date",
"content": "Draft Markdown",
"status": "draft_generated",
"verification_checklist": "[]",
"missing_evidence_notes": null,
"correction_note": null,
"created_at": "2026-05-28T12:00:00Z",
"updated_at": "2026-05-28T12:00:00Z"
}
]
}
Checklist fields are JSON strings, not nested JSON arrays.
GET /api/evidence/:lead_id
Returns evidence items associated with a lead id.
[
{
"id": 1,
"source_id": 3,
"url": "https://example.gov/minutes/2026-05",
"fetched_at": "2026-05-28T12:00:00Z",
"excerpt": "Relevant excerpt",
"content_hash": "b94d27b9...",
"entities": "[\"City Council\"]"
}
]
entities is a JSON string containing an array of strings.
POST /api/drafts
Creates a new draft. The server forces status to draft_generated; clients cannot create an approved or published draft through this route.
Request:
{
"lead_id": 1,
"format": "brief",
"title": "Draft title",
"content": "Draft Markdown",
"verification_checklist": "[]"
}
Response:
{ "id": 42 }
POST /api/llm/task
Runs a single prompt against the selected local Ollama model, falling back to the default model if no model is selected.
Request:
{
"prompt": "Prompt text",
"system": "System instruction"
}
Response:
{ "result": "Model output" }
Errors: 503 Service Unavailable or 504 Gateway Timeout.
POST /api/guardrails/check
Runs advisory guardrails over an existing draft. Guardrails warn and record concerns; they do not veto the publisher's final decision.
Request:
{ "draft_id": 42 }
Response:
{
"is_clean": false,
"issues": [
{
"category": "Citation Coverage",
"message": "Missing citation",
"severity": "warning",
"paragraph_index": 3
}
]
}
Status Codes
| Code | Meaning | |---|---| | 200 OK | Success | | 401 Unauthorized | Missing, malformed, revoked, unknown bearer token, or wrong pairing token | | 403 Forbidden | Bad Host, untrusted Origin, or no-Origin pair request missing x-civicnews-pair | | 429 Too Many Requests | Pairing rate limit hit | | 503 Service Unavailable | Local Ollama instance could not be reached or the model call failed | | 504 Gateway Timeout | Local model call exceeded the configured timeout | | 500 Internal Server Error | Database or internal failure |