# API intro A JSON API over HTTPS at `https://sputnikintelligence.com/api/v1`. Everything the web app and MCP can do is available here. This page covers the things that apply to every endpoint. The [API reference](/docs/api-reference) lists each one. ## Authentication Send your API key as a bearer token. ```bash curl -H "Authorization: Bearer $SPUTNIK_TOKEN" \ "https://sputnikintelligence.com/api/v1/posts?limit=5" ``` Create a key in the app under **Settings → API keys**. It is shown once. We store only a hash, so if you lose it, create a new one and revoke the old. A key acts for the team it was created in. It does not follow you between teams. It stops working if you leave that team. See [Teams](/docs/teams). Revoking takes effect immediately. Send every request over HTTPS, and keep the key out of URLs, repositories and browsers. MCP uses a different credential. It signs you in through the browser, and an API key does not work against it. If your code cannot do an interactive sign-in, use this API. ## Conventions ### Everything is addressed by slug A slug looks like `k3mq-8w1p-ttz4`. It comes back on every search and list result. We do not expose database ids. If you pass a number where a slug belongs, you get a 404 that tells you so. ### Lists ```json { "items": [...], "total": 1204, "next_cursor": "50" } ``` Every list endpoint returns this shape, including short ones like an alert's destinations. ### Single items ```json { "data": { "slug": "k3mq-8w1p-ttz4", "title": "..." } } ``` ### Timestamps ISO 8601 in UTC, for example `2026-08-19T06:00:00Z`. A null timestamp means we do not know it. Some feeds do not date their items. ### Booleans `true`, `false`, `1` and `0` all work in query strings. ## Paging **Follow `next_cursor` until it comes back null.** That is the only stop condition. A page shorter than your `limit` does not mean you are finished. ```bash cursor="" while :; do page=$(curl -sG -H "Authorization: Bearer $SPUTNIK_TOKEN" \ --data-urlencode "q=your company" \ --data-urlencode "limit=100" \ --data-urlencode "cursor=$cursor" \ "https://sputnikintelligence.com/api/v1/posts/search") echo "$page" | jq -c '.items[]' cursor=$(echo "$page" | jq -r '.next_cursor // empty') [ -z "$cursor" ] && break done ``` The cursor is opaque. Pass back what we gave you. Do not build one or read anything out of it. An unrecognised cursor gives you a 422. `limit` runs from 1 to 100. It defaults to 50, or 20 for people and publications. Asking for more than 100 gives you a 422 rather than a silent clamp. `total` is null when we do not know the number cheaply. Null means unknown, not zero. ### Two endpoints are capped instead of paged `/topics` and `/sponsors` return at most 100 rows. `next_cursor` is always null and passing `cursor` gives you a 422. Narrow with `q`. ### Alert hits are a resumable stream `/alert-hits` uses the same shape for something different. Its `next_cursor` is a resume token. Store it, pass it back next time, and you get only what has arrived since. It stays non-null even when nothing is new, so poll it on a schedule. ## Errors ```json { "message": "Invalid cursor \"page-2\"; pass the next_cursor value from a previous response.", "code": "invalid_cursor", "errors": { "cursor": ["Invalid cursor \"page-2\"; ..."] } } ``` Branch on `code`. The `message` is for a human to read and may be reworded. `errors` appears on validation failures and gives you per-field messages. | Code | Status | Meaning | | --- | --- | --- | | `validation_failed` | 422 | A parameter was wrong. `errors` says which. | | `invalid_cursor` | 422 | A cursor we did not issue. Start again without one. | | `scope_conflict` | 422 | You passed two scopes that cannot be combined. | | `not_found` | 404 | No such slug. | | `unauthenticated` | 401 | Missing, malformed or revoked key. | | `forbidden` | 403 | Valid key, but its team cannot touch this. | | `no_plan` | 402 | The team has no active plan. | | `rate_limited` | 429 | Too many requests. | Retry `rate_limited` after the seconds given in the `Retry-After` header. Do not retry anything else. A 402 in particular will not clear on retry: the team has no plan, and a client that treats it as a throttle will loop forever. ## Rate limits | Plan | Calls per month | Calls per day | Burst, per minute | | --- | --- | --- | --- | | Startup | 10,000 | 1,000 | 60 | | Scale | 50,000 | 5,000 | 120 | | Enterprise | custom | 50,000 and up | 300 | The allowance is shared across the API, MCP and the CLI. One MCP tool call costs the same as one HTTP request. The monthly allowance resets on the 1st. The daily limit resets 24 hours after your first call of the day. The per-minute limit stops one runaway process burning a day's allowance in a minute. It sits well above normal use. The 429 body tells you which limit you hit and how long to wait. ```json { "message": "You have hit the burst limit of 60 calls per minute...", "code": "rate_limited", "limit_type": "requests_per_minute", "limit": 60, "retry_after_seconds": 12 } ``` To use fewer calls: - Raise `limit`. A page of 100 costs the same as a page of 10. - Use search results to decide what is worth fetching in full. - Poll `/alert-hits` instead of re-running searches. - Scope searches to a collection. A request that reached us counts, including one that failed validation. Our own 429s and 402s do not count. Month-to-date usage is on the billing page. ## Generating a client [openapi.json](/docs/openapi.json) is an OpenAPI 3.1 description of the whole API. It is public and needs no key. We check it against the live routes on every build.