# API reference Every operation on `/api/v1`. Authentication, paging, errors and rate limits are on the [API intro](/docs/api) page. This page is generated from [openapi.json](/docs/openapi.json). We check that file against the live routes on every build, in both directions. ## Posts The corpus itself: newsletter issues, podcast episodes, blog posts. ### GET /api/v1/posts **Browse posts, newest first** A chronological feed. Give at most one scope — `collection`, `source`, `person`, `topic` or `sponsor` — to narrow it; they are alternative ways of cutting the same list, not filters that compose, so passing two is a 422 with `code: scope_conflict`. For relevance-ranked search, use `GET /api/v1/posts/search`. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `collection` | query | `string` | Collection slug. Restricts to that collection's publications. | | `source` | query | `string` | Publication slug. | | `person` | query | `string` | Person slug. Posts they are credited on. | | `topic` | query | `string` | Topic slug. | | `sponsor` | query | `string` | Sponsor slug. Posts that read the brand out. | | `window` | query | `all | 24h | 48h | week | month | 3months | year` | A rolling window measured back from now, not a calendar period. | | `include_dead_sources` | query | `boolean` | Include posts from publications we have marked dead (no longer publishing). Archived publications stay excluded either way. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of posts. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/posts/search **Full-text search** Ranked by relevance by default, or by recency for a chronological stream of mentions. Bare space-separated terms are ANDed, and may match anywhere in a document — on a two-hour transcript, `comfort food` matches an episode saying "comfort" in the intro and "food" an hour later. Quote a phrase you mean as a phrase. Quoted phrases match an exact word sequence; uppercase `AND`/`OR`/`NOT` combine terms; `field:value` scopes to `title`, `body`, `transcript` or `author`. Malformed syntax is repaired rather than rejected. There is deliberately **no person filter** here. We credit a person on far too few posts for it to be a filter worth trusting, and a keyword search for the name finds more. Chronological browse still takes `person`, because that reads the credits directly rather than the index. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `q` (required) | query | `string` | What to search for. | | `sort` | query | `relevance | recency` | Result ordering. | | `collection` | query | `string` | Collection slug. Pass at most one of collection or source. | | `source` | query | `string` | Publication slug. | | `window` | query | `all | 24h | 48h | week | month | 3months | year` | A rolling window measured back from now. | | `include_dead_sources` | query | `boolean` | Include posts from publications marked dead. | | `source_type` | query | `string` | One or more of `podcast`, `newsletter`, `blog`, comma-separated and OR'd. What the publication *is*. | | `publishing_platform` | query | `string` | One or more of `rss`, `substack`, `beehiiv`, `bluesky`, `email`, comma-separated and OR'd. Where it is *hosted*. Independent of source_type: a Substack-hosted podcast is both. | | `language` | query | `string` | Two-letter language code, e.g. `en`. | | `has_transcript` | query | `boolean` | Omit for no filter. | | `has_audio` | query | `boolean` | Omit for no filter. | | `min_word_count` | query | `integer` | Inclusive. | | `min_duration_seconds` | query | `integer` | Inclusive. Seconds, so 1200 is twenty minutes. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of results, each with a highlighted `snippet`. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/posts/{post} **One post** The full record, including body text where we hold it. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `post` (required) | path | `string` | The post slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The post. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/posts/{post}/transcript **A podcast episode as speaker turns** Paged speaker turns, with real names substituted for "Speaker 1" wherever we could identify them. This is the only way to read a transcript — the words are never inlined on the post itself. `transcript_status` rides alongside the envelope so a caller that gets zero turns can tell "not transcribed yet" from "transcribed, but empty". **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `post` (required) | path | `string` | The post slug. | | `format` | query | `turns | markdown` | turns returns JSON objects, one per turn. markdown returns the same page as readable text, laid out the way the web page shows it. Both page by turn. Over MCP the default is markdown. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of the transcript, in the requested shape. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## People Authors, hosts and guests credited on posts. ### GET /api/v1/people **Find a person by name or handle** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `q` (required) | query | `string` | A name or handle. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of people. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/people/{person} **One person** Adds what they have been talking about lately and where they appear. `topics` is empty for anyone whose posts predate topic extraction. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `person` (required) | path | `string` | The person slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The person. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Sources The publications we index. ### GET /api/v1/sources **The publications we index** With no `q`, the browsable list of everything we index. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `q` | query | `string` | Optional name filter. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of publications. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/sources/{source} **One publication** Adds what it has been covering lately and which brands have advertised on it. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `source` (required) | path | `string` | The publication slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The publication. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Topics What the corpus is about, extracted off posts. ### GET /api/v1/topics **What the corpus is about** With `q`, all-time matches for that name; without it, the most-covered topics of the last 90 days. **This index is capped, not paged.** It returns at most 100 rows, `next_cursor` is always null, and `total` goes null when the cap was hit. Passing `cursor` is a 422 — narrow with `q` instead. Past the first hundred, a ranked list of a million topics is not something anyone reads, and the query stops being cheap. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `q` | query | `string` | Optional name filter, matched anywhere in the name. | | `limit` | query | `integer` | How many rows. Honoured up to the cap. | **Responses** | Status | Meaning | | --- | --- | | `200` | Up to 100 topics. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/topics/{topic} **One topic** Adds the publications covering it most and the people credited on its posts. For the posts themselves, use `GET /api/v1/posts?topic=`. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `topic` (required) | path | `string` | The topic slug. Names are display text; do not pass one as a key. | **Responses** | Status | Meaning | | --- | --- | | `200` | The topic. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Sponsors Brands read out as sponsors. Ads that ran, not deals a show signed. ### GET /api/v1/sponsors **Brands read out as sponsors** With `q`, all-time matches for that brand name, matched case-insensitively; without it, the most-heard sponsors of the last 90 days. Capped rather than paged, exactly like topics. **Read these as ads that ran, not deals a show signed.** Programmatic insertion puts spots into feeds the publisher never sold — a physics channel's transcript reads out ads a streaming platform placed, not sponsors it found. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `q` | query | `string` | Optional brand-name filter. | | `limit` | query | `integer` | How many rows. Honoured up to the cap. | **Responses** | Status | Meaning | | --- | --- | | `200` | Up to 100 sponsors. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/sponsors/{sponsor} **One sponsor** Adds the shows the brand has been heard on, most appearances first. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `sponsor` (required) | path | `string` | The sponsor slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The sponsor. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Collections Named sets of publications, used to scope a search. ### GET /api/v1/collections **Your team's collections, and ours** Both the collections your team owns and the global ones Sputnik curates. `is_global` tells them apart; global ones are readable by everyone and editable by no one. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of collections. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/collections **Create a collection** Created empty. Add publications with `POST /collections/{collection}/sources`. **Body** | Field | Type | Notes | | --- | --- | --- | | `name` (required) | `string` | | | `description` | `string?` | | **Responses** | Status | Meaning | | --- | --- | | `201` | The new collection. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/collections/{collection} **One collection, with its publications** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `collection` (required) | path | `string` | The collection slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The collection. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### PATCH /api/v1/collections/{collection} **Rename a collection** Only the given fields change. Which publications are in it is a separate thing. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `collection` (required) | path | `string` | The collection slug. | **Body** | Field | Type | Notes | | --- | --- | --- | | `name` | `string` | | | `description` | `string?` | | **Responses** | Status | Meaning | | --- | --- | | `200` | The updated collection. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/collections/{collection} **Delete a collection** Removes the set, not the publications in it. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `collection` (required) | path | `string` | The collection slug. | **Responses** | Status | Meaning | | --- | --- | | `204` | Deleted. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/collections/{collection}/sources **Add publications to a collection** Adding one already in it is a no-op, so this is safe to retry. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `collection` (required) | path | `string` | The collection slug. | **Body** | Field | Type | Notes | | --- | --- | --- | | `source_slugs` (required) | `array` | Publication slugs, from search_sources. | **Responses** | Status | Meaning | | --- | --- | | `200` | The collection, with its new count. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/collections/{collection}/sources/{source} **Remove a publication from a collection** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `collection` (required) | path | `string` | The collection slug. | | `source` (required) | path | `string` | The publication slug. | **Responses** | Status | Meaning | | --- | --- | | `204` | Removed. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Projects One thing your team monitors for. Alerts live inside a project. ### GET /api/v1/projects **What your team monitors for** A project is usually one of your own customers. Archived ones are hidden unless you ask for them. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `include_archived` | query | `boolean` | Include archived projects. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of projects. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/projects **Create a project** **Responses** | Status | Meaning | | --- | --- | | `201` | The new project. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/projects/{project} **One project** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `project` (required) | path | `string` | The project slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The project. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### PATCH /api/v1/projects/{project} **Update or archive a project** Setting `is_active` to false archives it and switches off every alert in it. Setting it back to true restores the project but leaves those alerts off — turning monitoring back on should be a decision, not a side effect. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `project` (required) | path | `string` | The project slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The updated project. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/projects/{project} **Delete a project** Takes its alerts and their hits with it. Archive instead if you want the history. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `project` (required) | path | `string` | The project slug. | **Responses** | Status | Meaning | | --- | --- | | `204` | Deleted. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Alerts Standing searches that tell you when something new matches. ### GET /api/v1/alerts **Your team's alerts** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of alerts. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/alerts **Create an alert** An alert belongs to a project and holds up to ten saved searches; a post hits the alert if any of them matches. `search` is a convenience that creates the alert's first saved search in the same call, so "watch for this" is one request. It is nested because an alert and its search both have a name — `search.q` and `search.name` are exactly what the searches endpoint takes. A new alert delivers nowhere until you give it a destination. That state is legal, and the app flags it. **Body** | Field | Type | Notes | | --- | --- | --- | | `name` (required) | `string` | Shown in notifications, e.g. "Competitor: Acme". | | `project` (required) | `string` | Project slug. An archived project, or one belonging to another team, is a 422. | | `is_active` | `boolean` | | | `search` | `object` | Optional first saved search. | **Responses** | Status | Meaning | | --- | --- | | `201` | The new alert. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/alerts/{alert} **One alert** With its searches, its destinations and its hit count. The hits themselves come from `GET /alert-hits`. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The alert. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### PATCH /api/v1/alerts/{alert} **Update an alert** Changes only what is true of the notification — its name, its project, whether it is on. What it looks for lives on its searches; where it delivers on its destinations. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Body** | Field | Type | Notes | | --- | --- | --- | | `name` | `string` | | | `project` | `string` | Move the alert to another project. | | `is_active` | `boolean` | | **Responses** | Status | Meaning | | --- | --- | | `200` | The updated alert. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/alerts/{alert} **Delete an alert** Takes its hits with it. Set `is_active` to false instead to stop it matching while keeping the history. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Responses** | Status | Meaning | | --- | --- | | `204` | Deleted. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/alerts/{alert}/searches **What an alert looks for** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | The alert's saved searches. Small and unpaged, but the envelope is the same. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/alerts/{alert}/searches **Add a saved search** Takes `q` and the same filter names `GET /posts/search` takes, so a search that worked lifts straight into an alert without renaming anything. A search added today starts finding hits on the next run, which looks back a day. It does not reach further back: an alert reports new things, and the history of a query belongs in search. An alert holds ten searches; the eleventh is a 422. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Responses** | Status | Meaning | | --- | --- | | `201` | The new saved search. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### PATCH /api/v1/alerts/{alert}/searches/{search} **Edit a saved search** Applies from the next run onwards. It does not re-run the new definition over the past — that would report old posts as news. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | | `search` (required) | path | `integer` | The search id, from the list endpoint. Resolved through its alert, so another team's id is a 404. | **Responses** | Status | Meaning | | --- | --- | | `200` | The updated search. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/alerts/{alert}/searches/{search} **Switch a saved search off** Deactivates rather than deletes: hits exist because this search found them, and removing it outright orphans that explanation. PATCH `is_active` back to true to restore it. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | | `search` (required) | path | `integer` | The search id. | **Responses** | Status | Meaning | | --- | --- | | `204` | Deactivated. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/alerts/{alert}/destinations **Where an alert delivers** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Responses** | Status | Meaning | | --- | --- | | `200` | Its destinations. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/alerts/{alert}/destinations **Point an alert somewhere** Give exactly one of `email` or `webhook`. The channel comes from the webhook rather than being passed in — the two must agree, and this is the only way to guarantee it. Adding a destination does not replay history: hits already delivered stay delivered, so connecting a webhook in week three does not fire hundreds of events about things you have already seen. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | **Body** | Field | Type | Notes | | --- | --- | --- | | `email` | `string?` | | | `webhook` | `string?` | Slug of one of your team's webhooks. | **Responses** | Status | Meaning | | --- | --- | | `201` | The new destination. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/alerts/{alert}/destinations/{destination} **Stop delivering to one destination** **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` (required) | path | `string` | The alert slug. | | `destination` (required) | path | `integer` | The destination id, from the list endpoint. | **Responses** | Status | Meaning | | --- | --- | | `204` | Removed. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### GET /api/v1/alert-hits **What your alerts have found** A resumable stream, not an ordinary page. `next_cursor` is a resume token: store it, pass it back as `cursor`, and you get only what has arrived since. It keeps coming back non-null even when there is nothing new, so poll it rather than waiting for null. Each hit carries the post, the alert, and which of its searches matched. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `alert` | query | `string` | Restrict to one alert, by slug. | | `limit` | query | `integer` | Rows per page, 1–100. Defaults to 50, or 20 for people and publications. | | `cursor` | query | `string` | The `next_cursor` from the previous response. Opaque — do not construct one. Omit for the first page. | **Responses** | Status | Meaning | | --- | --- | | `200` | A page of hits, oldest first. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ## Webhooks Reusable delivery endpoints owned by your team. ### GET /api/v1/webhooks **Your team's delivery endpoints** Registered once for the team and reusable by any of its alerts, in any project. The signing secret comes back on every read. **Responses** | Status | Meaning | | --- | --- | | `200` | The webhooks. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### POST /api/v1/webhooks **Register an endpoint** Registering one delivers nothing on its own — point an alert at it with `POST /alerts/{alert}/destinations`. **Body** | Field | Type | Notes | | --- | --- | --- | | `name` (required) | `string` | | | `url` (required) | `string` | For Slack, its incoming webhook URL. | | `channel` | `webhook | slack` | webhook sends signed JSON; slack sends Block Kit. | **Responses** | Status | Meaning | | --- | --- | | `201` | The new webhook, with its signing secret. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### PATCH /api/v1/webhooks/{webhook} **Rename, repoint, or switch off** Switching one back on also clears its failure streak, so an endpoint you have just fixed gets a fresh run of attempts rather than deactivating itself on its very next failure. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `webhook` (required) | path | `string` | The webhook slug. | **Body** | Field | Type | Notes | | --- | --- | --- | | `name` | `string` | | | `url` | `string` | | | `is_active` | `boolean` | false stops delivery to this endpoint. | **Responses** | Status | Meaning | | --- | --- | | `200` | The updated webhook. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. | ### DELETE /api/v1/webhooks/{webhook} **Delete an endpoint** Blocked with a 422 while any alert still delivers here: cascading would silently mute those alerts, and you would find out by not being told about something. PATCH `is_active` to false instead. **Parameters** | Name | In | Type | Notes | | --- | --- | --- | --- | | `webhook` (required) | path | `string` | The webhook slug. | **Responses** | Status | Meaning | | --- | --- | | `204` | Deleted. | | `422` | The request was rejected. `code` says why: `validation_failed` for a bad parameter, `invalid_cursor` for a cursor we did not issue, `scope_conflict` for mutually exclusive scopes passed together. | | `404` | No such slug. `code` is `not_found`. If you passed a numeric id, the message says so — this API addresses everything by slug. | | `403` | The key is valid but its team may not touch this row. `code` is `forbidden`. | | `401` | Missing, malformed or revoked API key. `code` is `unauthenticated`. | | `402` | The team has no active plan. `code` is `no_plan`. Unlike a rate limit, retrying will not help. | | `429` | Too many requests. `code` is `rate_limited`; wait the number of seconds in the `Retry-After` header. |