DOCUMENTATION

REST API documentation

Use the REST API to search clustered news stories, inspect their sources, and build persistent channels.

This page documents the public API serving at https://api.hivewire.ai. Unlisted routes are not part of the public API.

Quick start

REST MAKE AN API CALL

1. Create a key in the dashboard and store it in your environment:

export HIVEWIRE_API_KEY="hw_live_..."

2. Fetch five stories ranked by significance:

curl "https://api.hivewire.ai/v1/stories?sort=top&limit=5" \
  -H "Authorization: Bearer $HIVEWIRE_API_KEY"

Each story is one event with a title, summary, significance score, and cited coverage from every monitored outlet reporting it.

Authentication

Every request carries a bearer token in the Authorization header:

Authorization: Bearer hw_live_...

Create a key in your dashboard. The full key is shown once, at creation, and stored only as a hash, so we cannot recover it for you: if you lose it, revoke it and make a new one. Your account holds one active key at a time. A missing, malformed, unknown or revoked key returns 401.

Core concepts

Story. A cluster of articles about the same event, collapsed into one record with a title, a summary, a significance score (0–10), matched topics, and publishers with their source articles nested beneath them. Nested articles carry an id, headline, URL, publication time, and optional focus tag. We never return full article text, by design. You get a summary plus citations, which is what a feed or an LLM actually needs, and the reporting stays with the outlets that wrote it.

Clustering. Articles are matched to stories in two stages. Embedding similarity produces a shortlist, then a language model resolves ambiguous cases with one of three verdicts: same event, related-but-distinct, or unrelated. This keeps "Fed cuts rates" and "markets rally after the cut" linked but separate. A continuous merge pass revisits early decisions and folds duplicate stories together as later coverage makes the connection clear. A story_id can therefore disappear into another story (fetching it returns 404; see Objects). The longer explanation is in how clustering works.

Categories. Every story is one of news, feature, analysis, opinion, or podcast. The categories have different clustering rights. Commentary that matches a news event joins that story as one of its cited articles. Commentary that matches nothing becomes its own story and never attracts other articles or acts as a merge target. Only news stories cluster. That's why category=news gives you events, while category=opinion,analysis gives you the discourse about them.

Topics. Stories are tagged against a curated topic set. Search it with /v1/topics/search, see what is spiking with /v1/topics/trending, or turn a sentence into a weighted profile with /v1/topics/profile.

Weights. Topic interest is expressed as one of four levels, not a number: focus, more, less, avoid. Those are what /v1/topics/profile returns and what a channel stores. Omit a weight when creating a channel and it defaults to more.

Channels. A persistent feed profile stored on our side: topic levels, filters, and a learned affinity model built from your like/dislike feedback. Create one, poll its stories, teach it. Channels are the way to run a long-lived personalized feed.

Stories & articles

GET

/v1/stories — The story list, cursor-paginated.

PARAMDEFAULTNOTES
sortrecencyrecency or top (significance adjusted for corroboration and freshness)
limit25Clamped to 1–100
cursorOpaque, from next_cursor
from / tolast 24hISO 8601. The 24h default applies only when neither is given
min_significance3.0Lower it to see more
categoryCSV of news, feature, analysis, opinion, podcast
sources / exclude_sourcesCSV of publisher_id
geographyCSV of place names
curl "https://api.hivewire.ai/v1/stories?sort=top&limit=2" \
  -H "Authorization: Bearer $HIVEWIRE_API_KEY"

{
  "stories": [
    {
      "story_id": "5bc464718c645827",
      "title": "Iran mocks Rubio as Trump's 'little lapdog' in Lego video",
      "summary": "Iran trolled US Secretary of State Marco Rubio with an AI-generated Lego video...",
      "category": "news",
      "significance": 4.0,
      "first_seen": "2026-07-24T16:05:55.695170+00:00",
      "reported_at": "2026-07-24T15:59:56.784400+00:00",
      "article_count": 1,
      "publishers": [{"publisher_id": "the_independent", "name": "The Independent"}],
      "geography": [],
      "topics": []
    }
  ],
  "next_cursor": "eyJzIjoicmVjZW5jeSIsImsiOiIyMDI2...",
  "count": 2
}
GET

/v1/stories/{story_id} — One story with each publisher's articles nested under publishers[].articles. 404 if the id is unknown or the story has since been merged.

GET

/v1/stories/{story_id}/related — Related stories from two sources, in order: relation: "storyline" (stories our clustering pipeline linked as the same unfolding story, or the "how did this develop" edges) then relation: "semantic" (nearest stories by embedding, each with a similarity from 0–1). Takes limit (default 10, max 100), from and to. The seed story and semantic results below a similarity floor are excluded. A short or empty list means nothing genuinely related exists in that window; the endpoint does not fill the page with unrelated stories. 2 credits

POST

/v1/stories/batch — Hydrate up to 100 ids at once: {"story_ids": […]}. Returns {"stories": […], "missing": […]} in request order.

GET

/v1/articles/{article_id} — One source article's metadata. POST /v1/articles/batch takes {"article_ids": […]}, max 100.

GET

/v1/sources — Every publication we cluster: {"sources": [{"publisher_id", "name", "domain"}], "count"}. Optional limit; the full catalog is returned by default. Browse it at hivewire.ai/sources.

GET

/v1/stats — Corpus volume and freshness: stories filed, articles published and distinct sources_reporting over the trailing window (hours, default 24, max 168), plus sources_total, freshest_story and coverage_start. Useful for monitoring and for agents deciding whether the corpus covers their question. Results are cached server-side for a few minutes.

POST

/v1/search — Semantic + keyword search across stories. Note it is a POST with a JSON body, not a query string. 2 credits

FIELDDEFAULTNOTES
queryrequiredSupports quoted phrases and (A OR B)
modesemanticsemantic, keyword
sortrelevancerelevance, recency, top
limit25Clamped to 1–100
filters{}from, to, category, min_significance, sources, exclude_sources, geography
exclude_keywords[]Array of strings
include["related_topics"] for adjacent topics. Source articles are always nested under publishers[].articles.
curl -X POST https://api.hivewire.ai/v1/search \
  -H "Authorization: Bearer $HIVEWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "offshore wind permitting", "limit": 5}'

{
  "stories": [
    {
      "story_id": "c588b0be2566d9a8",
      "title": "Sunda Energy applies for NZ offshore exploration permit",
      "summary": "Sunda Energy applied for a petroleum exploration permit...",
      "significance": 3.0,
      "relevance": 0.8721,
      "matched_keywords": ["offshore", "permit"],
      "publishers": [{"publisher_id": "energy_voice", "name": "Energy Voice"}]
    }
  ],
  "next_cursor": null,
  "count": 5
}

Search results carry two extra keys on each story: relevance (0–1) and matched_keywords. The envelope's strong_matches counts how many rows directly answer the query. Treat rows past that count as context, not answers. In semantic mode it counts rows clearing a calibrated similarity bar. relevance is normalized against the page's best hit, so it can read high even when nothing matches well; strong_matches: 0 signals that a page contains only near-neighbors. In keyword mode it counts every keyword-matched row plus any semantic fill rows clearing the same bar. Fill rows are tagged match: "semantic", and the envelope's recall says when matching was relaxed (any-term) or fell through to semantic fill. Pass "include": ["related_topics"] to also get related_topics: the topics of the returned stories ranked by how many rows carry each one ([{"name", "story_count"}]). These are useful for pivoting or feeding channel creation. In semantic mode every sort paginates to a depth of 500 results, after which next_cursor stops being issued because results come from a bounded candidate pool. In keyword mode the 500-result ceiling applies to relevance only, while recency and top paginate without it.

POST

/v1/articles/search — The same, over individual articles rather than clustered stories. sort accepts relevance or recency, and only the from, to, sources and exclude_sources filters apply. 2 credits

Topics

GET

/v1/topics/search?query= — Find topics by meaning. The parameter is query. limit defaults to 20, max 50; min_similarity (0–1, default 0.5) sets the confidence floor. Genuine matches typically score 0.7+, while lookalikes land around 0.4. An empty topics list means nothing in the catalog confidently matches. That is a real answer, not an error. 2 credits

curl "https://api.hivewire.ai/v1/topics/search?query=energy" \
  -H "Authorization: Bearer $HIVEWIRE_API_KEY"

{"topics": [
  {"topic_id": "energy_storage",    "name": "Energy Storage",    "similarity": 0.9598},
  {"topic_id": "energy_transition", "name": "Energy Transition", "similarity": 0.9598}
], "count": 2}
GET

/v1/topics/trending — What is spiking, by today-vs-baseline lift: today's per-day story rate over the trailing 7-day baseline rate (baseline floored at 0.5/day, so brand-new topics cap at 2× today's rate). Topics need at least 3 stories in the window to qualify. Takes hours (default 24, max 168), limit (default 20), category (default news).

{"topics": [
  {"name": "Rare Earth Elements", "story_count_window": 3, "baseline_daily": 0.57, "lift": 5.25}
], "count": 1}

story_count_window spans the requested hours window (story_count_today is a deprecated alias with the same value). lift is smoothed rather than a raw ratio. A topic with baseline_daily: 0 reports a large finite lift instead of infinity, so rank by it rather than setting a threshold.

POST

/v1/topics/profile — Free text in, weighted topic profile out. The bridge from human intent to a feed. LLM-backed. 50 credits

curl -X POST https://api.hivewire.ai/v1/topics/profile \
  -H "Authorization: Bearer $HIVEWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "chip export controls and fusion energy"}'

{"profile": {"topics": [
  {"name": "Fusion Energy",           "weight": "focus"},
  {"name": "Semiconductors & Chips",  "weight": "focus"},
  {"name": "Export Controls",         "weight": "more"},
  {"name": "Trade & Tariffs",         "weight": "more"}
]}}

A query we cannot map returns {"profile": {"topics": []}} with a 200. Text over 10,000 characters, or a query that is not about news interests, returns 400.

Channels

Where a search forgets you between requests, a channel remembers. Feed a profile straight into one, then poll it.

POST

/v1/channels — Create from explicit topics or from a free-text query. 50 credits

FIELDDEFAULTNOTES
topics[{"name", "weight"}]; weight is focus, more, less or avoid
queryFree text, run through the profile pipeline. Give topics or query
namenullFree text
turnoverdailybreaking, frequent, daily, relaxed
filters{}Same filter object as search
curl -X POST https://api.hivewire.ai/v1/channels \
  -H "Authorization: Bearer $HIVEWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Chips and export controls",
       "topics": [{"name": "Semiconductors & Chips", "weight": "focus"},
                  {"name": "Export Controls", "weight": "more"}],
       "turnover": "daily"}'

{"channel": {
  "channel_id": "ch_9f2a1b3c4d5e",
  "name": "Chips and export controls",
  "topics": [{"name": "Semiconductors & Chips", "weight": "focus"},
             {"name": "Export Controls", "weight": "more"}],
  "turnover": "daily",
  "filters": {},
  "created_at": "2026-07-24T14:00:00.123456Z",
  "updated_at": "2026-07-24T14:00:00.123456Z"
}}
GET

/v1/channels/{channel_id}/stories — The channel's ranked feed. Takes tier (primary or all), limit (default 25, max 200), and since (ISO 8601). Each story carries tier and is_new, and the response includes {"channel": {"channel_id", "last_refreshed"}}. Poll with since set to your last poll to get only what is new.

POST

/v1/channels/{channel_id}/feedback{"story_id": "...", "rating": "like" | "dislike" | "clear"}. Teaches the channel's affinity model. Returns {"ok": true}. A story_id that doesn't exist returns 404 (the same ids GET /v1/stories/{id} serves); the story doesn't need to be in the channel's feed — rating anything real is valid signal. clear always succeeds, even for a story that has since merged or vanished.

GET PATCH DELETE

/v1/channels/{channel_id} — Inspect, adjust or remove. PATCH touches only the keys you send. GET /v1/channels lists yours. Deleting returns {"deleted": "ch_..."}.

Channels are scoped to the key's owner: another account's channel id returns 404, not 403, so ids cannot be probed.

Object reference

Story. story_id, title, summary, category, significance (0–10, one decimal), first_seen, reported_at, article_count, publishers ([{publisher_id, name, articles}]), geography (strings), topics (topic names as strings, without weights). Each nested article has article_id, headline, url, published_at, and optional focus. Endpoints add relevance and matched_keywords on search, similarity on related, and tier and is_new on channel feeds.

IDs. Stable 16-character hex tokens. Story and article ids share a namespace by design: a story's id is the id of the article that originated it, so a single-source story and its article carry the same id. An article's story_id is null when no active story lists it (it was dropped as an exact-title duplicate, or its story was merged away).

Article. article_id, headline, publisher, domain, url, author, published_at, story_id. Metadata and a link, never body text. One asymmetry worth coding against: a story has a title, an article has a headline.

Channel. channel_id, name, topics ([{name, weight}]), turnover, filters, created_at, updated_at.

Requests & pagination

JSON over HTTPS, UTF-8, timestamps in ISO 8601 UTC. /v1/stories, /v1/search and /v1/articles/search paginate with an opaque cursor: pass back the next_cursor you were given, and stop when it comes back null. There is no offset or page parameter. A cursor encodes the sort and a fingerprint of the query, so reusing one against a different query returns 400 rather than silently wrong results. Every other endpoint returns a single page. limit is clamped to its maximum rather than rejected.

Errors

Errors return a flat JSON body: {"error": "<message>"}. The message is meant to be read.

STATUSMEANING
400Malformed request: a missing required field, an out-of-enum value, or a cursor that does not match the query.
401Missing, malformed, unknown or revoked API key.
404No such story, article or channel, including channels owned by someone else.
429Rate limit or credit allowance exhausted. The message identifies the ceiling; retry after the rate window, reset, or an upgrade.
502The LLM step failed on /v1/topics/profile or channel creation.
503Semantic search or the database is briefly unavailable. Safe to retry with backoff.
500Our fault. Retry with backoff; check status.

Responses do not carry X-RateLimit-* headers. Rate-limit denials include Retry-After; quota denials do not. Track consumption in your dashboard, which reports the live counter broken down by day and by endpoint.

Rate limits, quotas & request weights

Two ceilings apply: an account-wide rate limit and your plan's monthly credits. Both are shared across everything your account does. Your API key and any OAuth/MCP sessions draw from the same bucket. The Free plan allows 1 request per second with a burst of 3; Developer allows 10 per second with a burst of 20; Professional allows 25 per second with a burst of 50. Exceeding the rate limit returns 429.

Resets. Credits count per calendar month and reset on the 1st at 00:00 UTC, whatever date your billing renews on; unused credits don't roll over. Exhausting them returns 429 until the reset. There is no overage billing, and upgrading raises the ceiling on your key within a minute.

Request weights. Calls cost credits according to what they do. Reads and channel management cost one, search and embedding routes two, and LLM-backed routes fifty. On the free plan the LLM routes draw on a separate pool of 20 a month rather than your credits, so exhausting them never costs you your reads.

CALLCOSTS
Everything not listed below1 credit
POST /v1/search, POST /v1/articles/search, GET /v1/topics/search, GET /v1/stories/{story_id}/related2 credits
POST /v1/topics/profile, POST /v1/channels50 credits

Channel creation is charged as an LLM call whether or not you pass explicit topics, because the price is set by the route and we cannot see your body before authorizing. Quotas reset at the start of each calendar month, and the free plan also has a daily ceiling. See pricing for the per-plan numbers.

Versioning & deprecation

The API is versioned in the path (/v1/). Additive changes (new fields, new endpoints) ship without notice, so build your parsers to ignore unknown fields. Breaking changes to stable endpoints get at least 30 days' notice on the changelog.

Channel delivery. Channels use polling, not webhooks. Poll /v1/channels/{channel_id}/stories with since to fetch new stories.

Attribution. Free-tier applications must display "News via Hivewire" with a link wherever Service data is shown. Paid plans have no attribution requirement, though we never mind.