# CLSTR API

> API reference for CLSTR. HTML version: https://clstr.news/developers

Structured "what is happening": ~100k articles/day ingested, then grouped into multi-source events and into situations with memory. 30 to 90 minutes behind the wires by design. Multi-source shape, not breaking-news speed.

- Base URL: `https://api.clstr.news/v1`
- MCP endpoint: `https://mcp.clstr.news`
- Auth: `Authorization: Bearer clstr_...`
- Format: JSON. `data` on success, `error` on failure.
- Versioning: v1, additive only.

## Quickstart

1. Create a free API key in Settings (https://clstr.news/settings). Sign in first, no card needed.
2. Call an endpoint:

```bash
curl -s "https://api.clstr.news/v1/situations?limit=3" \
  -H "Authorization: Bearer clstr_YOUR_KEY"
```

3. Or connect an agent over MCP instead. See "MCP server" below.

## Authentication

Every request needs an API key. Send it as `Authorization: Bearer clstr_YOUR_KEY` or as an `X-Api-Key` header. Responses allow any origin (`Access-Control-Allow-Origin: *`) so you can call the API from a browser during development. Keys are secrets: a key shipped inside front-end JavaScript is a key you have published. Call the API from your own server for anything real.

## Endpoints

Four read-only endpoints. All responses are JSON: `data` on success, `error.code` plus `error.message` on failure, including 404s on unknown paths and server errors. Timestamps are ISO-8601 UTC.

### GET /situations

Top developing situations, ranked by relevance: a blend of significance, how many outlets are covering it, and how recently it moved. Pass `sort=recent` for newest activity first. `significance_score` is an integer from 1 to 10, where 8 and above is exceptional.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `limit` | integer | 20 | 1 to 50. |
| `days` | integer | 7 | 1 to your tier's history window (7 on Free). Larger values are clamped, not rejected. |
| `sort` | string | relevance | `relevance` or `recent`. |
| `category` | string | all | One category slug, e.g. `international`. |
| `country` | string | all | Comma separated ISO country codes, up to 10. Extras are dropped, not rejected. |
| `cursor` | string | none | From the previous response's `next_cursor`. Paging stops after 9 pages. |

```json
{
  "data": [
    {
      "id": "8f2b1c6e-0a47-4d93-b8e1-77c5d9a2f410",
      "slug": "red-sea-shipping-attacks",
      "title": "Red Sea shipping attacks",
      "summary_preview": "Attacks on merchant shipping in the southern Red Sea have pushed most container lines onto the Cape route. Insurers have widened the war risk zone twice, and escort capacity remains the binding constraint on convoy slots",
      "cluster_count": 34,
      "source_count": 212,
      "first_seen": "2026-07-02T06:14:00.000Z",
      "last_updated": "2026-08-14T11:38:00.000Z",
      "status": "ACTIVE",
      "category": "international",
      "categories": [
        "international",
        "business"
      ],
      "countries": [
        "YE",
        "EG",
        "US"
      ],
      "significance_score": 9,
      "latest_cluster_title": "Insurers widen the Red Sea war risk zone",
      "url": "https://clstr.news/situations/red-sea-shipping-attacks"
    }
  ],
  "next_cursor": "eyJ2IjoxLCJzIjoicmVsZXZhbmNlIiwicCI6MiwicmVsIjo4LjIsImx1IjoiMjAyNi0wOC0xNFQxMTozODowMC4wMDBaIiwiaWQiOiI4ZjJiMWM2ZS0wYTQ3LTRkOTMtYjhlMS03N2M1ZDlhMmY0MTAiLCJmcCI6IiJ9"
}
```

`status` is `ACTIVE` or `QUIET`.

`category` filters on a situation's full `categories` list, so the top-level `category` field (which is just `categories[0]`) can differ from the value you requested.

Cursor pagination stops after 9 pages: past that, `next_cursor` comes back null. Narrow with `days`, `category` or `country` rather than paging deeper.

### GET /situations/{id}

One situation, by id or slug, with its ordered timeline of member events, newest first.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | path | required | Situation id or slug. |
| `timeline_limit` | integer | 100 | 1 to 500. |
| `timeline_before` | string | none | Cluster id cursor. Returns a continuation page. |

Example response, with the timeline truncated to two entries. A real response carries up to `timeline_limit` of them.

```json
{
  "data": {
    "id": "8f2b1c6e-0a47-4d93-b8e1-77c5d9a2f410",
    "slug": "red-sea-shipping-attacks",
    "title": "Red Sea shipping attacks",
    "summary": "Attacks on merchant shipping in the southern Red Sea have pushed most container lines onto the Cape route. Insurers have widened the war risk zone twice, and escort capacity remains the binding constraint on convoy slots.",
    "summary_preview": "Attacks on merchant shipping in the southern Red Sea have pushed most container lines onto the Cape route. Insurers have widened the war risk zone twice, and escort capacity remains the binding constraint on convoy slots",
    "cluster_count": 34,
    "source_count": 212,
    "first_seen": "2026-07-02T06:14:00.000Z",
    "last_updated": "2026-08-14T11:38:00.000Z",
    "status": "ACTIVE",
    "category": "international",
    "categories": [
      "international",
      "business"
    ],
    "latest_cluster_title": "Insurers widen the Red Sea war risk zone",
    "significance_score": 9,
    "url": "https://clstr.news/situations/red-sea-shipping-attacks",
    "is_timeline_page": false,
    "timeline": [
      {
        "id": "4f3c0a11-9d21-4b7e-8a55-2c9f1d6b0e34",
        "slug": "insurers-widen-red-sea-war-risk-zone",
        "title": "Insurers widen the Red Sea war risk zone",
        "summary": "Marine underwriters extended the listed war risk area to the southern Red Sea, raising premiums for Suez transits. Two carriers said they would keep routing around the Cape.",
        "category": "business",
        "countries": [
          "YE",
          "EG"
        ],
        "significance_score": 8,
        "sources": 18,
        "published_at": "2026-08-14T09:12:00.000Z",
        "updated_at": "2026-08-14T11:38:00.000Z",
        "url": "https://clstr.news/cluster/insurers-widen-red-sea-war-risk-zone"
      },
      {
        "id": "2d7ae803-64bf-41c0-9a1d-58e0c3f2b917",
        "slug": "second-naval-escort-group-ordered-to-bab-el-mandeb",
        "title": "A second naval escort group is ordered to Bab el-Mandeb",
        "summary": "A second escort group was ordered to the strait, roughly doubling the convoy slots on offer to commercial traffic.",
        "category": "international",
        "countries": [
          "YE",
          "US",
          "FR"
        ],
        "significance_score": 7,
        "sources": 24,
        "published_at": "2026-08-13T17:45:00.000Z",
        "updated_at": "2026-08-13T21:02:00.000Z",
        "url": "https://clstr.news/cluster/second-naval-escort-group-ordered-to-bab-el-mandeb"
      }
    ],
    "timeline_cursor": {
      "has_more": true,
      "next_before": "2d7ae803-64bf-41c0-9a1d-58e0c3f2b917",
      "remaining_count": 32,
      "total_count": 34
    },
    "day_span": 43
  }
}
```

Situation-wide `countries` is a list-endpoint field only. Each timeline entry carries its own.

A `timeline_before` request is a continuation page: it returns only `id`, `slug`, `is_timeline_page: true`, `timeline[]` and `timeline_cursor`. The header fields are deliberately absent rather than zeroed, so keep the ones you got from page one instead of overwriting them.

```json
{
  "data": {
    "id": "8f2b1c6e-0a47-4d93-b8e1-77c5d9a2f410",
    "slug": "red-sea-shipping-attacks",
    "is_timeline_page": true,
    "timeline": [
      {
        "id": "9c81b4f2-3e6d-42a8-b0d7-1f5a7e2c98db",
        "slug": "cape-route-diversions-add-two-weeks-to-asia-europe-runs",
        "title": "Cape route diversions add two weeks to Asia to Europe runs",
        "summary": "Schedule reliability fell again as lines rerouted around southern Africa. Two alliances published revised port rotations with longer transit times.",
        "category": "business",
        "countries": [
          "ZA",
          "EG",
          "CN"
        ],
        "significance_score": 7,
        "sources": 21,
        "published_at": "2026-08-12T14:20:00.000Z",
        "updated_at": "2026-08-12T18:05:00.000Z",
        "url": "https://clstr.news/cluster/cape-route-diversions-add-two-weeks-to-asia-europe-runs"
      },
      {
        "id": "b5e6f70a-8c13-4d29-9f44-6a2b0e17c3d5",
        "slug": "container-lines-add-a-red-sea-risk-surcharge",
        "title": "Container lines add a Red Sea risk surcharge",
        "summary": "Three carriers filed a per-container surcharge covering the higher war risk premium. Shippers asked regulators to review how long it stays on.",
        "category": "business",
        "countries": [
          "DK",
          "CH"
        ],
        "significance_score": 6,
        "sources": 15,
        "published_at": "2026-08-11T08:30:00.000Z",
        "updated_at": "2026-08-11T12:15:00.000Z",
        "url": "https://clstr.news/cluster/container-lines-add-a-red-sea-risk-surcharge"
      }
    ],
    "timeline_cursor": {
      "has_more": true,
      "next_before": "b5e6f70a-8c13-4d29-9f44-6a2b0e17c3d5",
      "remaining_count": 30,
      "total_count": 34
    }
  }
}
```

The cursor is exclusive: a continuation page starts strictly after the cluster named by `next_before`, so the entry you paged from is never repeated.

A situation that was merged into another returns the successor with `merged_into_shown: true` and `requested_id` set to what you asked for. A retired situation whose successor is an event answers 404 with `error.moved_to`. A concluded situation whose archive was removed answers 410.

### GET /clusters/{id}

One event (a cluster of articles from many outlets reporting the same thing), with its source articles.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | path | required | Cluster id or slug. |

```json
{
  "data": {
    "id": "4f3c0a11-9d21-4b7e-8a55-2c9f1d6b0e34",
    "slug": "insurers-widen-red-sea-war-risk-zone",
    "title": "Insurers widen the Red Sea war risk zone",
    "summary": "Marine underwriters extended the listed war risk area to the southern Red Sea, raising premiums for Suez transits. Two carriers said they would keep routing around the Cape.",
    "summary_full": "Marine underwriters extended the listed war risk area to the southern Red Sea, raising premiums for Suez transits. Two carriers said they would keep routing around the Cape until escort capacity increases. Brokers put the added cost of a single transit in the low hundreds of thousands of dollars.",
    "keywords": [
      "red sea",
      "war risk",
      "marine insurance",
      "suez"
    ],
    "category": "business",
    "countries": [
      "YE",
      "EG"
    ],
    "significance_score": 8,
    "sources": 18,
    "published_at": "2026-08-14T09:12:00.000Z",
    "updated_at": "2026-08-14T11:38:00.000Z",
    "url": "https://clstr.news/cluster/insurers-widen-red-sea-war-risk-zone",
    "articles": [
      {
        "source_host": "newswire.example.com",
        "url": "https://newswire.example.com/business/red-sea-war-risk",
        "title": "Underwriters widen Red Sea war risk area",
        "published_at": "2026-08-14T09:12:00.000Z"
      },
      {
        "source_host": "shipping.example.com",
        "url": "https://shipping.example.com/premiums-rise",
        "title": "Premiums rise again for southern Red Sea transits",
        "published_at": "2026-08-14T10:40:00.000Z"
      }
    ],
    "situation": {
      "id": "8f2b1c6e-0a47-4d93-b8e1-77c5d9a2f410",
      "title": "Red Sea shipping attacks",
      "slug": "red-sea-shipping-attacks",
      "cluster_count": 34
    }
  }
}
```

`summary` is the short form, `summary_full` the long one. `situation` is null for an event that has not joined one.

A merged-away cluster returns its successor with `merged_into_shown: true`. A retired one answers 410.

### GET /search

Semantic search over recent events.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `q` | string | required | Max 200 characters. |
| `days` | integer | 7 | 1 to 30, further clamped to your tier's history window. |
| `limit` | integer | 30 | 1 to 30. |
| `cursor` | string | none | From the previous response's `next_cursor`. Its own token, not the situations cursor. |

```json
{
  "data": [
    {
      "id": "4f3c0a11-9d21-4b7e-8a55-2c9f1d6b0e34",
      "slug": "insurers-widen-red-sea-war-risk-zone",
      "title": "Insurers widen the Red Sea war risk zone",
      "summary": "Marine underwriters extended the listed war risk area to the southern Red Sea, raising premiums for Suez transits. Two carriers said they would keep routing around the Cape.",
      "category": "business",
      "countries": [
        "YE",
        "EG"
      ],
      "significance_score": 8,
      "sources": 18,
      "published_at": "2026-08-14T09:12:00.000Z",
      "updated_at": "2026-08-14T11:38:00.000Z",
      "url": "https://clstr.news/cluster/insurers-widen-red-sea-war-risk-zone"
    },
    {
      "id": "2d7ae803-64bf-41c0-9a1d-58e0c3f2b917",
      "slug": "second-naval-escort-group-ordered-to-bab-el-mandeb",
      "title": "A second naval escort group is ordered to Bab el-Mandeb",
      "summary": "A second escort group was ordered to the strait, roughly doubling the convoy slots on offer to commercial traffic.",
      "category": "international",
      "countries": [
        "YE",
        "US",
        "FR"
      ],
      "significance_score": 7,
      "sources": 24,
      "published_at": "2026-08-13T17:45:00.000Z",
      "updated_at": "2026-08-13T21:02:00.000Z",
      "url": "https://clstr.news/cluster/second-naval-escort-group-ordered-to-bab-el-mandeb"
    }
  ],
  "query": {
    "q": "red sea shipping insurance",
    "days": 7
  },
  "next_cursor": "eyJ2IjoxLCJvIjozMH0"
}
```

The search index reaches back at most 30 days, further clamped to your tier's history window. `query.days` in the response is the window actually searched, not what you asked for.

Results are relevance ordered and bounded: the pool is the top matches, not an unbounded scan. Pass `next_cursor` back as `cursor` to page further; it comes back null once the candidate pool is exhausted. Each page re-runs the search, so one page counts as one search against the cap.

## Limits and pricing

Caps are hard limits: requests past the cap get a 429, and there are no overage charges, ever. Search is capped separately because each search runs an embedding.

| Tier | Price | Requests/day | Requests/min | Searches/day | History window |
| --- | --- | --- | --- | --- | --- |
| Anonymous (MCP taster) | No key | 10 | 5 | Keyed only | 7 days |
| Free | Free | 100 | 20 | 10 | 7 days |
| Builder | $29/mo | 5,000 | 60 | 250 | 30 days |
| Scale | $299/mo | 100,000 | 1,000 | 10,000 | 30 days |

The anonymous row is the MCP taster: 2 tools (get_top_situations, get_situation_timeline), no signup, counted per network rather than per person and capped platform-wide. Everything else, including search, needs a key.

Caps apply per key, and an account may hold up to 5 active keys. The history window is the widest `days` value a tier can ask for: larger values are clamped, not rejected. Search reaches back at most 30 days, and never beyond the tier's history window.

Builder and Scale are live: upgrade on the pricing page (https://clstr.news/pricing?product=api). If the caps still do not fit, get in touch (https://clstr.news/contact).

Free-tier use must link back to clstr.news when displayed publicly.

Every authenticated response carries `X-RateLimit-Limit-Day`, `X-RateLimit-Remaining-Day` and `X-RateLimit-Limit-Minute`, all exposed to browsers via CORS. There is no remaining-count for the per-minute window: it is enforced per Cloudflare location and reports no count, so any number here would be a guess. Pace against the daily remaining, and back off on a 429 using `Retry-After`. `/search` additionally carries `X-RateLimit-Limit-Search` and `X-RateLimit-Remaining-Search` for the daily search cap, which is the limit a free key reaches first. Responses rejected before authentication (a missing or invalid key) carry none of them, because there is no key to report usage for. There is also a generous per-address ceiling in front of authentication.

A 429 carries `Retry-After` in seconds: the exact time left in the window that fired, not a fixed guess.

## Errors

Every failure is `{"error": {"code": "...", "message": "..."}}` with a matching HTTP status. Branch on `code`. `message` is for humans and may change.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Daily cap reached for the hobby tier. Caps: https://clstr.news/developers#limits."
  }
}
```

Error messages name tiers by their internal key, which is what your key record stores. The Free tier's key is `hobby`.

| Status | code | Meaning |
| --- | --- | --- |
| 400 | `bad_request` | Missing or malformed parameter (e.g. no `q` on search). |
| 401 | `unauthorized` | No key, or the key is unknown or revoked. |
| 404 | `not_found` | Unknown id, slug, or endpoint. May carry `moved_to`. |
| 410 | `gone` | The record existed and was retired. Do not retry. |
| 429 | `rate_limited` | A cap was reached. The message names which one. |
| 500 | `internal_error` | Our fault. Safe to retry with backoff. |
| 502 | `upstream_error` | An upstream (embeddings) failed. Retry shortly. |
| 503 | `unavailable` | Temporarily unavailable. Retry shortly. |

Error objects are additive too: a 404 for a retired situation carries `moved_to` alongside `code` and `message`. Read the fields you know and ignore the rest.

A 429 may carry `error.upgrade_url`, a link to the plan that lifts the cap you hit.

## Versioning and stability

v1 is stable: we only add fields, never rename or remove them within v1. Breaking changes get a new version prefix and at least 90 days' notice on this page. Caps are hard limits. No overage charges, ever.

## MCP server

Your agent can search the web. It can't tell you what's happening: which developing situations matter, what 40 sources collectively report, and how a story evolved over six weeks. CLSTR can, over MCP.

One event covered by many outlets becomes a single cluster, deduplicated, with a source count and a significance score. Related clusters link into a situation that carries a maintained summary and a full timeline, so an agent gets memory, not just the latest headline.

Remote server, no install. Same API key, same caps (one tool call counts as one request).

```
Endpoint:  https://mcp.clstr.news
Auth:      Authorization: Bearer clstr_YOUR_KEY
Tools:     get_top_situations, search_situations, get_situation_timeline, get_cluster
```

| Tool | REST equivalent |
| --- | --- |
| `get_top_situations` | `GET /situations` |
| `search_situations` | `GET /search` |
| `get_situation_timeline` | `GET /situations/{id}` |
| `get_cluster` | `GET /clusters/{id}` |

Try it with no key: get_top_situations, get_situation_timeline answer anonymously, 10 calls per day per network (burst 5 per minute), and anonymous timelines come back at most 50 entries deep. A free key lifts that to 100 requests per day, full timeline depth, and search.

Clients with an `mcpServers` config:

```json
"clstr": {
  "url": "https://mcp.clstr.news",
  "headers": { "Authorization": "Bearer clstr_YOUR_KEY" }
}
```

stdio-only client? Bridge with `npx mcp-remote https://mcp.clstr.news --header "Authorization: Bearer clstr_YOUR_KEY"`.

The tools obey the same windows as the REST endpoints: `search_situations` reaches back at most 30 days, and `get_top_situations` takes 24h, 7d or 30d. Refusals a caller can act on (a cap, an unknown id) come back as readable tool results. Transport problems come back as JSON-RPC errors with the HTTP status to match.

`get_top_situations` and `search_situations` paginate: when more results exist, the tool result ends with a `cursor` to pass back. For `get_top_situations`, send the same window and category with it. For `search_situations`, each page re-runs the search, so one page counts as one search against the cap.

---
Questions or a use case the caps do not fit? https://clstr.news/contact
