{"openapi":"3.1.0","info":{"title":"Octolens API","version":"2.0.0","description":"The Octolens API lets you query mentions, manage keywords, and configure feeds programmatically. Every action available in the Octolens UI is available here.\n\n### Authentication\n\nAll v2 endpoints require an API key. Create one in **Settings > API** and pass it as `Authorization: Bearer <key>`. Keys are scoped to the organization they were minted in; you cannot access another org's data.\n\nAPI keys carry a scope — `read`, `write` (implies read), or `admin` (implies write). Each endpoint documents the scope it needs via the `x-required-scope` OpenAPI extension and the scope badge in the docs surface.\n\n### Rate limiting\n\nThe v2 API is rate-limited at **500 requests per hour per organization**, across all keys for that org. The limit resets at the top of each clock hour (fixed hourly window — the counter is keyed by the UTC hour bucket, not a rolling trailing hour).\n\nEvery 2xx response carries three headers so clients can pace themselves:\n* `X-RateLimit-Limit` — the hourly cap (500)\n* `X-RateLimit-Remaining` — requests left in the current window\n* `X-RateLimit-Reset` — Unix timestamp (seconds) when the window resets\n\nWhen the cap is hit, the endpoint returns **429 Rate Limited** with an additional `Retry-After` header (seconds until the next window). The response body is the standard `ErrorResponse` with `code: \"RATE_LIMITED\"`.\n\n### Response timing\n\nEvery response also carries a standard `Server-Timing` header with the server-side breakdown of that request — `auth` (credential verification, plan gate and rate limiting), `handler` (the endpoint's own work) and `total`, in milliseconds. Use it to tell whether a slow call was spent in the API's request pipeline, in the query behind the endpoint, or on the network in between; browsers surface it automatically in devtools. Responses rejected before the endpoint ran carry `auth` and `total` only, and an endpoint may publish extra metrics of its own alongside these three — parse the header as a list, not as a fixed triple.\n\n### Error handling\n\nAll non-2xx responses share the same `ErrorResponse` envelope: `{ error: { code, message, status, details? } }`. The `code` field is a stable `ApiErrorCode` enum — branch on it programmatically instead of parsing `message`. See the `ApiErrorCode` schema for the full catalog grouped by category.\n\n`VALIDATION_ERROR` (400) responses include a `details` array with per-field Zod issues — inspect `details[i].path` to pinpoint which input was rejected.\n\n### Building filter bodies\n\nEndpoints that accept `simpleFilters` / `advancedFilters` (e.g. `POST /api/v2/mentions`, `PATCH /api/v2/feeds/{id}`) take a structured object that can be tricky to hand-craft. If you just have a natural-language description of what you want (\"negative posts about pricing on reddit in the last week\"), call `POST /api/v2/ai/filter-wizard` with that prompt and it will return a ready-to-use filter object you can pass straight through."},"servers":[{"url":"https://app.octolens.com","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"security":[{"ApiKey":[]}],"paths":{"/api/v2/auth":{"get":{"operationId":"introspectAuth","summary":"Introspect the credential","description":"Returns the authenticated credential: the workspace it is bound to, how it authenticates, and its effective scopes. Use this to preflight whether the key can perform writes (`write`) or admin actions (`admin`) before calling a mutating endpoint. Keys minted before scoping existed report `[\"read\"]` — the access the scope gate actually grants them.","tags":["Auth"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthIntrospection"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/feeds":{"get":{"operationId":"listFeeds","summary":"List feeds","description":"Returns all feeds. A feed combines a saved filter (view) with optional notification destinations.","tags":["Feeds"],"x-required-scope":"read","parameters":[{"in":"query","name":"excludeWithNotifications","schema":{"description":"When `true`, omits feeds that already have a notification destination attached. Useful for surfacing feeds that exist but aren't wired up to email/Slack/webhook yet. Accepts the literal string `\"true\"` or `\"false\"`.","example":"true","$ref":"#/components/schemas/QueryBoolean"},"description":"When `true`, omits feeds that already have a notification destination attached. Useful for surfacing feeds that exist but aren't wired up to email/Slack/webhook yet. Accepts the literal string `\"true\"` or `\"false\"`."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"createFeed","summary":"Create a feed","tags":["Feeds"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeedRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feed"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/feeds/{id}":{"get":{"operationId":"getFeed","summary":"Get a feed","tags":["Feeds"],"x-required-scope":"read","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feed"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateFeed","summary":"Update a feed","description":"Update any combination of name, icon, filters, default flag, or destinations. Omit `destinations` to leave them unchanged; pass an empty array to remove all destinations.","tags":["Feeds"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeedRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feed"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"deleteFeed","summary":"Delete a feed","description":"Delete a feed. This also deletes the notification attached to it, if any — their ids come back in `deletedNotificationIds`. Deleting a feed that is already gone returns 404 `FEED_NOT_FOUND`, including when it was removed by a concurrent caller.","tags":["Feeds"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deletedNotificationIds":{"description":"Notifications destroyed along with the feed (empty when the feed had none). Deleting a feed CASCADES to the notification attached to it — this is how a scripted caller learns which alerting it just lost.","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}},"required":["ok","deletedNotificationIds"],"additionalProperties":false}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/keywords":{"get":{"operationId":"listKeywords","summary":"List keywords","description":"Returns all keywords being tracked by the authenticated organization.","tags":["Keywords"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"createKeyword","summary":"Create a keyword","description":"Start monitoring a new keyword. The keyword is immediately added to the data collection pipeline. Creating a term that is already tracked (case-insensitive exact match) is a no-op: the existing keyword is returned with `alreadyExisted: true` and nothing new is created — pass `allowDuplicate: true` to force a duplicate.","tags":["Keywords"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeywordRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeywordResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/keywords/{id}":{"patch":{"operationId":"updateKeyword","summary":"Update a keyword","tags":["Keywords"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateKeywordRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Keyword"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"deleteKeyword","summary":"Delete a keyword","description":"Stops monitoring and removes the keyword. Feeds whose filters reference only this keyword are deleted too, and each of those feeds takes its notification with it. The response names both: `deletedFeeds` and `deletedNotifications` are always present, and empty when nothing else was destroyed. Use `GET /api/v2/keywords/{id}/deletion-preview` to see the same cascade before committing to it.","tags":["Keywords"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteKeywordResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/keywords/{id}/pause":{"post":{"operationId":"setKeywordPaused","summary":"Pause or resume a keyword","description":"Moves a keyword to an explicit `paused` state. The write is ABSOLUTE and idempotent — `{ \"paused\": true }` on an already-paused keyword changes nothing and answers `changed: false` — so it is safe to retry and safe to call concurrently. The `paused` field in the response is the COMMITTED value read back after the write, never an echo of the request.","tags":["Keywords"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PauseKeywordRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PauseKeywordResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/keywords/{id}/deletion-preview":{"get":{"operationId":"previewKeywordDeletion","summary":"Preview what deleting a keyword would destroy","description":"Lists the feeds that would be deleted along with this keyword (their filters reference only it) and the notifications attached to those feeds, which go with them. Changes nothing. Both arrays are empty when the keyword can be deleted with no collateral. A preview is a snapshot, not a lock — a feed created after it is answered is still destroyed by the delete; `DELETE /api/v2/keywords/{id}` reports what was ACTUALLY removed. 404 `KEYWORDS_NOT_FOUND` for an unknown id, exactly as the delete does.","tags":["Keywords"],"x-required-scope":"read","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordDeletionPreview"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/mentions":{"post":{"operationId":"listMentions","summary":"List mentions","description":"Returns mentions scoped to the authenticated organization. Filter by keyword, source, sentiment, date range, or custom filter groups.\n\nBody shape matches `POST /api/v1/mentions` exactly — simple or advanced `filters` object, `view` to reuse a feed's filters, `includeAll` to toggle low-relevance, cursor pagination via `limit` + `cursor`.\n\nTip: call `POST /api/v2/ai/filter-wizard` with a natural-language prompt to get a ready-to-paste `filters` object.","tags":["Mentions"],"x-required-scope":"read","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMentionsRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MentionListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/mentions/by-author":{"get":{"operationId":"listMentionsByAuthor","summary":"List mentions by author","description":"Returns mentions authored by one person on a specific platform — e.g. to enrich a CRM record with everything a prospect has said. Scoped to the authenticated organization and always to a single `source` (the same handle can belong to different people on different platforms). Identify the author by `handle` or `profileUrl` (at least one required). Paginated via `limit` + `cursor`.","tags":["Mentions"],"x-required-scope":"read","parameters":[{"in":"query","name":"source","schema":{"description":"Platform to scope the lookup to. One of: twitter, reddit, bluesky, dev, github, hackernews, tiktok, linkedin. Display-name-only platforms (youtube, stackoverflow, news, newsletter, podcasts) are not supported — the author there isn't a unique handle.","type":"string","enum":["twitter","reddit","bluesky","dev","github","hackernews","tiktok","linkedin"]},"required":true,"description":"Platform to scope the lookup to. One of: twitter, reddit, bluesky, dev, github, hackernews, tiktok, linkedin. Display-name-only platforms (youtube, stackoverflow, news, newsletter, podcasts) are not supported — the author there isn't a unique handle."},{"in":"query","name":"handle","schema":{"description":"The author's handle/username on `source` (the handle platforms: twitter, reddit, bluesky, dev, github, hackernews, tiktok). Accepts `elonmusk`, `@elonmusk`, or a profile URL — normalized automatically. Required unless `profileUrl` is given.","type":"string"},"description":"The author's handle/username on `source` (the handle platforms: twitter, reddit, bluesky, dev, github, hackernews, tiktok). Accepts `elonmusk`, `@elonmusk`, or a profile URL — normalized automatically. Required unless `profileUrl` is given."},{"in":"query","name":"profileUrl","schema":{"description":"For `source=linkedin`: a `linkedin.com/in/<slug>` URL, `in/<slug>`, or a bare `<slug>`. Required for LinkedIn unless `handle` is given.","type":"string"},"description":"For `source=linkedin`: a `linkedin.com/in/<slug>` URL, `in/<slug>`, or a bare `<slug>`. Required for LinkedIn unless `handle` is given."},{"in":"query","name":"limit","schema":{"description":"Max mentions to return (1–50, default 10).","type":"integer","minimum":1,"maximum":50},"description":"Max mentions to return (1–50, default 10)."},{"in":"query","name":"cursor","schema":{"description":"Pagination cursor from a previous response's `nextCursor`.","type":"string"},"description":"Pagination cursor from a previous response's `nextCursor`."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MentionByAuthorListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/mentions/{sourceId}":{"get":{"operationId":"getMention","summary":"Get a mention","description":"Returns one mention by its stable platform-native `sourceId` (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a `POST /api/v2/mentions` row is also accepted, for recent and archived mentions alike. Shape matches items in `POST /api/v2/mentions`.\n\nIf the id is path-reserved (`.`, `..`) or equals a sibling route name (`by-author`, `export`), address it via `GET /api/v2/mentions/by-id?sourceId=…` — the query-parameter twin of this route.","tags":["Mentions"],"x-required-scope":"read","parameters":[{"in":"path","name":"sourceId","schema":{"description":"Stable platform-native mention id (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a list row is also accepted — both resolve for recent and archived mentions. For ids that cannot ride in a URL path segment (`.`, `..`, or ids equal to the sibling route names `by-author`/`export`), use `GET /api/v2/mentions/by-id?sourceId=…` instead.","example":"reddit_t3_1abc234","type":"string"},"required":true,"description":"Stable platform-native mention id (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a list row is also accepted — both resolve for recent and archived mentions. For ids that cannot ride in a URL path segment (`.`, `..`, or ids equal to the sibling route names `by-author`/`export`), use `GET /api/v2/mentions/by-id?sourceId=…` instead."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mention"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateMention","summary":"Update a mention","description":"Set the engaged state, or update AI-judged relevance or sentiment. The `action` field selects the operation. For `engage`, pass `engaged: true|false` for an absolute, idempotent write; omitting it toggles the current value.\n\nReturns the COMMITTED mention row (same shape as `GET /api/v2/mentions/{sourceId}`), never an echo of the request — e.g. `relevance: 3` (clear) answers with the restored AI verdict, and the `engaged` field is the value the toggle actually stored. No follow-up GET is needed to learn the resulting state.\n\nFor ids that cannot ride in a URL path segment, use `PATCH /api/v2/mentions/by-id?sourceId=…` with the same body.","tags":["Mentions"],"x-required-scope":"write","parameters":[{"in":"path","name":"sourceId","schema":{"type":"string"},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMentionRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mention"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/mentions/by-id":{"get":{"operationId":"getMentionById","summary":"Get a mention (id as query parameter)","description":"Returns one mention by its stable platform-native `sourceId` (e.g. `reddit_t3_1abc234`), passed as a query parameter. The internal numeric `id` from a `POST /api/v2/mentions` row is also accepted, for recent and archived mentions alike. Shape matches items in `POST /api/v2/mentions`.\n\nQuery-parameter twin of `GET /api/v2/mentions/{sourceId}` — identical behaviour, but safe for ids a URL path segment cannot carry.","tags":["Mentions"],"x-required-scope":"read","parameters":[{"in":"query","name":"sourceId","schema":{"description":"Stable platform-native mention id (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a list row is also accepted — both resolve for recent and archived mentions. Unlike the `/api/v2/mentions/{sourceId}` path form, EVERY id is accepted here verbatim, including ids that are path-reserved (`.`, `..`) or equal to sibling route names (`by-author`, `export`).","example":"reddit_t3_1abc234","type":"string"},"required":true,"description":"Stable platform-native mention id (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a list row is also accepted — both resolve for recent and archived mentions. Unlike the `/api/v2/mentions/{sourceId}` path form, EVERY id is accepted here verbatim, including ids that are path-reserved (`.`, `..`) or equal to sibling route names (`by-author`, `export`)."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mention"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateMentionById","summary":"Update a mention (id as query parameter)","description":"Set the engaged state, or update AI-judged relevance or sentiment, addressing the mention by a `sourceId` query parameter. The `action` field in the body selects the operation — same body as `PATCH /api/v2/mentions/{sourceId}`.\n\nReturns the COMMITTED mention row (same shape as `GET /api/v2/mentions/by-id`), never an echo of the request — e.g. `relevance: 3` (clear) answers with the restored AI verdict, and the `engaged` field is the value the toggle actually stored. No follow-up GET is needed to learn the resulting state.\n\nQuery-parameter twin of the path route — identical behaviour, but safe for ids a URL path segment cannot carry.","tags":["Mentions"],"x-required-scope":"write","parameters":[{"in":"query","name":"sourceId","schema":{"description":"Stable platform-native mention id (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a list row is also accepted — both resolve for recent and archived mentions. Unlike the `/api/v2/mentions/{sourceId}` path form, EVERY id is accepted here verbatim, including ids that are path-reserved (`.`, `..`) or equal to sibling route names (`by-author`, `export`).","example":"reddit_t3_1abc234","type":"string"},"required":true,"description":"Stable platform-native mention id (e.g. `reddit_t3_1abc234`). The internal numeric `id` from a list row is also accepted — both resolve for recent and archived mentions. Unlike the `/api/v2/mentions/{sourceId}` path form, EVERY id is accepted here verbatim, including ids that are path-reserved (`.`, `..`) or equal to sibling route names (`by-author`, `export`)."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMentionRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mention"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/mentions/export":{"post":{"operationId":"exportMentions","summary":"Export mentions","description":"Export up to 50,000 mentions matching the given view and filters. Body shape matches `POST /api/v2/mentions` (v1-compatible) plus a `format` selector, an optional total-row `limit` cap, and an optional `author` pivot (one author's mentions on a single `source`). Returns a downloadable file. Pass `format: \"csv\"` for a CSV or omit / `\"json\"` for a JSON file.","tags":["Mentions"],"x-required-scope":"read","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportMentionsRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportMentionsResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/analytics/sentiment":{"get":{"operationId":"getAnalyticsSentiment","summary":"Sentiment distribution","description":"Returns the count of mentions for each sentiment label (Positive, Neutral, Negative, unknown) within the filter window.","tags":["Analytics"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordIds","schema":{"description":"Limit aggregation to specific keyword IDs.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}]},"description":"Limit aggregation to specific keyword IDs."},{"in":"query","name":"platforms","schema":{"description":"Filter to specific platforms.","anyOf":[{"$ref":"#/components/schemas/Platform"},{"type":"array","items":{"$ref":"#/components/schemas/Platform"}}]},"description":"Filter to specific platforms."},{"in":"query","name":"tag","schema":{"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values.","$ref":"#/components/schemas/MentionTag"},"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values."},{"in":"query","name":"sentiment","schema":{"type":"string","enum":["POSITIVE","NEUTRAL","NEGATIVE"]}},{"in":"query","name":"relevance","schema":{"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1].","anyOf":[{"type":"integer","minimum":0,"maximum":2},{"type":"array","items":{"type":"integer","minimum":0,"maximum":2}}]},"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1]."},{"in":"query","name":"startDate","schema":{"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days."},{"in":"query","name":"endDate","schema":{"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\".","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\"."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SentimentDistributionResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/analytics/volume":{"get":{"operationId":"getAnalyticsVolume","summary":"Mention volume over time","description":"Counts mentions per bucket (day or hour) across the filter window, sorted chronologically.","tags":["Analytics"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordIds","schema":{"description":"Limit aggregation to specific keyword IDs.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}]},"description":"Limit aggregation to specific keyword IDs."},{"in":"query","name":"platforms","schema":{"description":"Filter to specific platforms.","anyOf":[{"$ref":"#/components/schemas/Platform"},{"type":"array","items":{"$ref":"#/components/schemas/Platform"}}]},"description":"Filter to specific platforms."},{"in":"query","name":"tag","schema":{"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values.","$ref":"#/components/schemas/MentionTag"},"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values."},{"in":"query","name":"sentiment","schema":{"type":"string","enum":["POSITIVE","NEUTRAL","NEGATIVE"]}},{"in":"query","name":"relevance","schema":{"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1].","anyOf":[{"type":"integer","minimum":0,"maximum":2},{"type":"array","items":{"type":"integer","minimum":0,"maximum":2}}]},"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1]."},{"in":"query","name":"startDate","schema":{"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days."},{"in":"query","name":"endDate","schema":{"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\".","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\"."},{"in":"query","name":"granularity","schema":{"description":"Bucket size. Default `day`.","default":"day","type":"string","enum":["day","hour"]},"description":"Bucket size. Default `day`."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VolumeResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/analytics/sources":{"get":{"operationId":"getAnalyticsSources","summary":"Source/platform breakdown","description":"Counts mentions grouped by platform (reddit, twitter, linkedin, …) within the filter window, sorted by count desc.","tags":["Analytics"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordIds","schema":{"description":"Limit aggregation to specific keyword IDs.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}]},"description":"Limit aggregation to specific keyword IDs."},{"in":"query","name":"platforms","schema":{"description":"Filter to specific platforms.","anyOf":[{"$ref":"#/components/schemas/Platform"},{"type":"array","items":{"$ref":"#/components/schemas/Platform"}}]},"description":"Filter to specific platforms."},{"in":"query","name":"tag","schema":{"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values.","$ref":"#/components/schemas/MentionTag"},"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values."},{"in":"query","name":"sentiment","schema":{"type":"string","enum":["POSITIVE","NEUTRAL","NEGATIVE"]}},{"in":"query","name":"relevance","schema":{"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1].","anyOf":[{"type":"integer","minimum":0,"maximum":2},{"type":"array","items":{"type":"integer","minimum":0,"maximum":2}}]},"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1]."},{"in":"query","name":"startDate","schema":{"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days."},{"in":"query","name":"endDate","schema":{"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\".","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\"."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceBreakdownResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/analytics/keywords":{"get":{"operationId":"getAnalyticsKeywords","summary":"Per-keyword breakdown","description":"Counts mentions per tracked keyword within the filter window. A post that matched multiple keywords contributes to each. Sorted by count desc.","tags":["Analytics"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordIds","schema":{"description":"Limit aggregation to specific keyword IDs.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}]},"description":"Limit aggregation to specific keyword IDs."},{"in":"query","name":"platforms","schema":{"description":"Filter to specific platforms.","anyOf":[{"$ref":"#/components/schemas/Platform"},{"type":"array","items":{"$ref":"#/components/schemas/Platform"}}]},"description":"Filter to specific platforms."},{"in":"query","name":"tag","schema":{"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values.","$ref":"#/components/schemas/MentionTag"},"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values."},{"in":"query","name":"sentiment","schema":{"type":"string","enum":["POSITIVE","NEUTRAL","NEGATIVE"]}},{"in":"query","name":"relevance","schema":{"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1].","anyOf":[{"type":"integer","minimum":0,"maximum":2},{"type":"array","items":{"type":"integer","minimum":0,"maximum":2}}]},"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1]."},{"in":"query","name":"startDate","schema":{"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days."},{"in":"query","name":"endDate","schema":{"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\".","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\"."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordBreakdownResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/analytics/dashboard":{"get":{"operationId":"getAnalyticsDashboard","summary":"Composite dashboard overview","description":"Volume trend, per-keyword breakdown, sentiment split, and plan usage in one response. Each section matches the corresponding focused endpoint (`/analytics/volume`, `/analytics/keywords`, `/analytics/sentiment`, `/org/usage`) exactly, but the whole dashboard costs one request and one rate-limit token.","tags":["Analytics"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordIds","schema":{"description":"Limit aggregation to specific keyword IDs.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}]},"description":"Limit aggregation to specific keyword IDs."},{"in":"query","name":"platforms","schema":{"description":"Filter to specific platforms.","anyOf":[{"$ref":"#/components/schemas/Platform"},{"type":"array","items":{"$ref":"#/components/schemas/Platform"}}]},"description":"Filter to specific platforms."},{"in":"query","name":"tag","schema":{"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values.","$ref":"#/components/schemas/MentionTag"},"description":"Narrow to mentions carrying this AI-assigned tag. See `MentionTag` for all values."},{"in":"query","name":"sentiment","schema":{"type":"string","enum":["POSITIVE","NEUTRAL","NEGATIVE"]}},{"in":"query","name":"relevance","schema":{"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1].","anyOf":[{"type":"integer","minimum":0,"maximum":2},{"type":"array","items":{"type":"integer","minimum":0,"maximum":2}}]},"description":"Relevance scores to include. 0 = high relevance, 1 = medium, 2 = low. Default: [0, 1]."},{"in":"query","name":"startDate","schema":{"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive start of the window. ISO 8601 datetime. If omitted together with endDate, defaults to the last 30 days."},{"in":"query","name":"endDate","schema":{"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\".","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Exclusive end of the window. ISO 8601 datetime. If omitted together with startDate, defaults to \"now\"."},{"in":"query","name":"granularity","schema":{"description":"Bucket size. Default `day`.","default":"day","type":"string","enum":["day","hour"]},"description":"Bucket size. Default `day`."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsDashboardResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org":{"get":{"operationId":"getOrg","summary":"Get organization","description":"Returns the authenticated workspace: name, plan, enabled platforms, lifecycle timestamps.","tags":["Organization"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Org"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateOrg","summary":"Update organization","description":"Update editable workspace settings. Currently: `name` and `platforms`. Other fields (plan, billing, global filters, company profile, members) have their own endpoints.","tags":["Organization"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Org"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/usage":{"get":{"operationId":"getOrgUsage","summary":"Get usage stats","description":"Mention and keyword counts for the current billing cycle, plus plan limits. Includes flex (usage-based) state when enabled. On the Agents plan a `searches` block reports the lifetime search allowance (used/limit/remaining).","tags":["Organization"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgUsage"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/upgrade-link":{"post":{"operationId":"createUpgradeLink","summary":"Mint an authenticated upgrade link","description":"Mints a **single-use, short-lived** sign-in URL that opens the workspace's upgrade page (Agents plan) or billing page (other plans) already authenticated — no sign-in step, even for workspaces created headlessly that have never had a browser session. The embedded ticket is consumed on first redemption and expires after 10 minutes; replaying it fails. Requires the `write` scope. Every mint is audit-logged with the org, user and request id.","tags":["Organization"],"x-required-scope":"write","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeLinkResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/company":{"get":{"operationId":"getCompany","summary":"Get company profile","description":"Returns the monitored company profile for the authenticated organization.","tags":["Organization"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Company"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateCompany","summary":"Update company profile","description":"Update any combination of the monitored company's fields (name, description, social handles, AI guidelines).","tags":["Organization"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCompanyRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Company"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/feedback":{"post":{"operationId":"submitFeedback","summary":"Submit relevance feedback","description":"Submit thumbs-up / thumbs-down feedback on a mention. Updates Tinybird and adjusts relevance + tags so the feed reflects user intent immediately.","tags":["Feedback"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitFeedbackRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackRecord"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"removeFeedback","summary":"Remove relevance feedback","description":"Remove a stored feedback record by mention key. Always succeeds; `removed` reports whether a record actually existed (`false` = nothing was stored — a harmless no-op).","tags":["Feedback"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveFeedbackRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveFeedbackResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/feedback/stats":{"get":{"operationId":"getFeedbackStats","summary":"Get feedback stats","description":"Aggregated relevance-feedback totals for the workspace: how many mentions were marked `RELEVANT` vs `NOT_RELEVANT`, and the resulting false-positive rate. Pass `keywordId` to narrow the stats to a single keyword.","tags":["Feedback"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordId","schema":{"description":"Narrow stats to one keyword. Omit for org-wide totals.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"Narrow stats to one keyword. Omit for org-wide totals."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackStats"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/ai/filter-wizard":{"post":{"operationId":"generateFilter","summary":"Convert natural language to filters","description":"Uses an LLM to convert a natural-language query (e.g. \"posts from twitter influencers about pricing this week\") into a structured filter object usable with /api/v2/mentions.","tags":["AI"],"x-required-scope":"read","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateFilterRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateFilterResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/ai/monitoring-recommendations":{"get":{"operationId":"getMonitoringRecommendations","summary":"Recommend monitoring settings from search history","description":"Turns the workspace's actual search history into recommended monitoring settings: a ranked keyword set (with rationale and evidence), platform recommendations, and relevance-guideline hints — capped at the target plan's keyword allowance. Workspaces with little or no search history fall back to company-profile-derived suggestions (`source: \"domain\"`); the set is never empty.","tags":["AI"],"x-required-scope":"read","parameters":[{"in":"query","name":"targetPlan","schema":{"description":"Plan the user is upgrading to (default `Starter_v3`, the entry paid tier). Plans without keyword monitoring (e.g. `Agents`) are rejected with `TARGET_PLAN_HAS_NO_KEYWORDS`.","$ref":"#/components/schemas/TargetPlan"},"description":"Plan the user is upgrading to (default `Starter_v3`, the entry paid tier). Plans without keyword monitoring (e.g. `Agents`) are rejected with `TARGET_PLAN_HAS_NO_KEYWORDS`."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitoringRecommendations"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/keywords/suggestions":{"get":{"operationId":"listKeywordSuggestions","summary":"List keyword suggestions","description":"AI-generated tweaks for keyword config. Pass `keywordId` for per-keyword view, or omit for org-wide enriched list.","tags":["Keywords"],"x-required-scope":"read","parameters":[{"in":"query","name":"keywordId","schema":{"description":"Narrow to one keyword. Omit to list org-wide.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"Narrow to one keyword. Omit to list org-wide."},{"in":"query","name":"withVolume","schema":{"description":"Pass the string `\"true\"` or `\"false\"`. When omitted or `\"true\"`, the org-wide `SuggestionsWithVolume` shape is returned. When `\"false\"`, returns the `SuggestionCounts` per-keyword count summary instead.","$ref":"#/components/schemas/QueryBoolean"},"description":"Pass the string `\"true\"` or `\"false\"`. When omitted or `\"true\"`, the org-wide `SuggestionsWithVolume` shape is returned. When `\"false\"`, returns the `SuggestionCounts` per-keyword count summary instead."},{"in":"query","name":"cursor","schema":{"description":"Opaque pagination cursor — pass back `nextCursor` from the previous response. Only applies to the org-wide (`SuggestionsWithVolume`) shape.","type":"string"},"description":"Opaque pagination cursor — pass back `nextCursor` from the previous response. Only applies to the org-wide (`SuggestionsWithVolume`) shape."},{"in":"query","name":"limit","schema":{"description":"Max suggestions to return per page (1–100). Default 25. Only applies to the org-wide (`SuggestionsWithVolume`) shape.","type":"integer","minimum":1,"maximum":100},"description":"Max suggestions to return per page (1–100). Default 25. Only applies to the org-wide (`SuggestionsWithVolume`) shape."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordSuggestionsListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"acceptKeywordSuggestion","summary":"Accept a keyword suggestion","description":"Apply the suggestion to the source keyword. Pass `modifiedValue` to override the suggested text.","tags":["Keywords"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptSuggestionRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestionAccepted"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"rejectKeywordSuggestion","summary":"Reject keyword suggestion(s)","description":"Pass `suggestionId` to reject one suggestion, or `keywordId` to reject all pending suggestions for a keyword. Answers `rejectedCount` and `rejectedSuggestionIds` on both branches, so a bulk dismissal reports what it dismissed rather than a bare success (rejecting is terminal — accept/reject act only on `pending` suggestions).","tags":["Keywords"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectKeywordSuggestionRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestionSuccess"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/members":{"get":{"operationId":"listOrgMembers","summary":"List organization members","description":"Lists all members of the authenticated organization.","tags":["Members"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgMembersListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/members/invite":{"post":{"operationId":"inviteOrgMember","summary":"Invite a member","description":"Sends an email invitation to join the authenticated organization. The invitee must accept the invitation from their email before appearing in `GET /org/members`; until then it is listed by `GET /org/members/invitations`. IDEMPOTENT: re-inviting an address that already has an outstanding invitation returns that invitation with `alreadyInvited: true` and sends no second email, so repeated and concurrent invites cannot stack duplicates.","tags":["Members"],"x-required-scope":"admin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteMemberRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteMemberResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/members/invitations":{"get":{"operationId":"listOrgInvitations","summary":"List pending invitations","description":"Lists invitations that have been sent but not yet accepted. Accepted members appear in `GET /org/members` instead; a pending invitation appears in neither until this endpoint. Revoke one with `DELETE /org/members/{id}`, passing the `orginv_…` id.","tags":["Members"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgInvitationsListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/org/members/{id}":{"delete":{"operationId":"removeOrgMember","summary":"Remove a member or revoke an invitation","description":"Removes a member from the organization. The member loses access immediately. You cannot remove the last remaining admin. Pass an `orginv_…` id instead to REVOKE a pending invitation (OCT-1415) — before that, anything `POST /org/members/invite` created could not be undone through the API at all.","tags":["Members"],"x-required-scope":"admin","parameters":[{"in":"path","name":"id","schema":{"type":"string","minLength":1},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteMemberResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/filters/global":{"get":{"operationId":"getGlobalFilters","summary":"Get global filters","description":"Returns all five org-wide filter lists (negative keywords, authors, subreddits, positive subreddits, repos).","tags":["Filters"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalFilters"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateGlobalFilters","summary":"Update global filters","description":"Replace any of the five filter lists. Pass `[]` to clear a list; omit a field to leave it unchanged.","tags":["Filters"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGlobalFiltersRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalFilters"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/filters/global/add":{"post":{"operationId":"addGlobalFilterValues","summary":"Add values to a global filter list","description":"Append values to one org-wide filter list, merged server-side against the committed row so concurrent callers converge instead of overwriting each other. Values already on the list are skipped silently; values already in the mutually-exclusive counterpart list (`negativeSubreddits` ↔ `positiveSubreddits`) are refused and reported under `dropped`. Returns 409 `FILTER_WRITE_CONFLICT` only if the row stays contended across every retry.","tags":["Filters"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddGlobalFilterValuesRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddGlobalFilterValuesResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/filters/global/remove":{"post":{"operationId":"removeGlobalFilterValues","summary":"Remove values from a global filter list","description":"Remove values from one org-wide filter list, merged server-side against the committed row so a removal is never resurrected by a concurrent add. Values not present are ignored. A removal that would leave the list EMPTY is refused with 400 `CLEAR_NOT_CONFIRMED` unless `allowEmpty: true` is set — emptying a list erases org-wide filter config with no undo.","tags":["Filters"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveGlobalFilterValuesRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveGlobalFilterValuesResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/tags":{"get":{"operationId":"listTags","summary":"List filterable tags","description":"Returns the tag names you can pass to the `tag` / `!tag` filter on `POST /api/v2/mentions`. Result is the union of tags the AI has assigned to this org's mentions and a conventional fallback set, deduped and sorted alphabetically.","tags":["Filters"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/integrations/slack/channels":{"get":{"operationId":"searchSlackChannels","summary":"Search Slack channels","description":"Returns the workspace's accessible Slack channels filtered by name. Designed as a discovery tool for feed-destination configuration — pass returned `id` values to `Destination.slackDestination.channels`. Fails with 400 `SLACK_NOT_CONNECTED` when the Slack integration isn't installed for the workspace, so an EMPTY list unambiguously means 'connected, but nothing matched the query'.","tags":["Integrations"],"x-required-scope":"read","parameters":[{"in":"query","name":"q","schema":{"description":"Case-insensitive substring match against channel name. Omit to list every accessible channel.","example":"releases","type":"string"},"description":"Case-insensitive substring match against channel name. Omit to list every accessible channel."},{"in":"query","name":"cursor","schema":{"description":"Opaque pagination cursor from a previous response's `pagination.nextCursor`.","type":"string"},"description":"Opaque pagination cursor from a previous response's `pagination.nextCursor`."},{"in":"query","name":"pages","schema":{"description":"How many Slack API pages to fetch per call (1–10). Higher values increase latency but reduce client-side pagination. Default 2.","type":"integer","minimum":1,"maximum":10},"description":"How many Slack API pages to fetch per call (1–10). Higher values increase latency but reduce client-side pagination. Default 2."}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackChannelsResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/integrations/slack":{"get":{"operationId":"getSlackStatus","summary":"Slack connection status","description":"Returns whether the workspace has a linked, installed Slack integration. `connected` is `true` only when both hold — poll this after starting the browser OAuth flow to know when the connection is live.","tags":["Integrations"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackIntegrationStatus"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/search":{"get":{"operationId":"listSearches","summary":"List past on-demand searches","description":"Returns the workspace's past on-demand searches, most recent first. Paginated.","tags":["Search"],"x-required-scope":"read","parameters":[{"in":"query","name":"limit","schema":{"default":20,"type":"integer","minimum":1,"maximum":100}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"200 response","headers":{"X-Octolens-Mentions-Remaining":{"description":"Mentions remaining in the workspace's monthly AI-scoring quota after this request. Present on every successful search response (all plans) unless the quota read failed, in which case the header is omitted — never wrong. Use it to self-throttle before hitting the quota wall (`UPGRADE_REQUIRED` / `QUOTA_EXCEEDED`).","schema":{"type":"integer"}},"X-Octolens-Searches-Remaining":{"description":"Lifetime on-demand searches remaining. Present only on plans with a lifetime search cap (the Agents plan); absent elsewhere and when the quota read failed. At 0 the next search answers 403 `UPGRADE_REQUIRED`, whose envelope carries `upgradeUrl` and `upgradeCommand`.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"runSearch","summary":"Run an on-demand mention search","description":"Runs a one-time search for `query` across the workspace's enabled sources over the last 1/7/30 days and AI-scores the results inline using the workspace's company profile. Results belong to the search (they do NOT appear in the mentions feed); new results count against the monthly mention quota, while results the workspace already collected are flagged `alreadyInWorkspace` and are free.\n\nBlocks up to `waitMs` (≤25s). Returns 200 with the full result envelope if it finishes in time, otherwise 202 with a `searchId` + `pollUrl` to poll `GET /api/v2/search/{searchId}`.\n\nResponses carry remaining-quota headers (`X-Octolens-Mentions-Remaining` always; `X-Octolens-Searches-Remaining` on plans with a lifetime search cap) so clients can self-throttle before hitting the quota wall.","tags":["Search"],"x-required-scope":"read","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}}},"responses":{"200":{"description":"200 response","headers":{"X-Octolens-Mentions-Remaining":{"description":"Mentions remaining in the workspace's monthly AI-scoring quota after this request. Present on every successful search response (all plans) unless the quota read failed, in which case the header is omitted — never wrong. Use it to self-throttle before hitting the quota wall (`UPGRADE_REQUIRED` / `QUOTA_EXCEEDED`).","schema":{"type":"integer"}},"X-Octolens-Searches-Remaining":{"description":"Lifetime on-demand searches remaining. Present only on plans with a lifetime search cap (the Agents plan); absent elsewhere and when the quota read failed. At 0 the next search answers 403 `UPGRADE_REQUIRED`, whose envelope carries `upgradeUrl` and `upgradeCommand`.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchCompletedResponse"}}}},"202":{"description":"202 response","headers":{"X-Octolens-Mentions-Remaining":{"description":"Mentions remaining in the workspace's monthly AI-scoring quota after this request. Present on every successful search response (all plans) unless the quota read failed, in which case the header is omitted — never wrong. Use it to self-throttle before hitting the quota wall (`UPGRADE_REQUIRED` / `QUOTA_EXCEEDED`).","schema":{"type":"integer"}},"X-Octolens-Searches-Remaining":{"description":"Lifetime on-demand searches remaining. Present only on plans with a lifetime search cap (the Agents plan); absent elsewhere and when the quota read failed. At 0 the next search answers 403 `UPGRADE_REQUIRED`, whose envelope carries `upgradeUrl` and `upgradeCommand`.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRunningResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/search/{searchId}":{"get":{"operationId":"getSearch","summary":"Get an on-demand search","description":"Returns the current state of a search: `running` (with `partialStats`, a `pollUrl`, and a `Retry-After` header), `completed` (with the full `mentions` array + `stats`), `failed`, or `quota_exhausted`.","tags":["Search"],"x-required-scope":"read","parameters":[{"in":"path","name":"searchId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"200 response","headers":{"X-Octolens-Mentions-Remaining":{"description":"Mentions remaining in the workspace's monthly AI-scoring quota after this request. Present on every successful search response (all plans) unless the quota read failed, in which case the header is omitted — never wrong. Use it to self-throttle before hitting the quota wall (`UPGRADE_REQUIRED` / `QUOTA_EXCEEDED`).","schema":{"type":"integer"}},"X-Octolens-Searches-Remaining":{"description":"Lifetime on-demand searches remaining. Present only on plans with a lifetime search cap (the Agents plan); absent elsewhere and when the quota read failed. At 0 the next search answers 403 `UPGRADE_REQUIRED`, whose envelope carries `upgradeUrl` and `upgradeCommand`.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSearchResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/notifications":{"get":{"operationId":"listNotifications","summary":"List notifications","description":"Returns all notifications. A notification delivers mentions matching a feed's filters to its configured destinations (email, Slack, webhook).","tags":["Notifications"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"createNotification","summary":"Create a notification","description":"Create a notification for an existing feed (a feed can have at most one). Slack destinations are validated against the workspace's Slack connection — every channel id must resolve; webhook URLs must be public http(s) endpoints (a URL whose host is, or resolves to, a private/loopback address is refused, on create and at every delivery).","tags":["Notifications"],"x-required-scope":"write","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNotificationRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/notifications/{id}":{"get":{"operationId":"getNotification","summary":"Get a notification","tags":["Notifications"],"x-required-scope":"read","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateNotification","summary":"Update a notification","description":"Partial update: any combination of name, feed, enabled flag, or destinations. `destinations`, when provided, sets the destination LIST (one per type) — a type in the array is PATCHED onto the stored destination of that type (omitted fields such as `frequency`, `time` or `timezone` keep their current value), and a type absent from the array is removed. Omit `destinations` entirely to leave delivery targets untouched. Pass `enabled: false` to pause delivery.","tags":["Notifications"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationRequest"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"deleteNotification","summary":"Delete a notification","description":"Delete the notification and all its destinations. The linked feed is not affected.","tags":["Notifications"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/notifications/{id}/test":{"post":{"operationId":"testNotification","summary":"Test a notification","description":"Performs a REAL delivery of the latest mention matching the notification's feed filters to every configured destination (email is actually sent, Slack message actually posted, webhook actually called) and returns a per-destination outcome. Watermarks and delivery logs are untouched, so scheduled deliveries are unaffected. Returns `NO_SAMPLE_MENTION` (400) when no mention matches the feed's filters yet.","tags":["Notifications"],"x-required-scope":"write","parameters":[{"in":"path","name":"id","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationTestResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/agency":{"get":{"operationId":"getAgencyOverview","summary":"Agency overview","description":"Plan, shared mentions pool usage, keyword pool, and workspace counts for the agency the master key belongs to. Requires an agency master API key; the `x-octolens-organization-id` header is not needed (agency-scoped).","tags":["Agency"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyOverview"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/agency/workspaces":{"get":{"operationId":"listAgencyWorkspaces","summary":"List agency workspaces","description":"All workspaces of the agency, including per-workspace usage, limit overrides, and flex settings. The returned `organizationId` values are what the `x-octolens-organization-id` header takes on data-API calls.","tags":["Agency"],"x-required-scope":"read","responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyWorkspaceListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"operationId":"createAgencyWorkspace","summary":"Create an agency workspace","description":"Provision a new child workspace: Clerk organization, workspace settings (onboarding skipped), company profile enriched from the domain, and superadmin access for every agency superadmin. Fails with 403 AGENCY_WORKSPACE_LIMIT at the workspace cap. `companyEnriched: false` means enrichment failed and the profile needs manual completion.","tags":["Agency"],"x-required-scope":"admin","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":191},"domain":{"type":"string","minLength":1,"maxLength":191}},"required":["name","domain"]}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgencyWorkspaceResult"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/agency/workspaces/{orgId}":{"get":{"operationId":"getAgencyWorkspace","summary":"Get an agency workspace","tags":["Agency"],"x-required-scope":"read","parameters":[{"in":"path","name":"orgId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyWorkspace"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"operationId":"updateAgencyWorkspace","summary":"Update workspace limits / flex","description":"Partial update: only the provided concerns are touched. `mentionsLimitOverride` / `keywordsLimitOverride` accept `null` to clear the override (back to the shared pool / plan default). Enabling flex requires a saved mentions cap, a positive budget, and configured agency Stripe billing (412 `AGENCY_BILLING_NOT_CONFIGURED` otherwise). Returns the fresh workspace.","tags":["Agency"],"x-required-scope":"admin","parameters":[{"in":"path","name":"orgId","schema":{"type":"string"},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAgencyWorkspace"}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyWorkspace"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"412":{"description":"412 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"deleteAgencyWorkspace","summary":"Delete an agency workspace","description":"Permanently removes the workspace (settings first, then the Clerk organization). The anchor workspace cannot be deleted. `clerkOrgDeleted: false` means the workspace was removed from the agency but the Clerk organization needs out-of-band cleanup.","tags":["Agency"],"x-required-scope":"admin","parameters":[{"in":"path","name":"orgId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAgencyWorkspaceResult"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/agency/workspaces/{orgId}/members":{"get":{"operationId":"listAgencyWorkspaceMembers","summary":"List workspace members","description":"Current members, pending invitations, and queued access grants (bulk-invite overflow awaiting account creation) of one agency workspace.","tags":["Agency"],"x-required-scope":"read","parameters":[{"in":"path","name":"orgId","schema":{"type":"string"},"required":true}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyWorkspaceMembers"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/agency/members/invite":{"post":{"operationId":"inviteAgencyMembers","summary":"Invite members to workspaces","description":"Invite one or many emails to one/many/all agency workspaces (`targetOrgIds: \"all\"`). Existing Octolens accounts get direct membership grants (no email — `action: granted`). New users receive exactly ONE invitation email for their first selected workspace (`action: invited`); every additional workspace is queued (`action: queued`) and granted automatically once the account exists. Combined fan-out (emails × workspaces) is capped at 200 per call.","tags":["Agency"],"x-required-scope":"admin","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"targetOrgIds":{"anyOf":[{"type":"string","const":"all"},{"minItems":1,"type":"array","items":{"type":"string","maxLength":191}}]},"emails":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"role":{"default":"org:member","type":"string","enum":["org:member","org:admin"]}},"required":["targetOrgIds","emails"]}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyInviteResult"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v2/agency/members/remove":{"post":{"operationId":"removeAgencyMember","summary":"Remove a member from workspaces","description":"Remove a user from one/many/all agency workspaces (`orgIds: \"all\"`). Superadmins cannot be removed this way — demote them via the agency admin panel first.","tags":["Agency"],"x-required-scope":"admin","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"orgIds":{"anyOf":[{"type":"string","const":"all"},{"minItems":1,"type":"array","items":{"type":"string","maxLength":191}}]},"userId":{"type":"string"}},"required":["orgIds","userId"]}}}},"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyRemoveMemberResult"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid authentication","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden (insufficient plan or permissions)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"schemas":{"QueryBoolean":{"description":"Boolean query parameter — the string \"true\" or \"false\".","example":"false","type":"string","enum":["true","false"]},"CreateFeedRequest":{"description":"Create a new feed. Pass either `simpleFilters` or `advancedFilters` (or neither for a match-all feed).","type":"object","properties":{"name":{"description":"Feed display name. Max 191 characters, no control characters.","type":"string","minLength":1,"maxLength":191},"icon":{"$ref":"#/components/schemas/FeedIcon"},"simpleFilters":{"$ref":"#/components/schemas/SimpleFilters"},"advancedFilters":{"$ref":"#/components/schemas/AdvancedFilters"},"destinations":{"description":"Notification targets. Omit for a filter-only feed.","type":"array","items":{"$ref":"#/components/schemas/Destination"}}},"required":["name","icon"]},"FeedIcon":{"description":"Icon identifier for UI display (Heroicons outline set).","example":"BellIcon","type":"string","enum":["InboxStackIcon","HeartIcon","AtSymbolIcon","ExclamationTriangleIcon","MegaphoneIcon","ArrowTrendingUpIcon","BuildingOffice2Icon","ArrowTrendingDownIcon","BellIcon","BuildingLibraryIcon","ChartBarIcon","ChatBubbleLeftIcon","ChatBubbleLeftRightIcon","CheckCircleIcon","CloudIcon","CodeBracketIcon","ComputerDesktopIcon","CursorArrowRaysIcon","DevicePhoneMobileIcon","DocumentIcon","DocumentTextIcon","EnvelopeIcon","EyeIcon","FaceSmileIcon","FaceFrownIcon","FireIcon","FlagIcon","FolderIcon","GlobeAltIcon","GlobeAmericasIcon","HandThumbUpIcon","HashtagIcon","InboxArrowDownIcon","LightBulbIcon","PaperClipIcon","RocketLaunchIcon","RssIcon","SignalIcon","SparklesIcon","StarIcon","TrophyIcon","UserGroupIcon","WalletIcon"]},"SimpleFilters":{"description":"Simple AND-combined filter list.","type":"object","properties":{"conditions":{"description":"Flat list of conditions, implicitly combined with AND (max 50).","maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/SimpleFilterCondition"}}},"required":["conditions"]},"SimpleFilterCondition":{"type":"object","properties":{"field":{"description":"Field name to filter on. One of: `Keywords`, `Source`, `Sentiment`, `Language`, `Tags`, `RelevanceScore`, `Engaged`, `Bookmarked`, `RelevantOnly`, `TimeRange`, `TwitterFollowerCount`. Field names are case-sensitive (`Keywords`, not `Keyword`/`keywords`).","example":"Keywords","type":"string","enum":["Keywords","Source","Sentiment","Language","Tags","RelevanceScore","Engaged","Bookmarked","RelevantOnly","TimeRange","TwitterFollowerCount"]},"values":{"description":"Values to match. Most fields accept comma-separated values (IDs for `Keywords`, lowercase platform slugs for `Source` — e.g. `reddit,twitter` — and Title-case enum values for `Sentiment` — `Positive`, `Neutral`, `Negative`). `Source` and `Sentiment` values are normalized to their canonical case on write. Simple conditions always match inclusively (`IN`); to exclude values use an advanced filter with the `not in` operator.","example":"42,43","type":"string","maxLength":2000}},"required":["field","values"]},"AdvancedFilters":{"description":"Nested filter expression — groups of conditions combined with AND/OR.","type":"object","properties":{"top_level_operator":{"description":"How the groups combine at the top level.","type":"string","enum":["AND","OR"]},"groups":{"description":"One or more filter groups (max 50).","maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/AdvancedFilterGroup"}}},"required":["top_level_operator","groups"]},"AdvancedFilterGroup":{"type":"object","properties":{"group_operator":{"description":"How conditions *within* this group combine.","type":"string","enum":["AND","OR"]},"conditions":{"description":"Conditions inside this group (max 50).","maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/AdvancedFilterCondition"}},"isSingleCondition":{"description":"UI hint. When `true`, the group was created from a single condition in the builder.","type":"boolean"}},"required":["group_operator","conditions"]},"AdvancedFilterCondition":{"type":"object","properties":{"field":{"description":"Field name to filter on. One of: `Keywords`, `Source`, `Sentiment`, `Language`, `Tags`, `RelevanceScore`, `Engaged`, `Bookmarked`, `RelevantOnly`, `TimeRange`, `TwitterFollowerCount`. Field names are case-sensitive (`Keywords`, not `Keyword`/`keywords`).","type":"string","enum":["Keywords","Source","Sentiment","Language","Tags","RelevanceScore","Engaged","Bookmarked","RelevantOnly","TimeRange","TwitterFollowerCount"]},"operator":{"description":"Comparison operator. `in`/`not in` work with comma-separated `values`; `equals`/`=`/`>=`/`<=` treat `values` as a single scalar.","type":"string","enum":["in","not in","equals",">=","<=","="]},"values":{"description":"Comma-separated values for `in`/`not in`, single value for comparisons.","example":"42,43","type":"string","maxLength":2000}},"required":["field","operator","values"]},"Destination":{"description":"One delivery configuration. The relevant `*Destination` sub-object is populated based on `type`.","type":"object","properties":{"type":{"$ref":"#/components/schemas/DestinationType"},"frequency":{"$ref":"#/components/schemas/DestinationFrequency"},"deliveryMode":{"description":"`batch` sends one digest per period; `individual` sends one message per matching mention. Ignored on `WEBHOOK` (always individual).","type":"string","enum":["batch","individual"]},"time":{"description":"Wall-clock time for daily/weekly delivery, `HH:mm` 24-hour, zero-padded (`00:00`–`23:59`). Requests with any other format are rejected.","example":"09:00","type":"string","maxLength":16},"timezone":{"description":"IANA timezone (e.g. `Europe/Madrid`) or UTC offset (`UTC+2:00`) paired with `time`. Requests with unknown timezones are rejected.","example":"UTC","type":"string","maxLength":64},"dayOfWeek":{"description":"0 (Sunday) through 6 (Saturday). Required for `frequency=weekly`.","type":"integer","minimum":0,"maximum":6},"emailDestination":{"description":"Present when `type=EMAIL`.","type":"object","properties":{"emails":{"$ref":"#/components/schemas/EmailList"}},"required":["emails"]},"slackDestination":{"description":"Present when `type=SLACK`.","type":"object","properties":{"channels":{"description":"Comma-separated Slack channel IDs, e.g. `C0123ABC,C0456DEF`. Max 191 characters (the column width).","type":"string","minLength":1,"maxLength":191},"channelNamesMap":{"description":"Optional comma-separated `id:name` pairs matching `channels` — used for UI display.","type":"string","maxLength":8000}},"required":["channels"]},"webhookDestination":{"description":"Present when `type=WEBHOOK`.","type":"object","properties":{"url":{"example":"https://example.com/webhook","description":"HTTP(S) URL to POST events to.","$ref":"#/components/schemas/HttpUrl"}},"required":["url"]}},"required":["type","frequency"]},"DestinationType":{"type":"string","enum":["EMAIL","SLACK","WEBHOOK"]},"DestinationFrequency":{"description":"Delivery cadence. `hourly` fires on a rolling window; `hourlyAtTopOfHour` on the hour; `daily`/`weekly` use `time`+`timezone` (and `dayOfWeek` for weekly).","type":"string","enum":["hourly","hourlyAtTopOfHour","daily","weekly"]},"EmailList":{"description":"Comma-separated email addresses.","example":"alice@example.com, bob@example.com","type":"string","maxLength":191},"HttpUrl":{"example":"https://example.com/webhook","type":"string","format":"uri"},"UpdateFeedRequest":{"description":"Partial update. Passing `simpleFilters` or `advancedFilters` replaces that side of the filter; passing `null` clears it (switch a feed between simple and advanced by sending the new side and `null` on the old one). Destinations are all-or-nothing — include the full list to update any of them.","type":"object","properties":{"name":{"description":"Replace feed name. Max 191 characters.","type":"string","minLength":1,"maxLength":191},"icon":{"$ref":"#/components/schemas/FeedIcon"},"simpleFilters":{"anyOf":[{"$ref":"#/components/schemas/SimpleFilters"},{"type":"null"}]},"advancedFilters":{"anyOf":[{"$ref":"#/components/schemas/AdvancedFilters"},{"type":"null"}]},"destinations":{"description":"If provided, replaces all destinations. If omitted, destinations are unchanged.","type":"array","items":{"$ref":"#/components/schemas/Destination"}}}},"CreateKeywordRequest":{"description":"Create a new monitored keyword. Triggers an immediate first data-stream run.","type":"object","properties":{"keyword":{"description":"The phrase to track.","example":"acme corp","type":"string","minLength":1,"maxLength":100},"context":{"description":"Short sentence describing what this keyword is tracking. Used for AI relevance scoring. If omitted, a context sentence is auto-generated from your company profile.","type":"string","maxLength":200},"additionalTerms":{"description":"Comma-separated terms required alongside the main keyword.","example":"deployment, preview","type":"string","maxLength":500},"additionalTermsAndOr":{"description":"If `true` (default), any one of the additional terms must appear alongside the keyword (OR). If `false`, all of them must appear (AND). Matches the app UI.","default":true,"type":"boolean"},"caseSensitive":{"description":"If `true`, matching is case-sensitive.","default":false,"type":"boolean"},"symbolSensitive":{"description":"Exact-match mode. If `true` (default), the keyword is matched as a whole phrase — for multi-word keywords, the words must appear together. If `false`, each word can appear anywhere in the post, which typically produces noisier results. Matches the app UI's \"Exact match\" toggle.","default":true,"type":"boolean"},"platforms":{"description":"Platforms to monitor. Optional — when omitted, the server defaults to every plan-allowed source. Pass an explicit subset only to narrow the monitoring (e.g. Reddit + Twitter only).","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"excludeWords":{"description":"Comma-separated exact-match exclusions.","type":"string","maxLength":1000},"wildcardExcludeWords":{"description":"Comma-separated wildcard exclusion patterns (use `*`).","type":"string","maxLength":1000},"excludeAuthors":{"description":"Comma-separated author handles to filter out.","type":"string","maxLength":1000},"tag":{"description":"Optional. When omitted, the AI classifies the keyword from the company profile (`own_brand` / `competitor` / `industry_term`) and persists it alongside the rest of the AI enrichment (`context`, `excludeWords`, `additionalTerms`). Pass an explicit value only when the user has already classified it.","$ref":"#/components/schemas/KeywordTag"},"isSubReddit":{"description":"Set to `true` if this keyword represents a subreddit rather than a free-text phrase.","type":"boolean"},"allowDuplicate":{"description":"By default, creating a keyword whose term exactly matches an already-tracked keyword (case-insensitive) is a no-op: the existing keyword is returned with `alreadyExisted: true` and nothing is created. Pass `true` to force creating a second keyword with the same term.","type":"boolean"}},"required":["keyword"]},"Platform":{"description":"Platform a mention can originate from / a keyword can be monitored on.","example":"reddit","type":"string","enum":["dev","github","hackernews","linkedin","producthunt","reddit","stackoverflow","twitter","youtube","tiktok","medium","reddit_comment","bluesky","newsletter","podcasts","news","firehose"]},"KeywordTag":{"description":"Classification of what this keyword represents. Used to route posts to brand / competitor / industry-context workflows and pick the right AI prompt.","example":"own_brand","type":"string","enum":["own_brand","competitor","industry_term"]},"UpdateKeywordRequest":{"description":"Partial update. Omitted fields are left unchanged. Pass an empty string to clear a text-list field; pass an empty array to require explicit platform reset (not allowed).","type":"object","properties":{"context":{"description":"Replace the relevance-context sentence.","type":"string","maxLength":200},"additionalTerms":{"description":"Replace comma-separated additional terms. Empty string clears.","type":"string","maxLength":500},"additionalTermsAndOr":{"description":"OR (`true`) or AND (`false`) for additional terms.","type":"boolean"},"caseSensitive":{"description":"Toggle case-sensitive matching.","type":"boolean"},"symbolSensitive":{"description":"Toggle \"exact match\" mode.","type":"boolean"},"platforms":{"description":"Replace the platforms list. Must be a non-empty subset of plan-allowed sources.","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"excludeWords":{"description":"Replace comma-separated exact-match exclusions. Empty string clears.","type":"string","maxLength":1000},"wildcardExcludeWords":{"description":"Replace comma-separated wildcard exclusions. Empty string clears.","type":"string","maxLength":1000},"excludeAuthors":{"description":"Replace comma-separated excluded author handles. Empty string clears.","type":"string","maxLength":1000},"tag":{"$ref":"#/components/schemas/KeywordTag"}}},"PauseKeywordRequest":{"description":"The paused state to move a keyword to.","type":"object","properties":{"paused":{"description":"Target state: `true` pauses data collection, `false` resumes it. The write is absolute and idempotent — sending the state the keyword is already in changes nothing and answers `changed: false`.","example":true,"type":"boolean"}},"required":["paused"]},"ListMentionsRequest":{"description":"Filter and paginate mentions. Either provide `view` to reuse an existing feed's filters, or supply `filters` directly (or both - they merge). Body shape matches v1 `/api/v1/mentions` so v1 clients can reuse the same request bodies. Date ranges live inside `filters.startDate` / `filters.endDate` (ISO 8601).","type":"object","properties":{"view":{"description":"Feed/view ID to reuse as a base filter. If `filters` is also provided, the two are merged (view's saved filters + inline `filters` both apply).","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"filters":{"description":"Filter object. Accepts the simple flat form (`{ source: [\"twitter\"], sentiment: [\"positive\"] }`) or the advanced group form with AND/OR operators. Use `POST /api/v2/ai/filter-wizard` to generate one from natural language.","$ref":"#/components/schemas/ApiFilters"},"includeAll":{"description":"When `true`, includes low-relevance mentions (internal `relevanceScore` = 2). Default `false` returns only high and medium relevance (scores 0 and 1).","default":false,"type":"boolean"},"includeRelevantOnly":{"description":"When `true`, forces relevant-only results (scores 0 and 1) even if `includeAll` is `true`. Convenience parity with the in-app feed toggle; omit to defer to `includeAll`.","type":"boolean"},"limit":{"description":"Page size, 1-100.","default":20,"type":"integer","minimum":1,"maximum":100},"cursor":{"description":"Opaque cursor from a previous response's `pagination.nextCursor`.","type":"string"},"search":{"description":"Free-text search. Case-insensitive substring match across the mention's title, body, author handle, and author display name. Composes with `view`/`filters` (AND semantics). Empty or whitespace-only values are ignored.","type":"string","maxLength":200}}},"ApiFilters":{"description":"Mention filter object. Accepts the simple flat form (`{ source: [...] }`) or the advanced group form (`{ operator: \"AND\", groups: [...] }`). Matches the shape returned by `POST /api/v2/ai/filter-wizard`, so the wizard output plugs straight into this field. `endDate` must be on or after `startDate`.","anyOf":[{"$ref":"#/components/schemas/SimpleApiFilters"},{"$ref":"#/components/schemas/AdvancedApiFilters"}]},"SimpleApiFilters":{"description":"Flat `field: values` map. Prefix a field with `!` to invert (NOT IN). Keys you can use: source / sentiment / keyword / language / tag / relevance (arrays); minXFollowers / maxXFollowers (numbers); startDate / endDate (ISO 8601 strings). Unknown keys are rejected with a 400.","type":"object","properties":{"source":{"description":"Filter by source platforms. Prefix with ! to exclude.","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"sentiment":{"description":"Filter by sentiment. Prefix with ! to exclude.","minItems":1,"type":"array","items":{"type":"string","enum":["positive","neutral","negative"]}},"keyword":{"description":"Filter by specific keyword IDs (numeric). Prefix with ! to exclude.","minItems":1,"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"language":{"description":"Filter by language codes (ISO 639-1). Prefix with ! to exclude.","minItems":1,"type":"array","items":{"type":"string","enum":["en","es","fr","de","pt","it","nl","ja","ko","zh"]}},"tag":{"description":"Filter by tags. Prefix with ! to exclude.","minItems":1,"maxItems":200,"type":"array","items":{"anyOf":[{"type":"string","maxLength":200},{"type":"number"}]}},"relevance":{"description":"Filter by relevance buckets. Prefix with ! to exclude.","minItems":1,"maxItems":200,"type":"array","items":{"anyOf":[{"type":"string","maxLength":200},{"type":"number"}]}},"!source":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"!sentiment":{"minItems":1,"type":"array","items":{"type":"string","enum":["positive","neutral","negative"]}},"!keyword":{"minItems":1,"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"!language":{"minItems":1,"type":"array","items":{"type":"string","enum":["en","es","fr","de","pt","it","nl","ja","ko","zh"]}},"!tag":{"minItems":1,"maxItems":200,"type":"array","items":{"anyOf":[{"type":"string","maxLength":200},{"type":"number"}]}},"!relevance":{"minItems":1,"maxItems":200,"type":"array","items":{"anyOf":[{"type":"string","maxLength":200},{"type":"number"}]}},"minXFollowers":{"description":"Minimum X/Twitter follower count for the author. Follower counts only exist for X/Twitter authors, so this also scopes results to `source: twitter` (OCT-1422) — mentions from other platforms are excluded rather than returned unchecked.","type":"integer","minimum":0,"maximum":9007199254740991},"maxXFollowers":{"description":"Maximum X/Twitter follower count for the author. Also scopes results to `source: twitter` — see `minXFollowers`.","type":"integer","minimum":0,"maximum":9007199254740991},"startDate":{"description":"Only return posts from this date onwards (ISO 8601).","type":"string","maxLength":64},"endDate":{"description":"Only return posts up to this date (ISO 8601).","type":"string","maxLength":64}},"additionalProperties":false},"AdvancedApiFilters":{"description":"Groups of conditions combined with AND/OR. Use this shape when a simple field-to-values map isn't enough (e.g. mixed OR logic across fields).","type":"object","properties":{"operator":{"description":"How groups combine at the top level.","type":"string","enum":["AND","OR"]},"groups":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/ApiAdvancedGroup"}},"minXFollowers":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxXFollowers":{"type":"integer","minimum":0,"maximum":9007199254740991},"startDate":{"description":"Inclusive ISO 8601 lower bound applied to all groups.","type":"string","maxLength":64},"endDate":{"description":"Inclusive ISO 8601 upper bound applied to all groups.","type":"string","maxLength":64}},"required":["operator","groups"],"additionalProperties":false},"ApiAdvancedGroup":{"type":"object","properties":{"operator":{"description":"How conditions inside this group combine.","default":"AND","type":"string","enum":["AND","OR"]},"conditions":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/ApiAdvancedCondition"}}},"required":["conditions"]},"ApiAdvancedCondition":{"description":"A single condition inside an advanced filter group. Object with exactly one key.","anyOf":[{"$ref":"#/components/schemas/ApiArrayCondition"},{"$ref":"#/components/schemas/ApiNumericCondition"},{"$ref":"#/components/schemas/ApiDateCondition"}]},"ApiArrayCondition":{"type":"object","propertyNames":{"type":"string","maxLength":200},"additionalProperties":{"minItems":1,"maxItems":200,"type":"array","items":{"anyOf":[{"type":"string","maxLength":200},{"type":"number"}]}}},"ApiNumericCondition":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":0,"maximum":9007199254740991}},"ApiDateCondition":{"type":"object","propertyNames":{"type":"string","maxLength":64},"additionalProperties":{"type":"string","maxLength":64}},"UpdateMentionRequest":{"description":"Mutation request for a mention. The `action` field discriminates which update to apply.","oneOf":[{"type":"object","properties":{"action":{"description":"Set the engaged-with flag on the mention. Pass `engaged` for an absolute, idempotent write; omit it to TOGGLE the current value.","type":"string","const":"engage"},"postId":{"description":"Internal post id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"timestamp":{"description":"The mention's timestamp, copied verbatim from the list/export response. Tinybird-style format (`YYYY-MM-DD HH:mm:ss.SSS`, UTC, no `Z` suffix); ISO 8601 also accepted. Used together with `sourceId` / `postId` to uniquely key the mention.","example":"2026-05-06 13:35:37.000","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:?\\d{2})?$"},"engaged":{"description":"Target state: `true` marks the mention engaged, `false` un-marks it. The write is absolute and idempotent — safe to retry and safe to call concurrently. OMITTING it toggles the current value, which is what the web app's engage button does; non-interactive callers should always pass it.","example":true,"type":"boolean"}},"required":["action","postId","timestamp"]},{"type":"object","properties":{"action":{"description":"Set the AI relevance score manually (user override).","type":"string","const":"relevance"},"postId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"relevance":{"description":"0 = high, 1 = medium, 2 = low, 3 = clear (reset).","type":"integer","minimum":0,"maximum":3},"timestamp":{"description":"The mention's timestamp, copied verbatim from the list/export response. Tinybird-style format (`YYYY-MM-DD HH:mm:ss.SSS`, UTC, no `Z` suffix); ISO 8601 also accepted. Used together with `sourceId` / `postId` to uniquely key the mention.","example":"2026-05-06 13:35:37.000","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:?\\d{2})?$"}},"required":["action","postId","relevance","timestamp"]},{"type":"object","properties":{"action":{"description":"Set the sentiment label manually (user override).","type":"string","const":"sentiment"},"timestamp":{"description":"The mention's timestamp, copied verbatim from the list/export response. Tinybird-style format (`YYYY-MM-DD HH:mm:ss.SSS`, UTC, no `Z` suffix); ISO 8601 also accepted. Used together with `sourceId` / `postId` to uniquely key the mention.","example":"2026-05-06 13:35:37.000","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:?\\d{2})?$"},"sentimentLabel":{"$ref":"#/components/schemas/MentionSentiment"}},"required":["action","timestamp","sentimentLabel"]}],"type":"object"},"MentionSentiment":{"description":"AI-judged sentiment of the mention.","example":"Neutral","type":"string","enum":["Positive","Neutral","Negative"]},"ExportMentionsRequest":{"description":"Same filter shape as `ListMentionsRequest` plus a `format` selector, an optional total-row `limit` cap, and an optional `author` pivot. Exports up to 50,000 mentions per call and returns the result as a downloadable file.","type":"object","properties":{"view":{"description":"Feed/view ID to reuse as a base filter. If `filters` is also provided, the two are merged (view's saved filters + inline `filters` both apply).","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"filters":{"description":"Filter object. Accepts the simple flat form (`{ source: [\"twitter\"], sentiment: [\"positive\"] }`) or the advanced group form with AND/OR operators. Use `POST /api/v2/ai/filter-wizard` to generate one from natural language.","$ref":"#/components/schemas/ApiFilters"},"includeAll":{"description":"When `true`, includes low-relevance mentions (internal `relevanceScore` = 2). Default `false` returns only high and medium relevance (scores 0 and 1).","default":false,"type":"boolean"},"includeRelevantOnly":{"description":"When `true`, forces relevant-only results (scores 0 and 1) even if `includeAll` is `true`. Convenience parity with the in-app feed toggle; omit to defer to `includeAll`.","type":"boolean"},"cursor":{"description":"Opaque cursor from a previous response's `pagination.nextCursor`.","type":"string"},"search":{"description":"Free-text search. Case-insensitive substring match across the mention's title, body, author handle, and author display name. Composes with `view`/`filters` (AND semantics). Empty or whitespace-only values are ignored.","type":"string","maxLength":200},"format":{"default":"json","$ref":"#/components/schemas/ExportMentionsFormat"},"limit":{"description":"Cap the export at the first N matching mentions (1-50,000). A TOTAL row cap, not a page size — the export paginates internally. Omit for the full 50,000-row cap.","type":"integer","minimum":1,"maximum":50000},"author":{"description":"Export one author's mentions instead of the filtered feed (the by-author lookup): the author's handle, or for `source: [\"linkedin\"]` a profile URL/slug. Requires `filters` to contain exactly one `source` platform and nothing else, and cannot be combined with `view` or `search` — the by-author lookup honors none of them.","type":"string","minLength":1,"maxLength":500}}},"ExportMentionsFormat":{"description":"Response format. `json` returns a downloadable JSON file (`{data, total}`); `csv` returns a CSV file with one row per mention across 15 columns.","example":"csv","type":"string","enum":["json","csv"]},"MentionTag":{"description":"AI-assigned topic tag. Applied by the relevance worker during scoring. A post can carry multiple tags.","example":"competitor_mention","type":"string","enum":["buy_intent","competitor_mention","customer_testimonial","promotional_post","own_brand_mention","industry_insights","hiring","event","product_question","pricing","churn_intent","launch_announcement","bug_report","user_feedback","ai_generated"]},"UpdateOrgRequest":{"description":"Partial update of the workspace. Omitted fields are unchanged. Other org concerns (plan, billing, global filters, company profile, members) have their own endpoints.","type":"object","properties":{"name":{"description":"New workspace name. Max 191 characters, no control characters.","type":"string","minLength":1,"maxLength":191},"platforms":{"$ref":"#/components/schemas/OrgPlatforms"}}},"OrgPlatforms":{"description":"`\"all\"` means every supported platform is enabled org-wide. Otherwise a list of Platform enum values.","example":"all","anyOf":[{"type":"string","const":"all"},{"type":"array","items":{"$ref":"#/components/schemas/Platform"}}]},"UpdateCompanyRequest":{"description":"Partial update of the monitored company profile. Omitted fields are unchanged.","type":"object","properties":{"name":{"description":"Display name. Max 191 characters.","type":"string","minLength":1,"maxLength":191},"description":{"description":"Prose description of the company. Max 10000 characters.","type":"string","maxLength":10000},"twitter":{"description":"X/Twitter handle(s), comma-separated, no `@`. Max 191 characters. Pass an empty string to clear.","type":"string","maxLength":191},"linkedin":{"description":"LinkedIn company slug. Max 191 characters. Pass an empty string to clear.","type":"string","maxLength":191},"logo":{"description":"URL to a public logo image. Must be an absolute `http(s)` URL.","type":"string","format":"uri"},"productUseCases":{"description":"Free-text list (often newline-separated) of use cases. Max 10000 characters.","type":"string","maxLength":10000},"relevanceContext":{"description":"Short disambiguation sentence (≤400 chars). Injected into every relevance prompt.","type":"string","maxLength":400},"relevanceGuidelines":{"description":"Extra guidance injected into the relevance-scoring prompt. Max 10000 characters.","type":"string","maxLength":10000},"classificationGuidelines":{"description":"Extra guidance injected into the post-classification prompt. Max 10000 characters.","type":"string","maxLength":10000},"competitors":{"description":"JSON-encoded competitors list — an array of `{\"name\": string, \"description\"?: string}`. Pass an empty string to clear.","type":"string","maxLength":20000},"companyMoat":{"description":"Prose description of competitive advantage. Max 10000 characters.","type":"string","maxLength":10000}}},"SubmitFeedbackRequest":{"description":"Submit or upsert a relevance feedback record for a mention.","type":"object","properties":{"sourceId":{"description":"Mention's `sourceId` (e.g. `reddit_t3_1abc234`).","example":"reddit_t3_1abc234","type":"string"},"timestamp":{"description":"Mention's timestamp, copied verbatim from the list/export response. Tinybird-style format (`YYYY-MM-DD HH:mm:ss.SSS`, UTC, no `Z` suffix); ISO 8601 also accepted. Required to uniquely key the feedback row.","example":"2026-05-06 13:35:37.000","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:?\\d{2})?$"},"postId":{"description":"Optional internal post id for extra joinability.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keywordId":{"description":"ID of the keyword the mention matched (from `Keyword.id`).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"source":{"description":"Platform source (e.g. `reddit`, `twitter`).","example":"reddit","type":"string","minLength":1,"maxLength":64},"feedbackType":{"$ref":"#/components/schemas/FeedbackType"},"feedbackReason":{"description":"Optional free-text reason shown to the relevance agent as extra context.","type":"string","maxLength":4000},"feedbackSource":{"default":"API","$ref":"#/components/schemas/FeedbackSource"},"originalRelevanceScore":{"description":"What the AI originally scored the mention (0=high, 1=medium, 2=low). Captured for analytics.","type":"number"}},"required":["sourceId","timestamp","keywordId","source","feedbackType"]},"FeedbackType":{"description":"The user's verdict on the mention. `RELEVANT` = the mention matters, `NOT_RELEVANT` = it's noise.","type":"string","enum":["RELEVANT","NOT_RELEVANT"]},"FeedbackSource":{"description":"Where the feedback came from. `WEB` = app UI, `SLACK` = Slack notification buttons, `API` = programmatic (REST v2). Must match the `FeedbackSource` enum in the database.","example":"API","type":"string","enum":["WEB","SLACK","API"]},"RemoveFeedbackRequest":{"description":"Remove an existing feedback record by mention key.","type":"object","properties":{"sourceId":{"description":"Mention's `sourceId`.","type":"string"},"timestamp":{"description":"Mention's timestamp — composite key with `sourceId`. Pass the value as returned by the list endpoint (Tinybird-style `YYYY-MM-DD HH:mm:ss.SSS`); ISO 8601 also accepted.","example":"2026-05-06 13:35:37.000","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:?\\d{2})?$"}},"required":["sourceId","timestamp"]},"GenerateFilterRequest":{"description":"Convert a natural-language query into an Octolens filter object.","type":"object","properties":{"query":{"description":"Natural-language description of what the user wants to see.","example":"only negative tweets about pricing from the last 7 days","type":"string","minLength":1,"maxLength":1000},"target":{"description":"Which filter contract to return (default `mentions`). `mentions` returns the flat filter map accepted by `POST /api/v2/mentions` `filters`. `feed` returns the `SimpleFilters`/`AdvancedFilters` shape accepted verbatim by the feed endpoints (`POST /api/v2/feeds` / `PATCH /api/v2/feeds/{id}`) — the two contracts are not interchangeable.","type":"string","enum":["mentions","feed"]}},"required":["query"]},"TargetPlan":{"description":"The plan the recommendations are sized for — keyword recommendations are capped at that plan's monthly keyword allowance.","example":"Starter_v3","type":"string","enum":["Individual","Start_up","Scale_up","AppSumoTier1","AppSumoTier2","AppSumoTier3","Free_trial","Basic","Pro","Max","Starter","Scale","Pro_v2","Enterprise","Starter_v3","Pro_v3","Agency","Agents"]},"AcceptSuggestionRequest":{"description":"Apply a suggestion to the keyword's config.","type":"object","properties":{"suggestionId":{"description":"Suggestion id to accept.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"modifiedValue":{"description":"Override the suggested value before applying it — useful when the user wants to accept the direction but tweak the exact text.","type":"string","maxLength":2000}},"required":["suggestionId"]},"RejectKeywordSuggestionRequest":{"description":"Reject a single suggestion or all suggestions for a keyword.","anyOf":[{"$ref":"#/components/schemas/RejectSuggestionRequest"},{"$ref":"#/components/schemas/RejectAllRequest"}]},"RejectSuggestionRequest":{"description":"Dismiss a single suggestion without applying it.","type":"object","properties":{"suggestionId":{"description":"Suggestion id to reject.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["suggestionId"]},"RejectAllRequest":{"type":"object","properties":{"keywordId":{"description":"Dismiss every pending suggestion for this keyword.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["keywordId"]},"InviteMemberRequest":{"description":"Send an email invitation to join the authenticated organization.","type":"object","properties":{"email":{"description":"Address to send the invitation to.","type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"role":{"description":"Role to assign when the invitation is accepted.","default":"member","$ref":"#/components/schemas/OrgMemberRole"}},"required":["email"]},"OrgMemberRole":{"description":"Role within the organization.","example":"member","type":"string","enum":["admin","member"]},"UpdateGlobalFiltersRequest":{"description":"Replace the given lists wholesale. Omitted fields are unchanged; pass `[]` to clear a list. See each field's description for the exact format expected — subreddits without `r/`, repos as `owner/name`, etc.","type":"object","properties":{"negativeKeywords":{"maxItems":2000,"type":"array","items":{"description":"Word, phrase, or `@handle` / `$TICKER` token. Trimmed; 1-200 chars.","type":"string","minLength":1,"maxLength":200}},"negativeAuthors":{"maxItems":2000,"type":"array","items":{"description":"Author handle/username exactly as it appears on `mention.author`. Don't add a leading `@` unless the platform does (Twitter stores handles without `@`; Reddit stores usernames as-is; YouTube uses channel titles).","type":"string","minLength":1,"maxLength":200}},"negativeSubreddits":{"maxItems":2000,"type":"array","items":{"description":"Subreddit name. Both `nextjs` and `r/nextjs` work — the `r/` prefix is stripped on input. Stored and matched case-insensitively without the prefix.","type":"string","maxLength":120}},"positiveSubreddits":{"maxItems":2000,"type":"array","items":{"description":"Subreddit name. Both `nextjs` and `r/nextjs` work — the `r/` prefix is stripped on input. Stored and matched case-insensitively without the prefix.","type":"string","maxLength":120}},"negativeRepos":{"maxItems":2000,"type":"array","items":{"description":"GitHub repo in `owner/name` form (e.g. `vercel/next.js`).","type":"string","minLength":3,"maxLength":140}}}},"AddGlobalFilterValuesRequest":{"description":"Append values to one filter list. The merge happens server-side against the committed row, so concurrent callers converge instead of overwriting each other. Values already on the list are skipped; values already in the mutually-exclusive counterpart list (`negativeSubreddits` ↔ `positiveSubreddits`) are refused and reported under `dropped`.","oneOf":[{"type":"object","properties":{"list":{"type":"string","const":"negativeKeywords"},"values":{"description":"Values to add. Already-present values are skipped.","minItems":1,"maxItems":2000,"type":"array","items":{"description":"Word, phrase, or `@handle` / `$TICKER` token. Trimmed; 1-200 chars.","type":"string","minLength":1,"maxLength":200}}},"required":["list","values"]},{"type":"object","properties":{"list":{"type":"string","const":"negativeAuthors"},"values":{"description":"Values to add. Already-present values are skipped.","minItems":1,"maxItems":2000,"type":"array","items":{"description":"Author handle/username exactly as it appears on `mention.author`. Don't add a leading `@` unless the platform does (Twitter stores handles without `@`; Reddit stores usernames as-is; YouTube uses channel titles).","type":"string","minLength":1,"maxLength":200}}},"required":["list","values"]},{"type":"object","properties":{"list":{"type":"string","const":"negativeSubreddits"},"values":{"description":"Values to add. Already-present values are skipped.","minItems":1,"maxItems":2000,"type":"array","items":{"description":"Subreddit name. Both `nextjs` and `r/nextjs` work — the `r/` prefix is stripped on input. Stored and matched case-insensitively without the prefix.","type":"string","maxLength":120}}},"required":["list","values"]},{"type":"object","properties":{"list":{"type":"string","const":"positiveSubreddits"},"values":{"description":"Values to add. Already-present values are skipped.","minItems":1,"maxItems":2000,"type":"array","items":{"description":"Subreddit name. Both `nextjs` and `r/nextjs` work — the `r/` prefix is stripped on input. Stored and matched case-insensitively without the prefix.","type":"string","maxLength":120}}},"required":["list","values"]},{"type":"object","properties":{"list":{"type":"string","const":"negativeRepos"},"values":{"description":"Values to add. Already-present values are skipped.","minItems":1,"maxItems":2000,"type":"array","items":{"description":"GitHub repo in `owner/name` form (e.g. `vercel/next.js`).","type":"string","minLength":3,"maxLength":140}}},"required":["list","values"]}],"type":"object"},"RemoveGlobalFilterValuesRequest":{"description":"Remove values from one filter list, server-side against the committed row — so a removal is never undone by a concurrent add to the same list.","type":"object","properties":{"list":{"$ref":"#/components/schemas/FilterListName"},"values":{"description":"Values to remove. Values not present are ignored.","minItems":1,"maxItems":2000,"type":"array","items":{"type":"string","minLength":1,"maxLength":200}},"allowEmpty":{"description":"Permission to commit a removal that leaves the list EMPTY. Emptying a list erases org-wide filter config with no undo, so without this the request is refused with `CLEAR_NOT_CONFIRMED` (400). Checked against the committed contents, so a concurrent removal cannot slip an unconsented clear through.","default":false,"type":"boolean"}},"required":["list","values"]},"FilterListName":{"description":"Which org-wide filter list to operate on.","type":"string","enum":["negativeKeywords","negativeAuthors","negativeSubreddits","positiveSubreddits","negativeRepos"]},"SearchRequest":{"description":"On-demand mention search request.","type":"object","properties":{"query":{"description":"The search phrase.","type":"string","minLength":1,"maxLength":500},"timeWindow":{"description":"How far back to search: last 1, 7, or 30 days.","type":"string","enum":["1d","7d","30d"]},"sources":{"description":"Subset of workspace-enabled platforms to search. Defaults to all enabled sources.","maxItems":64,"type":"array","items":{"type":"string","maxLength":64}},"excludeSources":{"description":"Sources subtracted from the resolved source list.","maxItems":64,"type":"array","items":{"type":"string","maxLength":64}},"keywordContext":{"description":"Per-call relevance disambiguation. Overrides the workspace's relevance context for this call only.","type":"string","maxLength":2000},"additionalTerms":{"description":"Comma-separated terms required to co-occur with the query.","type":"string","maxLength":500},"additionalTermsAndOr":{"description":"`false` = AND (default), `true` = OR for `additionalTerms`.","default":false,"type":"boolean"},"excludeWords":{"description":"Comma-separated exclusions.","type":"string","maxLength":500},"minRelevance":{"description":"Display filter: hide relevant results below this AI-relevance bar from the response. Does not affect what is saved or billed. Ignored when `includeNonRelevant` is true.","default":"medium","type":"string","enum":["high","medium","low"]},"includeNonRelevant":{"description":"Display flag only. When true, mentions the AI judged NOT relevant are also returned (marked `relevance: \"not_relevant\"`). This does NOT change billing or persistence: every mention that reaches AI scoring is saved to the search and counts against the monthly quota regardless of this flag — it only controls what the response shows. Default false.","default":false,"type":"boolean"},"maxResults":{"description":"Result budget: drives how many candidates are fetched per source (min(maxResults, 100) each), AI-scored (which is what consumes mention quota), and returned. Default 100, cap 500.","default":100,"type":"integer","minimum":1,"maximum":500},"waitMs":{"description":"How long the request blocks before falling back to async (202 + poll). Default and max 25000.","default":25000,"type":"integer","minimum":0,"maximum":25000}},"required":["query","timeWindow"]},"CreateNotificationRequest":{"description":"Create a notification for an existing feed. Slack destinations are validated against the workspace's Slack connection (channel IDs must resolve); webhook URLs must be public http(s) endpoints — a URL whose host is, or resolves to, a private, loopback, link-local or unique-local address is refused (`WEBHOOK_URL_INVALID`), and the same screen runs again at every delivery.","type":"object","properties":{"name":{"description":"Notification display name. Max 191 characters, no control characters.","type":"string","minLength":1,"maxLength":191},"feedId":{"description":"ID of the feed whose filters drive this notification. A feed can have at most one notification.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"destinations":{"description":"Delivery destinations — at least one, at most one per type (EMAIL / SLACK / WEBHOOK). Each entry must carry exactly the sub-object matching its `type`.","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/Destination"}}},"required":["name","feedId","destinations"]},"UpdateNotificationRequest":{"description":"Partial update — only the provided fields change.","type":"object","properties":{"name":{"description":"Replace the display name. Max 191 characters.","type":"string","minLength":1,"maxLength":191},"feedId":{"description":"Re-point the notification at a different feed.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"enabled":{"description":"Pause (`false`) or resume (`true`) the notification.","type":"boolean"},"destinations":{"description":"If provided, sets the destination LIST (one per type): a type present here is PATCHED onto the stored destination of that type — every field you omit keeps its current value — and a type absent from the array is removed. Omit the whole field to leave destinations untouched. An empty array is not allowed — a notification always keeps at least one destination; delete the notification instead.","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/DestinationPatch"}}}},"DestinationPatch":{"description":"A partial delivery configuration. Only `type` is required — every omitted field keeps the value stored on the destination of that type. A type with no stored destination is an ADD and must carry its matching `*Destination` sub-object.","type":"object","properties":{"type":{"$ref":"#/components/schemas/DestinationType"},"frequency":{"description":"Omit to keep the stored destination's current cadence.","$ref":"#/components/schemas/DestinationFrequency"},"deliveryMode":{"description":"`batch` sends one digest per period; `individual` one message per mention. Omit to keep the current value.","type":"string","enum":["batch","individual"]},"time":{"description":"`HH:mm` 24-hour, zero-padded. Omit to keep the current value.","example":"09:00","type":"string","maxLength":16},"timezone":{"description":"IANA timezone or `UTC±H:MM` offset. Omit to keep the current value.","example":"UTC","type":"string","maxLength":64},"dayOfWeek":{"description":"0 (Sunday)–6 (Saturday). Omit to keep the current value.","type":"integer","minimum":0,"maximum":6},"emailDestination":{"description":"Present when `type=EMAIL`; omit to keep the current recipients.","type":"object","properties":{"emails":{"$ref":"#/components/schemas/EmailList"}},"required":["emails"]},"slackDestination":{"description":"Present when `type=SLACK`; omit to keep the current channels.","type":"object","properties":{"channels":{"description":"Comma-separated Slack channel IDs, e.g. `C0123ABC,C0456DEF`. Max 191 characters (the column width).","type":"string","minLength":1,"maxLength":191},"channelNamesMap":{"type":"string","maxLength":8000}},"required":["channels"]},"webhookDestination":{"description":"Present when `type=WEBHOOK`; omit to keep the current URL.","type":"object","properties":{"url":{"$ref":"#/components/schemas/HttpUrl"}},"required":["url"]}},"required":["type"]},"PatchAgencyWorkspace":{"type":"object","properties":{"mentionsLimitOverride":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"keywordsLimitOverride":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"flexPricingEnabled":{"type":"boolean"},"flexPricingBudgetCents":{"type":"integer","minimum":0,"maximum":9007199254740991}}},"AuthIntrospection":{"description":"The authenticated credential: workspace binding, kind, and effective scopes.","type":"object","properties":{"organizationId":{"description":"The workspace this credential is bound to.","example":"org_2abc...","type":"string"},"authKind":{"description":"How the request authenticated: `api_key` (REST v2 key — the only kind the public API serves today), `session` (UI cookie), or `direct` (the internal CLI's in-process transport).","example":"api_key","type":"string","enum":["api_key","session","direct"]},"apiKeyId":{"description":"Clerk API key id (`ak_...`) when authenticated with a key, else null.","example":"ak_2abc...","anyOf":[{"type":"string"},{"type":"null"}]},"scopes":{"description":"Effective scopes of the credential. Keys minted before scoping existed carry no explicit scopes and are enforced as read-only, so they report `[\"read\"]` here — exactly what the scope gate applies.","example":["read","write"],"type":"array","items":{"$ref":"#/components/schemas/AuthScope"}},"userId":{"description":"User the request acts as (an API key acts as the user who minted it), when known.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["organizationId","authKind","apiKeyId","scopes","userId"],"additionalProperties":false},"AuthScope":{"description":"One API scope. `read` < `write` < `admin` (each implies the previous).","example":"write","type":"string","enum":["read","write","admin"]},"ErrorResponse":{"description":"Standard error envelope returned for all non-2xx responses. The `code` field is stable — safe to branch on programmatically. `code`, `message` and `status` are always present; anything else is additive and per-code (`details` on `VALIDATION_ERROR`, `upgradeUrl`/`upgradeCommand` on `UPGRADE_REQUIRED`).","type":"object","properties":{"error":{"type":"object","properties":{"code":{"description":"Machine-readable error code. See `ApiErrorCode` for the full list.","example":"NOT_FOUND","$ref":"#/components/schemas/ApiErrorCode"},"message":{"description":"Human-readable error message.","example":"Resource not found","type":"string"},"status":{"description":"HTTP status code — always matches the response status.","example":404,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"details":{"description":"Present on `VALIDATION_ERROR` responses. Contains Zod issues describing each failing field (path, code, message).","type":"array","items":{}},"upgradeUrl":{"description":"Present on `UPGRADE_REQUIRED` responses (additive — see `ApiErrorCode`): the URL of the upgrade page for this workspace.","example":"https://app.octolens.com/me/upgrade?src=agents","type":"string"},"upgradeCommand":{"description":"Present on `UPGRADE_REQUIRED` responses (additive): the CLI command that mints a single-use, already-authenticated link into the upgrade page.","example":"octolens upgrade","type":"string"}},"required":["code","message","status"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"ApiErrorCode":{"description":"Stable, machine-readable error code. Grouped as follows:\n\n**Auth / request shape** — `UNAUTHORIZED` (401), `FORBIDDEN` (403), `RATE_LIMITED` (429), `VALIDATION_ERROR` (400 — response carries a `details` array with Zod issues), `INTERNAL_ERROR` (500).\n\n**Agency master-key org selection** — `ORGANIZATION_REQUIRED` (400 — an agency master key must pass the `x-octolens-organization-id` header to select a target workspace), `FORBIDDEN_ORGANIZATION` (403 — the requested organization does not belong to the key's agency).\n\n**Plan / quota walls** — `UPGRADE_REQUIRED` (403 — an Agents-plan wall: a cap was hit (the lifetime search allowance or the monthly mention quota), or the endpoint is not part of the Agents plan at all — on that plan only search, org/usage/auth introspection, the upgrade link and monitoring recommendations are available, and every other endpoint answers this code before validating input; the envelope carries two ADDITIVE fields alongside the standard three, `upgradeUrl` (the upgrade page) and `upgradeCommand` (`octolens upgrade`, which mints a single-use authenticated link into that page), so an agent gets an actionable next step, not just prose), `QUOTA_EXCEEDED` (402 — the monthly mention quota is exhausted on a non-Agents plan; enable flex pricing or upgrade). The search routes also send `X-Octolens-Searches-Remaining` / `X-Octolens-Mentions-Remaining` response headers on success so clients can self-throttle before either wall.\n\n**Not found (404)** — generic `NOT_FOUND` plus domain-specific variants: `FEED_NOT_FOUND`, `KEYWORD_NOT_FOUND`, `KEYWORDS_NOT_FOUND`, `POST_NOT_FOUND`, `MENTION_NOT_FOUND`, `SUMMARY_NOT_FOUND`, `SEARCH_NOT_FOUND`, `SUGGESTION_NOT_FOUND`, `COMPANY_NOT_FOUND`, `ORG_NOT_FOUND`, `SETTINGS_NOT_FOUND`, `NOTIFICATION_NOT_FOUND`, `VIEW_NOT_FOUND`, `ALERT_NOT_FOUND`. A resource id that exists but belongs to another workspace answers the SAME not-found as an unused id — existence is never leaked across tenants.\n\n**Authorization (403)** — generic `FORBIDDEN` (the caller is not permitted to perform the operation, e.g. insufficient API-key scope). There are deliberately NO per-resource 403 codes: cross-workspace access reads as the resource's 404.\n\n**Business-rule violations (400)** — `INVALID_INPUT` (semantically invalid arguments), `KEYWORD_LIMIT_EXCEEDED` (plan cap hit), `LAST_ADMIN` (refuses to remove the only admin), `ITEM_EXISTS` (duplicate), `INVALID_DOMAIN`, `INVALID_TIMEZONE`, `SLACK_NOT_CONNECTED` / `SLACK_CHANNEL_NOT_FOUND` / `SLACK_CHANNEL_REQUIRED` (Slack destination validation), `WEBHOOK_URL_INVALID` (webhook URL rejected), `NO_DESTINATIONS` / `NO_SAMPLE_MENTION` (notification test delivery preconditions), `INVALID_DESTINATION` (a destination is missing/mismatched its type-specific sub-object, or two share a type) / `DESTINATION_INDEX_OUT_OF_RANGE` (feed destination index).\n\n**Operation failures (500)** — `ENGAGE_FAILED` / `RELEVANCE_UPDATE_FAILED` carry a specific code so clients can distinguish a failed mutation from a generic `INTERNAL_ERROR`.","type":"string","enum":["UNAUTHORIZED","FORBIDDEN","RATE_LIMITED","VALIDATION_ERROR","INTERNAL_ERROR","ORGANIZATION_REQUIRED","FORBIDDEN_ORGANIZATION","MASTER_KEY_REQUIRED","AGENCY_INACTIVE","NOT_AN_AGENCY_WORKSPACE","AGENCY_NOT_FOUND","WORKSPACE_NOT_IN_AGENCY","ANCHOR_WORKSPACE","AGENCY_WORKSPACE_LIMIT","INVITE_FANOUT_TOO_LARGE","SUPERADMIN_REMOVE_FORBIDDEN","USER_REQUIRED","FLEX_REQUIRES_CAP","FLEX_REQUIRES_BUDGET","AGENCY_BILLING_NOT_CONFIGURED","WORKSPACE_CREATE_FAILED","WORKSPACE_SETUP_FAILED","WORKSPACE_ACCESS_FAILED","UPGRADE_REQUIRED","QUOTA_EXCEEDED","NOT_FOUND","FEED_NOT_FOUND","KEYWORD_NOT_FOUND","KEYWORDS_NOT_FOUND","POST_NOT_FOUND","MENTION_NOT_FOUND","SUMMARY_NOT_FOUND","SEARCH_NOT_FOUND","SUGGESTION_NOT_FOUND","COMPANY_NOT_FOUND","ORG_NOT_FOUND","SETTINGS_NOT_FOUND","NOTIFICATION_NOT_FOUND","VIEW_NOT_FOUND","ALERT_NOT_FOUND","INVALID_INPUT","KEYWORD_LIMIT_EXCEEDED","LAST_ADMIN","ITEM_EXISTS","INVALID_DOMAIN","INVALID_TIMEZONE","SLACK_NOT_CONNECTED","SLACK_CHANNEL_NOT_FOUND","SLACK_CHANNEL_REQUIRED","WEBHOOK_URL_INVALID","NO_DESTINATIONS","NO_SAMPLE_MENTION","INVALID_DESTINATION","DESTINATION_INDEX_OUT_OF_RANGE","ENGAGE_FAILED","RELEVANCE_UPDATE_FAILED"]},"FeedListResponse":{"description":"List of feeds for the authenticated organization.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Feed"}}},"required":["data"],"additionalProperties":false},"Feed":{"description":"A saved filter with optional notification destinations.","type":"object","properties":{"id":{"description":"Feed ID.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"description":"Display name shown in the app and included in notifications.","example":"Competitor mentions","type":"string"},"icon":{"$ref":"#/components/schemas/FeedIcon"},"simpleFilters":{"anyOf":[{"$ref":"#/components/schemas/SimpleFiltersOutput"},{"type":"null"}]},"advancedFilters":{"anyOf":[{"$ref":"#/components/schemas/AdvancedFiltersOutput"},{"type":"null"}]},"isDefault":{"description":"If `true`, this feed is pinned as the org's default view.","type":"boolean"},"destinations":{"description":"Notification destinations. Empty array = filter-only feed with no notifications.","type":"array","items":{"$ref":"#/components/schemas/DestinationOutput"}},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","name","icon","simpleFilters","advancedFilters","isDefault","destinations","createdAt","updatedAt"],"additionalProperties":false},"KeywordListResponse":{"description":"List of keywords for the authenticated organization.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Keyword"}}},"required":["data"],"additionalProperties":false},"Keyword":{"description":"One monitored keyword.","type":"object","properties":{"id":{"description":"Stable numeric keyword ID.","example":42,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"The phrase being tracked.","example":"acme corp","type":"string"},"context":{"description":"Short sentence disambiguating the keyword for AI relevance scoring. Auto-generated from the company profile when a keyword is created without an explicit context.","example":"Acme is a cloud storage company, not the cartoon.","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTerms":{"description":"Additional terms required alongside the main keyword (comma-separated). Combined with `additionalTermsAndOr` to form AND/OR semantics.","example":"deployment, preview","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTermsAndOr":{"description":"How `additionalTerms` combine: `true` = OR (any one must appear), `false` = AND (all must appear).","type":"boolean"},"caseSensitive":{"description":"If `true`, matching is case-sensitive. Default `false`.","type":"boolean"},"symbolSensitive":{"description":"Also known as \"exact match\" in the UI. When `true`, multi-word keywords require the words to appear together. When `false`, each word can appear anywhere in the post (noisier).","type":"boolean"},"platforms":{"description":"Platforms this keyword is monitored on.","example":["reddit","twitter","youtube"],"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"excludeWords":{"description":"Comma-separated words that disqualify a post if present. Applied as exact-match terms.","example":"jobs, hiring, is-for-sale","anyOf":[{"type":"string"},{"type":"null"}]},"wildcardExcludeWords":{"description":"Comma-separated wildcard exclusion patterns. Supports `*` for prefix/suffix match (e.g. `spam*` matches `spammer`).","anyOf":[{"type":"string"},{"type":"null"}]},"excludeAuthors":{"description":"Comma-separated author handles / usernames whose posts should be filtered out.","example":"spambot1, crypto_guy","anyOf":[{"type":"string"},{"type":"null"}]},"tag":{"description":"Classification tag. `null` for legacy keywords without a tag.","anyOf":[{"$ref":"#/components/schemas/KeywordTag"},{"type":"null"}]},"paused":{"description":"When `true`, data collection is paused for this keyword.","type":"boolean"},"isSubReddit":{"description":"When `true`, this keyword represents a subreddit to monitor rather than a free-text phrase. `null` for non-Reddit keywords.","anyOf":[{"type":"boolean"},{"type":"null"}]}},"required":["id","keyword","context","additionalTerms","additionalTermsAndOr","caseSensitive","symbolSensitive","platforms","excludeWords","wildcardExcludeWords","excludeAuthors","tag","paused","isSubReddit"],"additionalProperties":false},"CreateKeywordResponse":{"description":"The created keyword — or, on a duplicate term, the already-tracked keyword (`alreadyExisted: true`).","type":"object","properties":{"id":{"description":"Stable numeric keyword ID.","example":42,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"The phrase being tracked.","example":"acme corp","type":"string"},"context":{"description":"Short sentence disambiguating the keyword for AI relevance scoring. Auto-generated from the company profile when a keyword is created without an explicit context.","example":"Acme is a cloud storage company, not the cartoon.","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTerms":{"description":"Additional terms required alongside the main keyword (comma-separated). Combined with `additionalTermsAndOr` to form AND/OR semantics.","example":"deployment, preview","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTermsAndOr":{"description":"How `additionalTerms` combine: `true` = OR (any one must appear), `false` = AND (all must appear).","type":"boolean"},"caseSensitive":{"description":"If `true`, matching is case-sensitive. Default `false`.","type":"boolean"},"symbolSensitive":{"description":"Also known as \"exact match\" in the UI. When `true`, multi-word keywords require the words to appear together. When `false`, each word can appear anywhere in the post (noisier).","type":"boolean"},"platforms":{"description":"Platforms this keyword is monitored on.","example":["reddit","twitter","youtube"],"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"excludeWords":{"description":"Comma-separated words that disqualify a post if present. Applied as exact-match terms.","example":"jobs, hiring, is-for-sale","anyOf":[{"type":"string"},{"type":"null"}]},"wildcardExcludeWords":{"description":"Comma-separated wildcard exclusion patterns. Supports `*` for prefix/suffix match (e.g. `spam*` matches `spammer`).","anyOf":[{"type":"string"},{"type":"null"}]},"excludeAuthors":{"description":"Comma-separated author handles / usernames whose posts should be filtered out.","example":"spambot1, crypto_guy","anyOf":[{"type":"string"},{"type":"null"}]},"tag":{"description":"Classification tag. `null` for legacy keywords without a tag.","anyOf":[{"$ref":"#/components/schemas/KeywordTag"},{"type":"null"}]},"paused":{"description":"When `true`, data collection is paused for this keyword.","type":"boolean"},"isSubReddit":{"description":"When `true`, this keyword represents a subreddit to monitor rather than a free-text phrase. `null` for non-Reddit keywords.","anyOf":[{"type":"boolean"},{"type":"null"}]},"alreadyExisted":{"description":"`true` when the term was already tracked (case-insensitive exact match) and the existing keyword was returned instead of creating a duplicate; `false` when a new keyword was created.","type":"boolean"}},"required":["id","keyword","context","additionalTerms","additionalTermsAndOr","caseSensitive","symbolSensitive","platforms","excludeWords","wildcardExcludeWords","excludeAuthors","tag","paused","isSubReddit","alreadyExisted"],"additionalProperties":false},"DeleteKeywordResponse":{"description":"The keyword was deleted. `deletedFeeds`/`deletedNotifications` name the cascade — always present, empty when nothing else was destroyed.","type":"object","properties":{"deletedFeeds":{"description":"Feeds destroyed alongside the keyword, because their filters referenced only it. Empty when nothing cascaded.","type":"array","items":{"$ref":"#/components/schemas/CascadeDeletedFeed"}},"deletedNotifications":{"description":"Notifications destroyed alongside those feeds. Empty when nothing cascaded. A notification cannot outlive the feed it is attached to, so this is a strict consequence of `deletedFeeds`.","type":"array","items":{"$ref":"#/components/schemas/CascadeDeletedNotification"}},"ok":{"type":"boolean","const":true}},"required":["deletedFeeds","deletedNotifications","ok"],"additionalProperties":false},"CascadeDeletedFeed":{"description":"A feed removed (or removable) as collateral of a keyword deletion.","type":"object","properties":{"id":{"description":"The feed's id.","example":25248,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"description":"The feed's name.","example":"Acme mentions","type":"string"}},"required":["id","name"],"additionalProperties":false},"CascadeDeletedNotification":{"description":"A notification removed (or removable) as collateral of a keyword deletion — it goes when the feed it is attached to goes.","type":"object","properties":{"id":{"description":"The notification's id.","example":19,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"description":"The notification's name.","example":"Acme alert","type":"string"},"feedId":{"description":"The feed this notification was attached to.","example":25248,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"destinationCount":{"description":"How many delivery destinations the notification had.","example":1,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","name","feedId","destinationCount"],"additionalProperties":false},"PauseKeywordResponse":{"description":"The committed keyword row plus whether this request changed it.","type":"object","properties":{"id":{"description":"Stable numeric keyword ID.","example":42,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"The phrase being tracked.","example":"acme corp","type":"string"},"context":{"description":"Short sentence disambiguating the keyword for AI relevance scoring. Auto-generated from the company profile when a keyword is created without an explicit context.","example":"Acme is a cloud storage company, not the cartoon.","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTerms":{"description":"Additional terms required alongside the main keyword (comma-separated). Combined with `additionalTermsAndOr` to form AND/OR semantics.","example":"deployment, preview","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTermsAndOr":{"description":"How `additionalTerms` combine: `true` = OR (any one must appear), `false` = AND (all must appear).","type":"boolean"},"caseSensitive":{"description":"If `true`, matching is case-sensitive. Default `false`.","type":"boolean"},"symbolSensitive":{"description":"Also known as \"exact match\" in the UI. When `true`, multi-word keywords require the words to appear together. When `false`, each word can appear anywhere in the post (noisier).","type":"boolean"},"platforms":{"description":"Platforms this keyword is monitored on.","example":["reddit","twitter","youtube"],"type":"array","items":{"$ref":"#/components/schemas/Platform"}},"excludeWords":{"description":"Comma-separated words that disqualify a post if present. Applied as exact-match terms.","example":"jobs, hiring, is-for-sale","anyOf":[{"type":"string"},{"type":"null"}]},"wildcardExcludeWords":{"description":"Comma-separated wildcard exclusion patterns. Supports `*` for prefix/suffix match (e.g. `spam*` matches `spammer`).","anyOf":[{"type":"string"},{"type":"null"}]},"excludeAuthors":{"description":"Comma-separated author handles / usernames whose posts should be filtered out.","example":"spambot1, crypto_guy","anyOf":[{"type":"string"},{"type":"null"}]},"tag":{"description":"Classification tag. `null` for legacy keywords without a tag.","anyOf":[{"$ref":"#/components/schemas/KeywordTag"},{"type":"null"}]},"paused":{"description":"When `true`, data collection is paused for this keyword.","type":"boolean"},"isSubReddit":{"description":"When `true`, this keyword represents a subreddit to monitor rather than a free-text phrase. `null` for non-Reddit keywords.","anyOf":[{"type":"boolean"},{"type":"null"}]},"changed":{"description":"True when THIS request is the one that moved the row; false when the keyword was already in the requested state (or a concurrent identical request won the race).","example":true,"type":"boolean"}},"required":["id","keyword","context","additionalTerms","additionalTermsAndOr","caseSensitive","symbolSensitive","platforms","excludeWords","wildcardExcludeWords","excludeAuthors","tag","paused","isSubReddit","changed"],"additionalProperties":false},"KeywordDeletionPreview":{"description":"What deleting this keyword would destroy besides the keyword itself. A read — it changes nothing.","type":"object","properties":{"keywordId":{"description":"The keyword the preview is for.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"The keyword's term.","type":"string"},"feeds":{"description":"Feeds that WOULD be deleted with this keyword. Empty when none would.","type":"array","items":{"$ref":"#/components/schemas/CascadeDeletedFeed"}},"notifications":{"description":"Notifications that WOULD be deleted with those feeds. Empty when none would.","type":"array","items":{"$ref":"#/components/schemas/CascadeDeletedNotification"}}},"required":["keywordId","keyword","feeds","notifications"],"additionalProperties":false},"MentionListResponse":{"description":"Paginated list of mentions.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Mention"}},"pagination":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["nextCursor"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"Mention":{"description":"A single mention collected from a social platform. Canonical shape shared by `list_mentions` (MCP), `GET /api/v2/mentions` (REST), and the tRPC feed.","type":"object","properties":{"id":{"description":"Internal post id (numeric). Pass as `postId` to `PATCH /api/v2/mentions/{sourceId}`.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"sourceId":{"description":"Stable composite key for the mention across the collection pipeline (e.g. `reddit_t3_1abc234`). Use this as the path parameter for `PATCH /api/v2/mentions/{sourceId}` and as the `sourceId` field on `POST /api/v2/feedback`.","example":"reddit_t3_1abc234","type":"string"},"url":{"description":"Canonical URL to the original post.","example":"https://reddit.com/r/example/comments/abc123/...","type":"string"},"title":{"description":"Post title. `null` for platforms that don't have titles (tweets, etc.).","anyOf":[{"type":"string"},{"type":"null"}]},"body":{"description":"Post body / content. May be empty or `null` for posts with no text.","anyOf":[{"type":"string"},{"type":"null"}]},"source":{"$ref":"#/components/schemas/MentionSource"},"timestamp":{"description":"When the mention was posted. Tinybird-style datetime string (`YYYY-MM-DD HH:mm:ss.SSS`, UTC, no `Z` suffix).","example":"2026-05-06 13:35:37.000","type":"string"},"author":{"description":"Handle/username of the mention's author. `null` if not captured.","example":"jane.doe","anyOf":[{"type":"string"},{"type":"null"}]},"authorName":{"description":"Display name of the author, when distinct from the handle.","anyOf":[{"type":"string"},{"type":"null"}]},"authorAvatar":{"description":"URL to the author's avatar image. `null` if not captured.","anyOf":[{"type":"string"},{"type":"null"}]},"authorUrl":{"description":"URL to the author's profile page. `null` if not captured.","anyOf":[{"type":"string"},{"type":"null"}]},"authorFollowers":{"description":"Author's follower count at collection time. `null` if not available.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"relevance":{"description":"Relevance classification. Always one of `relevant` / `not_relevant` (unscored posts collapse to `not_relevant`).","example":"relevant","$ref":"#/components/schemas/MentionRelevance"},"relevanceComment":{"description":"Free-text justification the AI produced for its relevance verdict, when available.","anyOf":[{"type":"string"},{"type":"null"}]},"sentiment":{"description":"Sentiment classification. `null` until the AI has scored this mention.","anyOf":[{"$ref":"#/components/schemas/MentionSentiment"},{"type":"null"}]},"language":{"description":"Detected language as a full lowercase English name (`english`, `spanish`, …; `undetermined` when detection fails) — never an ISO 639-1 code.","example":"english","anyOf":[{"type":"string"},{"type":"null"}]},"tags":{"description":"AI-assigned topic tags applied to the mention.","example":["competitor_mention","pricing"],"type":"array","items":{"$ref":"#/components/schemas/MentionTag"}},"keywords":{"description":"Monitored keywords this mention matched, with id + display text.","example":[{"id":42,"keyword":"social listening"}],"type":"array","items":{"$ref":"#/components/schemas/MentionKeyword"}},"engaged":{"description":"Whether a workspace member has marked this mention as engaged-with (replied, liked on-platform, etc.).","type":"boolean"},"relevanceScore":{"description":"Raw internal relevance score (0 = high, 1 = medium, 2 = low). The public `relevance` field is derived from this. Optional; not all consumers populate it.","anyOf":[{"type":"number"},{"type":"null"}]},"feedbackRelevant":{"description":"User feedback on this mention's relevance: `1` for thumbs up, `-1` for thumbs down, `0` for no feedback. Optional.","type":"number"},"imageUrl":{"description":"URL to a preview image attached to the post, when present.","anyOf":[{"type":"string"},{"type":"null"}]},"keywordId":{"description":"Denormalized first-matched keyword id. Convenience for callers that need a single keyword reference without scanning `keywords[]`.","type":"number"}},"required":["id","sourceId","url","title","body","source","timestamp","author","authorName","authorAvatar","authorUrl","authorFollowers","relevance","relevanceComment","sentiment","language","tags","keywords","engaged"],"additionalProperties":false},"MentionSource":{"description":"Platform the mention was collected from. Lowercase values like `reddit`, `twitter`, `linkedin`. Historical rows may be uppercase.","example":"reddit","type":"string"},"MentionRelevance":{"description":"AI-judged relevance classification. `relevant` = high or medium confidence (score 0 or 1). `not_relevant` = low confidence or not yet scored.","example":"relevant","type":"string","enum":["relevant","not_relevant"]},"MentionKeyword":{"description":"Keyword the mention matched, with its display text.","type":"object","properties":{"id":{"description":"Tracked keyword id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"Keyword text.","type":"string"},"keywordTag":{"description":"Keyword's classification tag (e.g. `own_brand`, `competitor`, `industry_term`). Internal; ignored by external consumers.","type":"string"}},"required":["id","keyword"],"additionalProperties":false},"MentionByAuthorListResponse":{"description":"Paginated list of a single author's mentions.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Mention"}},"pagination":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["nextCursor"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"ExportMentionsResponse":{"description":"JSON shape returned when `format=json`. When `format=csv`, the response is `text/csv` with a matching `X-Total-Count` header.","type":"object","properties":{"data":{"description":"Exported mentions in the same shape as the list endpoint.","type":"array","items":{"$ref":"#/components/schemas/Mention"}},"total":{"description":"Total matching mentions returned. Up to the 50,000 export cap.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["data","total"],"additionalProperties":false},"SentimentDistributionResponse":{"type":"object","properties":{"data":{"description":"One entry per sentiment bucket that has a non-zero count.","type":"array","items":{"$ref":"#/components/schemas/SentimentDistributionEntry"}}},"required":["data"],"additionalProperties":false},"SentimentDistributionEntry":{"type":"object","properties":{"sentiment":{"description":"Sentiment label. `unknown` covers mentions the AI hasn't scored yet.","type":"string","enum":["Positive","Neutral","Negative","unknown"]},"count":{"description":"Number of mentions with this sentiment.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["sentiment","count"],"additionalProperties":false},"VolumeResponse":{"type":"object","properties":{"granularity":{"description":"Granularity that was used (echoes the request).","type":"string","enum":["day","hour"]},"data":{"description":"Sorted chronologically, ascending. Empty buckets are omitted (not zero-filled).","type":"array","items":{"$ref":"#/components/schemas/VolumeEntry"}}},"required":["granularity","data"],"additionalProperties":false},"VolumeEntry":{"type":"object","properties":{"bucket":{"description":"Bucket start. `YYYY-MM-DD` for daily granularity, ISO datetime (`YYYY-MM-DDTHH:00:00Z`) for hourly.","example":"2026-04-22","type":"string"},"count":{"description":"Number of mentions that fell in this bucket.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["bucket","count"],"additionalProperties":false},"SourceBreakdownResponse":{"type":"object","properties":{"data":{"description":"Sorted by `count` descending.","type":"array","items":{"$ref":"#/components/schemas/SourceBreakdownEntry"}}},"required":["data"],"additionalProperties":false},"SourceBreakdownEntry":{"type":"object","properties":{"source":{"description":"Platform source (e.g. `reddit`, `twitter`).","example":"reddit","type":"string"},"count":{"description":"Mentions from this source.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["source","count"],"additionalProperties":false},"KeywordBreakdownResponse":{"type":"object","properties":{"data":{"description":"Sorted by `count` descending.","type":"array","items":{"$ref":"#/components/schemas/KeywordBreakdownEntry"}}},"required":["data"],"additionalProperties":false},"KeywordBreakdownEntry":{"type":"object","properties":{"keywordId":{"description":"Tracked keyword id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"Keyword text.","type":"string"},"count":{"description":"Mentions that matched this keyword. A mention matching multiple keywords is counted once per match.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["keywordId","keyword","count"],"additionalProperties":false},"AnalyticsDashboardResponse":{"type":"object","properties":{"volume":{"$ref":"#/components/schemas/VolumeResponse"},"keywords":{"$ref":"#/components/schemas/KeywordBreakdownResponse"},"sentiment":{"$ref":"#/components/schemas/SentimentDistributionResponse"},"usage":{"$ref":"#/components/schemas/OrgUsage"}},"required":["volume","keywords","sentiment","usage"],"additionalProperties":false},"OrgUsage":{"description":"Current-cycle usage counters and plan limits.","type":"object","properties":{"plan":{"description":"Current subscription plan (e.g. `Free_trial`, `Pro_v3`, `Enterprise`). `null` if no active plan / free trial expired. The `*.limit` numbers below already reflect this plan.","example":"Pro_v3","anyOf":[{"type":"string"},{"type":"null"}]},"mentions":{"type":"object","properties":{"count":{"description":"Mentions consumed in the current billing cycle.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"limit":{"description":"Plan allowance + one-time `countExtension` + purchased add-on mentions (`additionalMentions`). Matches the cap shown in the app UI.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"resetAt":{"description":"When the mention counter resets (start of next cycle). `null` for plans without a reset schedule.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]}},"required":["count","limit","resetAt"],"additionalProperties":false},"keywords":{"type":"object","properties":{"count":{"description":"Live keyword count.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"limit":{"description":"Plan allowance + purchased add-on keywords (`additionalKeywords`).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["count","limit"],"additionalProperties":false},"searches":{"$ref":"#/components/schemas/OrgUsageSearches"},"flex":{"$ref":"#/components/schemas/OrgUsageFlex"}},"required":["plan","mentions","keywords"],"additionalProperties":false},"OrgUsageSearches":{"description":"Lifetime on-demand search allowance. Only present on the Agents plan (the only plan with a lifetime search cap). Numbers come from the canonical quota read — identical to the `X-Octolens-Searches-Remaining` header on `/api/v2/search*` responses.","type":"object","properties":{"used":{"description":"Lifetime on-demand searches consumed (never resets).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"limit":{"description":"Lifetime search allowance (plan allowance + any support-granted extension).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"remaining":{"description":"Searches left before the plan wall (`UPGRADE_REQUIRED`).","type":"integer","minimum":0,"maximum":9007199254740991}},"required":["used","limit","remaining"],"additionalProperties":false},"OrgUsageFlex":{"description":"Flex (usage-based) pricing state. Only present when flex pricing is enabled for the org.","type":"object","properties":{"enabled":{"description":"Flag marker so the field's presence unambiguously means flex is on.","type":"boolean","const":true},"budgetCents":{"description":"Max flex-pricing spend per cycle, in cents (e.g. 50000 = $500).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"used":{"description":"Flex mentions consumed in the current cycle.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"resetAt":{"description":"When the flex counter resets (synced with Stripe billing cycle).","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]}},"required":["enabled","budgetCents","used","resetAt"],"additionalProperties":false},"Org":{"description":"The authenticated workspace.","type":"object","properties":{"organizationId":{"description":"Organization ID (`org_...`). Immutable.","type":"string"},"name":{"description":"Workspace name. Shown in email notifications and in-app headers.","example":"Vercel","type":"string"},"plan":{"description":"Current subscription plan (e.g. `Free_trial`, `Pro_v3`, `Enterprise`). `null` if no active plan / free trial expired.","example":"Pro_v3","anyOf":[{"type":"string"},{"type":"null"}]},"isAnnualPlan":{"description":"If `true`, the subscription is billed annually (vs. monthly).","type":"boolean"},"platforms":{"$ref":"#/components/schemas/OrgPlatforms"},"createdAt":{"description":"When the organization was created.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"onboardingFinishedAt":{"description":"When the workspace finished onboarding. `null` if still onboarding.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"freeTrialExpired":{"description":"If `true`, the free trial has ended and a paid subscription is required.","type":"boolean"}},"required":["organizationId","name","plan","isAnnualPlan","platforms","createdAt","onboardingFinishedAt","freeTrialExpired"],"additionalProperties":false},"UpgradeLinkResponse":{"description":"A one-time authenticated deep link into the upgrade/billing page. Minting one is audit-logged (org, user, request id).","type":"object","properties":{"url":{"description":"Single-use, short-lived sign-in URL. Opening it in a browser lands on `destination` already authenticated as the calling workspace's user — no sign-in step. The embedded ticket is consumed on first redemption; replaying the URL fails.","example":"https://app.octolens.com/me/upgrade-auth?__clerk_ticket=sit_...&orgId=org_...&dest=%2Fme%2Fupgrade%3Fsrc%3Dagents","type":"string"},"destination":{"description":"The app path the link lands on: the Agents upgrade page (`/me/upgrade?src=agents`) on the Agents plan, the billing page (`/me/subscription`) on any other plan.","example":"/me/upgrade?src=agents","type":"string"},"plan":{"description":"The workspace's current plan (`null` when it has no active plan).","example":"Agents","anyOf":[{"type":"string"},{"type":"null"}]},"expiresAt":{"description":"ISO-8601 instant after which the link can no longer be redeemed.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"expiresInSeconds":{"description":"Seconds the link stays redeemable from the moment it was minted.","example":600,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["url","destination","plan","expiresAt","expiresInSeconds"],"additionalProperties":false},"Company":{"description":"The monitored company profile.","type":"object","properties":{"id":{"description":"Internal Company row id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"description":"Company display name. Used as the AI's subject.","example":"Vercel","type":"string"},"domain":{"description":"Primary domain (no scheme).","example":"vercel.com","type":"string"},"website":{"description":"Full website URL.","example":"https://vercel.com","type":"string"},"logo":{"description":"URL to the company logo image. `null` if not enriched yet.","anyOf":[{"type":"string"},{"type":"null"}]},"industry":{"description":"High-level industry classification (e.g. `Technology`, `SaaS`). Sourced from enrichment and injected into AI prompts.","example":"Technology","type":"string"},"sector":{"description":"Finer-grained sector beneath `industry`.","example":"Developer Tools","type":"string"},"tags":{"description":"Comma-separated category tags. Used as additional AI context for relevance.","example":"B2B,SaaS,Developer Tools","type":"string"},"description":{"description":"Prose description of the company. Fed into every relevance prompt — keep it factual and specific to improve scoring.","type":"string"},"linkedin":{"description":"LinkedIn company slug (e.g. `vercel`). `null` if not set.","example":"vercel","anyOf":[{"type":"string"},{"type":"null"}]},"twitter":{"description":"X/Twitter handle without `@`, or comma-separated list.","example":"vercel","anyOf":[{"type":"string"},{"type":"null"}]},"relevanceContext":{"description":"Short context sentence (≤400 chars) injected into every AI relevance prompt for this org. Useful for disambiguating common-word brands.","example":"Vercel is a cloud platform for frontend deployments, not a vehicle brand.","anyOf":[{"type":"string"},{"type":"null"}]},"productUseCases":{"description":"Free-text description (often newline-separated) of what the product does and typical use cases. Injected into relevance prompts.","anyOf":[{"type":"string"},{"type":"null"}]},"competitors":{"description":"JSON-encoded array of competitor names/descriptions stored as a string. The UI parses this; external clients can treat it opaquely.","anyOf":[{"type":"string"},{"type":"null"}]},"companyMoat":{"description":"Prose description of the company's competitive advantage.","anyOf":[{"type":"string"},{"type":"null"}]},"relevanceGuidelines":{"description":"Org-specific guidelines injected into the `<specific_guidelines>` section of the relevance-scoring prompt. Use this to teach the AI what 'relevant' means for you.","anyOf":[{"type":"string"},{"type":"null"}]},"classificationGuidelines":{"description":"Org-specific guidelines injected into the `<specific_guidelines>` section of the post-classification/tagging prompt.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","domain","website","logo","industry","sector","tags","description","linkedin","twitter","relevanceContext","productUseCases","competitors","companyMoat","relevanceGuidelines","classificationGuidelines"],"additionalProperties":false},"FeedbackRecord":{"description":"A persisted feedback record.","type":"object","properties":{"id":{"description":"Feedback record id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"sourceId":{"description":"Mention key.","type":"string"},"timestamp":{"description":"Mention timestamp, echoed back as submitted (Tinybird-style `YYYY-MM-DD HH:mm:ss.SSS` or ISO 8601).","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:?\\d{2})?$"},"organizationId":{"description":"Workspace id that owns this record.","type":"string"},"postId":{"description":"Linked internal post id, when known.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"keywordId":{"description":"Keyword id this feedback is about.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"source":{"description":"Platform source.","type":"string"},"feedbackType":{"$ref":"#/components/schemas/FeedbackType"},"feedbackReason":{"description":"Free-text reason, if provided.","anyOf":[{"type":"string"},{"type":"null"}]},"feedbackSource":{"$ref":"#/components/schemas/FeedbackSource"},"originalRelevanceScore":{"anyOf":[{"type":"number"},{"type":"null"}]},"createdBy":{"description":"User id of the submitter, if known.","anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","sourceId","timestamp","organizationId","postId","keywordId","source","feedbackType","feedbackReason","feedbackSource","originalRelevanceScore","createdBy","createdAt"],"additionalProperties":false},"RemoveFeedbackResponse":{"description":"Confirmation of a feedback removal. `removed` distinguishes a real deletion from a no-op on a mention that had no stored feedback.","type":"object","properties":{"success":{"type":"boolean","const":true},"removed":{"description":"`true` when a stored feedback record was deleted; `false` when no feedback existed for that mention key (the request is a harmless no-op either way).","type":"boolean"}},"required":["success","removed"],"additionalProperties":false},"FeedbackStats":{"type":"object","properties":{"totalFeedback":{"description":"Total feedback records.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"relevantCount":{"description":"Records marked `RELEVANT`.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"notRelevantCount":{"description":"Records marked `NOT_RELEVANT`.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"falsePositiveRate":{"description":"Fraction (0–1) of all feedback records marked `NOT_RELEVANT` (`notRelevantCount / totalFeedback`). Higher = AI over-reports. `0` when the org has no feedback.","type":"number"}},"required":["totalFeedback","relevantCount","notRelevantCount","falsePositiveRate"],"additionalProperties":false},"GenerateFilterResponse":{"description":"Filter object generated from a natural-language query. Client applies `filters` to the feed and shows `explanation` to the user.","type":"object","properties":{"filters":{"description":"Generated filter object, in the contract selected by `target`: the flat mentions filter map (default) or a feed `SimpleFilters`/`AdvancedFilters` shape (`target: \"feed\"`). `null` when the query is ambiguous or matches everything. Use `isAdvanced` to determine whether it's the simple or advanced shape.","anyOf":[{"anyOf":[{"$ref":"#/components/schemas/ApiFiltersOutput"},{"$ref":"#/components/schemas/SimpleFiltersOutput"},{"$ref":"#/components/schemas/AdvancedFiltersOutput"}]},{"type":"null"}]},"isAdvanced":{"description":"If `true`, `filters` is an `AdvancedFilters` shape (AND/OR groups). Otherwise it's a `SimpleFilters` (flat AND list).","type":"boolean"},"limit":{"description":"Requested page size — preserved from `currentLimit` unless the user asked to change it.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"includeAll":{"description":"Whether to include low-relevance posts.","type":"boolean"},"view":{"description":"View id the AI resolved the query against (if the user referenced a view by name). `null` otherwise.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"explanation":{"description":"Plain-English summary of what the filter will match — shown in the UI.","type":"string"}},"required":["filters","isAdvanced","limit","includeAll","view","explanation"],"additionalProperties":false},"MonitoringRecommendations":{"description":"Recommended monitoring settings (keywords, platforms, relevance-guideline hints) derived from the workspace's search history, sized for the target plan.","type":"object","properties":{"source":{"description":"`search_history` when derived from the org's actual searches; `domain` when the history was too thin (or the model unavailable) and the company-profile fallback path answered.","type":"string","enum":["search_history","domain"]},"targetPlan":{"$ref":"#/components/schemas/TargetPlan"},"keywordLimit":{"description":"The target plan's monthly keyword allowance the set was capped to.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keywords":{"description":"Ranked best-first. Never empty; never longer than `keywordLimit`.","type":"array","items":{"$ref":"#/components/schemas/KeywordRecommendation"}},"platforms":{"description":"Recommended platforms to monitor, best-first.","type":"array","items":{"$ref":"#/components/schemas/Platform"}},"relevanceGuidelineHints":{"description":"Candidate org-specific relevance guidelines distilled from search contexts. May be empty.","type":"array","items":{"type":"string"}},"history":{"description":"What the recommendation was computed from.","type":"object","properties":{"searchesConsidered":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"distinctQueries":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["searchesConsidered","distinctQueries"],"additionalProperties":false}},"required":["source","targetPlan","keywordLimit","keywords","platforms","relevanceGuidelineHints","history"],"additionalProperties":false},"KeywordRecommendation":{"description":"One recommended monitoring keyword, with rationale and evidence.","type":"object","properties":{"keyword":{"description":"The keyword to monitor (lowercase).","example":"pgvector","type":"string"},"rationale":{"description":"Why this keyword is worth monitoring, phrased for the user.","type":"string"},"tag":{"$ref":"#/components/schemas/KeywordTag"},"evidence":{"description":"Search-history evidence; `null` when the recommendation came from the domain-derived fallback path.","anyOf":[{"$ref":"#/components/schemas/KeywordRecommendationEvidence"},{"type":"null"}]}},"required":["keyword","rationale","tag","evidence"],"additionalProperties":false},"KeywordRecommendationEvidence":{"description":"Search-history evidence backing one recommended keyword.","type":"object","properties":{"searches":{"description":"How many searches in the org's history back this keyword.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"resultsFound":{"description":"Relevant results those searches surfaced, summed.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"queries":{"description":"The (normalized) search queries collapsed into this recommendation.","type":"array","items":{"type":"string"}}},"required":["searches","resultsFound","queries"],"additionalProperties":false},"KeywordSuggestionsListResponse":{"description":"Shape depends on the query: per-keyword list, count summary, or full enriched list with volume.","anyOf":[{"$ref":"#/components/schemas/KeywordSuggestionList"},{"$ref":"#/components/schemas/SuggestionCounts"},{"$ref":"#/components/schemas/SuggestionsWithVolume"}]},"KeywordSuggestionList":{"description":"Per-keyword suggestion bundle with a snapshot of current settings.","type":"object","properties":{"keyword":{"$ref":"#/components/schemas/KeywordSettingsPeek"},"suggestions":{"description":"All non-expired suggestions for the requested keyword.","type":"array","items":{"$ref":"#/components/schemas/KeywordSuggestion"}}},"required":["keyword","suggestions"],"additionalProperties":false},"KeywordSettingsPeek":{"description":"Snapshot of the current keyword config so the UI can show a diff preview.","type":"object","properties":{"excludeWords":{"description":"Current comma-separated exclude words on the keyword.","anyOf":[{"type":"string"},{"type":"null"}]},"excludeAuthors":{"description":"Current comma-separated excluded authors.","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTerms":{"description":"Current comma-separated additional terms.","anyOf":[{"type":"string"},{"type":"null"}]},"additionalTermsAndOr":{"description":"Current AND/OR combinator for additional terms.","type":"boolean"},"platforms":{"description":"Current comma-separated platforms list.","anyOf":[{"type":"string"},{"type":"null"}]},"symbolSensitive":{"description":"Current \"exact match\" mode.","type":"boolean"}},"required":["excludeWords","excludeAuthors","additionalTerms","additionalTermsAndOr","platforms","symbolSensitive"],"additionalProperties":false},"KeywordSuggestion":{"type":"object","properties":{"id":{"description":"Suggestion id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keywordId":{"description":"Keyword this suggestion targets.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"organizationId":{"description":"Organization id.","type":"string"},"type":{"$ref":"#/components/schemas/KeywordSuggestionType"},"value":{"description":"The proposed value. Shape depends on `type`: for `add_exclude_words` / `add_additional_terms` it's a comma-separated string; for `disable_source` it's a platform enum value; for `set_exact_match` it's `true`/`false` serialized; for `change_additional_terms_logic` it's `and`/`or`.","type":"string"},"reason":{"description":"AI-written justification shown to the user.","anyOf":[{"type":"string"},{"type":"null"}]},"impact":{"description":"AI-written estimated impact (e.g. 'reduces noise by ~15%'). Free-form prose.","anyOf":[{"type":"string"},{"type":"null"}]},"status":{"$ref":"#/components/schemas/SuggestionStatus"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"respondedAt":{"description":"When the user accepted / rejected. `null` while `status` is pending.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]}},"required":["id","keywordId","organizationId","type","value","reason","impact","status","createdAt","respondedAt"],"additionalProperties":false},"KeywordSuggestionType":{"description":"What kind of change the AI is proposing. `add_*` and `change_*` modify existing keyword fields; `set_exact_match` toggles the symbol-sensitive flag; `disable_source` removes a platform from the keyword's `platforms` list.","example":"add_exclude_words","type":"string","enum":["add_exclude_words","add_exclude_authors","add_additional_terms","change_additional_terms_logic","set_exact_match","disable_source"]},"SuggestionStatus":{"description":"Lifecycle. `pending` awaits user action; `accepted` / `rejected` are terminal; `expired` = the underlying keyword config changed and the suggestion no longer applies.","example":"pending","type":"string","enum":["pending","accepted","rejected","expired"]},"SuggestionCounts":{"description":"Count of pending suggestions per keyword, for showing an indicator in the keyword list.","type":"array","items":{"type":"object","properties":{"keywordId":{"description":"Keyword id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"count":{"description":"Number of pending suggestions.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["keywordId","count"],"additionalProperties":false}},"SuggestionsWithVolume":{"description":"Org-wide suggestion list enriched with keyword volume data.","type":"object","properties":{"keywords":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"type":"string"},"volume":{"description":"Mentions for this keyword in the recent window.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","keyword","volume"],"additionalProperties":false}},"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/EnrichedSuggestion"}},"totalSuggestionCount":{"description":"Total pending suggestions across all keywords (the full snapshot, not just the current page).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"nextCursor":{"description":"Opaque cursor for the next page of `suggestions`. Null when no more pages remain. `keywords` and `totalSuggestionCount` are not paginated. The cursor encodes a position in the current snapshot — if suggestions are accepted or rejected between page requests, later pages may skip or repeat items; restart pagination after mutations.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["keywords","suggestions","totalSuggestionCount","nextCursor"],"additionalProperties":false},"EnrichedSuggestion":{"description":"Suggestion augmented with keyword-level details — used when listing across keywords.","type":"object","properties":{"id":{"description":"Suggestion id.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keywordId":{"description":"Keyword the suggestion targets.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"keyword":{"description":"Keyword text.","type":"string"},"keywordVolume":{"description":"Mention volume on this keyword over the recent window.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"type":{"$ref":"#/components/schemas/KeywordSuggestionType"},"value":{"description":"Proposed value (shape depends on `type`, see `KeywordSuggestion.value`).","type":"string"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"impact":{"anyOf":[{"type":"string"},{"type":"null"}]},"impactScore":{"description":"Numeric impact score (0–1) used to sort suggestions. Higher = higher impact.","type":"number"},"keywordSettings":{"$ref":"#/components/schemas/KeywordSettingsPeek"}},"required":["id","keywordId","keyword","keywordVolume","type","value","reason","impact","impactScore","keywordSettings"],"additionalProperties":false},"SuggestionAccepted":{"type":"object","properties":{"success":{"type":"boolean","const":true},"appliedChanges":{"description":"Map of keyword fields that were changed to their new values (e.g. `{ \"excludeWords\": \"spam,scam\" }`). Shape depends on the suggestion `type`.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["success","appliedChanges"],"additionalProperties":false},"SuggestionSuccess":{"description":"The suggestion(s) were dismissed. `rejectedCount` / `rejectedSuggestionIds` name what actually left `pending`, which is the only way to tell a no-op from a bulk dismissal.","type":"object","properties":{"success":{"type":"boolean","const":true},"rejectedCount":{"description":"How many suggestions this call moved from `pending` to `rejected`. `0` is the affirmative 'there was nothing pending' — the request still succeeded.","example":3,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rejectedSuggestionIds":{"description":"Ids of the suggestions this call dismissed, ascending. Always present; `[]` when nothing was pending.","example":[11,12,13],"type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}},"required":["success","rejectedCount","rejectedSuggestionIds"],"additionalProperties":false},"OrgMembersListResponse":{"description":"Members of the authenticated organization.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrgMember"}}},"required":["data"],"additionalProperties":false},"OrgMember":{"description":"One member of the authenticated organization.","type":"object","properties":{"id":{"description":"Membership ID (not user ID). Use this value for `DELETE /org/members/{id}` to remove the member.","example":"orgmem_3CiC5zwdWxMVsGsePg1xYkHbQO0","type":"string"},"userId":{"description":"User ID of the member.","example":"user_2abc...","type":"string"},"email":{"description":"Primary email on the member's profile.","anyOf":[{"type":"string"},{"type":"null"}]},"firstName":{"description":"First name, if set on the profile.","anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"description":"Last name, if set on the profile.","anyOf":[{"type":"string"},{"type":"null"}]},"role":{"$ref":"#/components/schemas/OrgMemberRole"},"createdAt":{"description":"When the member joined the org.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","userId","email","firstName","lastName","role","createdAt"],"additionalProperties":false},"InviteMemberResponse":{"type":"object","properties":{"id":{"description":"Invitation id.","example":"orginv_3Ciq0qskipF0g0gxlfLckBtnXk2","type":"string"},"email":{"description":"Invitee's email address.","type":"string"},"role":{"$ref":"#/components/schemas/OrgMemberRole"},"createdAt":{"description":"When the invitation was created.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"alreadyInvited":{"description":"`true` when the address already had an outstanding invitation and this IS it — no second invitation was minted and no second email was sent. Mirrors `alreadyExisted` on keyword creation, so a repeated (or concurrent) invite is a safe no-op.","type":"boolean"}},"required":["id","email","role","createdAt","alreadyInvited"],"additionalProperties":false},"OrgInvitationsListResponse":{"description":"Invitations to the authenticated organization that have not been accepted yet.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PendingInvitation"}}},"required":["data"],"additionalProperties":false},"PendingInvitation":{"description":"An invitation that hasn't been accepted yet.","type":"object","properties":{"id":{"description":"Invitation id.","example":"orginv_3Ciq0qskipF0g0gxlfLckBtnXk2","type":"string"},"email":{"description":"Invitee's email address.","type":"string"},"role":{"$ref":"#/components/schemas/OrgMemberRole"},"createdAt":{"description":"When the invitation was created.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","email","role","createdAt"],"additionalProperties":false},"DeleteMemberResponse":{"description":"Confirmation of a successful member removal.","type":"object","properties":{"id":{"description":"Membership id that was removed.","type":"string"},"removed":{"description":"Always `true` on success — included for API symmetry with other responses.","type":"boolean","const":true}},"required":["id","removed"],"additionalProperties":false},"GlobalFilters":{"description":"Org-wide filter lists. Applied on top of per-keyword exclusions and always active.","type":"object","properties":{"negativeKeywords":{"description":"Words, phrases, or `@handle` / `$TICKER` tokens that disqualify a mention org-wide. Matched case-insensitively as whole words against the post title + body.","example":["porn","scam","@spambot"],"maxItems":2000,"type":"array","items":{"description":"Word, phrase, or `@handle` / `$TICKER` token. Trimmed; 1-200 chars.","type":"string","minLength":1,"maxLength":200}},"negativeAuthors":{"description":"Author handles/usernames to filter out org-wide. Matched case-sensitively as an exact string against the post's `author` field.","example":["spambot1"],"maxItems":2000,"type":"array","items":{"description":"Author handle/username exactly as it appears on `mention.author`. Don't add a leading `@` unless the platform does (Twitter stores handles without `@`; Reddit stores usernames as-is; YouTube uses channel titles).","type":"string","minLength":1,"maxLength":200}},"negativeSubreddits":{"description":"Subreddit names to exclude. `r/` prefix is optional — both `nextjs` and `r/nextjs` are accepted and stored in canonical form. Matched case-insensitively.","example":["theredpill","r/unpopularopinion"],"maxItems":2000,"type":"array","items":{"description":"Subreddit name. Both `nextjs` and `r/nextjs` work — the `r/` prefix is stripped on input. Stored and matched case-insensitively without the prefix.","type":"string","minLength":1,"maxLength":50,"pattern":"^[A-Za-z0-9_]+$"}},"positiveSubreddits":{"description":"Subreddit allowlist. `r/` prefix is optional. When non-empty, Reddit mentions are restricted to these subreddits only; empty array means all subreddits pass.","example":["nextjs","r/vercel"],"maxItems":2000,"type":"array","items":{"description":"Subreddit name. Both `nextjs` and `r/nextjs` work — the `r/` prefix is stripped on input. Stored and matched case-insensitively without the prefix.","type":"string","minLength":1,"maxLength":50,"pattern":"^[A-Za-z0-9_]+$"}},"negativeRepos":{"description":"GitHub repos to exclude in `owner/name` form. Matched case-insensitively against the repo parsed from the GitHub URL.","example":["acme/spam-repo"],"maxItems":2000,"type":"array","items":{"description":"GitHub repo in `owner/name` form (e.g. `vercel/next.js`).","type":"string","minLength":3,"maxLength":140}}},"required":["negativeKeywords","negativeAuthors","negativeSubreddits","positiveSubreddits","negativeRepos"],"additionalProperties":false},"AddGlobalFilterValuesResponse":{"type":"object","properties":{"list":{"$ref":"#/components/schemas/FilterListName"},"added":{"description":"Values that actually landed, in stored form. Read back from the committed row, so it never names a value that was not persisted.","type":"array","items":{"type":"string"}},"dropped":{"description":"Values refused because they already sit in the mutually-exclusive counterpart list. Values that were merely already present are in neither array.","type":"array","items":{"type":"string"}},"values":{"description":"The full list after the change, as committed.","type":"array","items":{"type":"string"}}},"required":["list","added","dropped","values"],"additionalProperties":false},"RemoveGlobalFilterValuesResponse":{"type":"object","properties":{"list":{"$ref":"#/components/schemas/FilterListName"},"removed":{"description":"Entries that left the list, exactly as they were stored.","type":"array","items":{"type":"string"}},"values":{"description":"The full list after the change, as committed.","type":"array","items":{"type":"string"}}},"required":["list","removed","values"],"additionalProperties":false},"TagListResponse":{"description":"List of tag names usable with the mentions `tag` filter. Returns the union of tags this organization has seen on its mentions and a conventional fallback set (common AI-assigned categories like `competitor_mention`, `bug_report`, `customer_testimonial`, …). Deduped and sorted alphabetically.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Tag"}}},"required":["data"],"additionalProperties":false},"Tag":{"description":"An AI-assigned topical label applied to one or more mentions. Free-form string — pass any value here to `tag` / `!tag` filters on `POST /api/v2/mentions`.","example":"competitor_mention","type":"string"},"SlackChannelsResponse":{"description":"Slack channels matching the query, with pagination cursor.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SlackChannel"}},"pagination":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["nextCursor"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"SlackChannel":{"type":"object","properties":{"id":{"description":"Slack channel id (e.g. `C0123ABC`). Pass this to feed/notification destinations.","type":"string"},"name":{"description":"Channel name without the leading `#`.","example":"releases","type":"string"}},"required":["id","name"],"additionalProperties":false},"SlackIntegrationStatus":{"description":"The org's Slack integration connection status.","type":"object","properties":{"connected":{"description":"Convenience flag — `true` only when the workspace has a Slack account link AND the app is still installed. This is what a `slack connect` poll waits on.","type":"boolean"},"hasSlackIntegration":{"description":"Whether the workspace has ever linked a Slack account.","type":"boolean"},"isInstalled":{"description":"Whether the Slack app is currently installed in the linked workspace.","type":"boolean"},"teamName":{"description":"The linked Slack workspace name, or `null` when not connected.","anyOf":[{"type":"string"},{"type":"null"}]},"hasNotificationSettings":{"description":"Whether the workspace has any Slack notification channel configuration saved.","type":"boolean"}},"required":["connected","hasSlackIntegration","isInstalled","teamName","hasNotificationSettings"],"additionalProperties":false},"SearchListResponse":{"description":"Paginated list of past searches.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SearchListItem"}},"pagination":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["nextCursor"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false},"SearchListItem":{"description":"A past on-demand search.","type":"object","properties":{"searchId":{"type":"string"},"status":{"type":"string","enum":["running","completed","failed","quota_exhausted"]},"query":{"type":"string"},"timeWindow":{"type":"string","enum":["1d","7d","30d"]},"sources":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string"},"completedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"mentionsConsumed":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["searchId","status","query","timeWindow","sources","createdAt","completedAt","mentionsConsumed"],"additionalProperties":false},"SearchCompletedResponse":{"description":"Completed on-demand search.","type":"object","properties":{"searchId":{"type":"string"},"status":{"type":"string","const":"completed"},"query":{"type":"string"},"timeWindow":{"type":"string","enum":["1d","7d","30d"]},"sources":{"type":"array","items":{"type":"string"}},"startedAt":{"type":"string"},"completedAt":{"type":"string"},"stats":{"$ref":"#/components/schemas/SearchStats"},"mentions":{"type":"array","items":{"$ref":"#/components/schemas/SearchMention"}}},"required":["searchId","status","query","timeWindow","sources","startedAt","completedAt","stats","mentions"],"additionalProperties":false},"SearchStats":{"description":"Per-stage funnel counts for a search.","type":"object","properties":{"rawMatches":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"afterDedup":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"afterRelevance":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"mentionsConsumed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"perSource":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"skippedSources":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["rawMatches","afterDedup","afterRelevance","mentionsConsumed","perSource"],"additionalProperties":false},"SearchMention":{"description":"An on-demand search result (canonical Mention + alreadyInWorkspace flag).","type":"object","properties":{"id":{"description":"Internal post id (numeric). Pass as `postId` to `PATCH /api/v2/mentions/{sourceId}`.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"sourceId":{"description":"Stable composite key for the mention across the collection pipeline (e.g. `reddit_t3_1abc234`). Use this as the path parameter for `PATCH /api/v2/mentions/{sourceId}` and as the `sourceId` field on `POST /api/v2/feedback`.","example":"reddit_t3_1abc234","type":"string"},"url":{"description":"Canonical URL to the original post.","example":"https://reddit.com/r/example/comments/abc123/...","type":"string"},"title":{"description":"Post title. `null` for platforms that don't have titles (tweets, etc.).","anyOf":[{"type":"string"},{"type":"null"}]},"body":{"description":"Post body / content. May be empty or `null` for posts with no text.","anyOf":[{"type":"string"},{"type":"null"}]},"source":{"$ref":"#/components/schemas/MentionSource"},"timestamp":{"description":"When the mention was posted. Tinybird-style datetime string (`YYYY-MM-DD HH:mm:ss.SSS`, UTC, no `Z` suffix).","example":"2026-05-06 13:35:37.000","type":"string"},"author":{"description":"Handle/username of the mention's author. `null` if not captured.","example":"jane.doe","anyOf":[{"type":"string"},{"type":"null"}]},"authorName":{"description":"Display name of the author, when distinct from the handle.","anyOf":[{"type":"string"},{"type":"null"}]},"authorAvatar":{"description":"URL to the author's avatar image. `null` if not captured.","anyOf":[{"type":"string"},{"type":"null"}]},"authorUrl":{"description":"URL to the author's profile page. `null` if not captured.","anyOf":[{"type":"string"},{"type":"null"}]},"authorFollowers":{"description":"Author's follower count at collection time. `null` if not available.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"relevance":{"description":"Relevance classification. Always one of `relevant` / `not_relevant` (unscored posts collapse to `not_relevant`).","example":"relevant","$ref":"#/components/schemas/MentionRelevance"},"relevanceComment":{"description":"Free-text justification the AI produced for its relevance verdict, when available.","anyOf":[{"type":"string"},{"type":"null"}]},"sentiment":{"description":"Sentiment classification. `null` until the AI has scored this mention.","anyOf":[{"$ref":"#/components/schemas/MentionSentiment"},{"type":"null"}]},"language":{"description":"Detected language as a full lowercase English name (`english`, `spanish`, …; `undetermined` when detection fails) — never an ISO 639-1 code.","example":"english","anyOf":[{"type":"string"},{"type":"null"}]},"tags":{"description":"AI-assigned topic tags applied to the mention.","example":["competitor_mention","pricing"],"type":"array","items":{"$ref":"#/components/schemas/MentionTag"}},"keywords":{"description":"Monitored keywords this mention matched, with id + display text.","example":[{"id":42,"keyword":"social listening"}],"type":"array","items":{"$ref":"#/components/schemas/MentionKeyword"}},"engaged":{"description":"Whether a workspace member has marked this mention as engaged-with (replied, liked on-platform, etc.).","type":"boolean"},"relevanceScore":{"description":"Raw internal relevance score (0 = high, 1 = medium, 2 = low). The public `relevance` field is derived from this. Optional; not all consumers populate it.","anyOf":[{"type":"number"},{"type":"null"}]},"feedbackRelevant":{"description":"User feedback on this mention's relevance: `1` for thumbs up, `-1` for thumbs down, `0` for no feedback. Optional.","type":"number"},"imageUrl":{"description":"URL to a preview image attached to the post, when present.","anyOf":[{"type":"string"},{"type":"null"}]},"keywordId":{"description":"Denormalized first-matched keyword id. Convenience for callers that need a single keyword reference without scanning `keywords[]`.","type":"number"},"alreadyInWorkspace":{"description":"True when this result was already collected by the workspace's monitored keywords (matched via contentHash). Flagged results do NOT count against the monthly mention quota.","type":"boolean"}},"required":["id","sourceId","url","title","body","source","timestamp","author","authorName","authorAvatar","authorUrl","authorFollowers","relevance","relevanceComment","sentiment","language","tags","keywords","engaged"],"additionalProperties":false},"SearchRunningResponse":{"description":"In-progress on-demand search (poll).","type":"object","properties":{"searchId":{"type":"string"},"status":{"type":"string","const":"running"},"query":{"type":"string"},"timeWindow":{"type":"string","enum":["1d","7d","30d"]},"sources":{"type":"array","items":{"type":"string"}},"partialStats":{"type":"object","properties":{"rawMatches":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"afterDedup":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"afterRelevance":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"mentionsConsumed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"perSource":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"skippedSources":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"additionalProperties":false},"pollUrl":{"type":"string"}},"required":["searchId","status","query","timeWindow","sources","pollUrl"],"additionalProperties":false},"GetSearchResponse":{"description":"State of a previously-started search.","type":"object","properties":{"searchId":{"type":"string"},"status":{"type":"string","enum":["running","completed","failed","quota_exhausted"]},"query":{"type":"string"},"timeWindow":{"type":"string","enum":["1d","7d","30d"]},"sources":{"type":"array","items":{"type":"string"}},"startedAt":{"type":"string"},"completedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"stats":{"anyOf":[{"type":"object","properties":{"rawMatches":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"afterDedup":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"afterRelevance":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"mentionsConsumed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"perSource":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"skippedSources":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"additionalProperties":false},{"type":"null"}]},"mentions":{"type":"array","items":{"$ref":"#/components/schemas/SearchMention"}},"pollUrl":{"type":"string"},"error":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["searchId","status","query","timeWindow","sources","startedAt","completedAt","stats","mentions"],"additionalProperties":false},"NotificationListResponse":{"description":"List of notifications for the authenticated organization.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}}},"required":["data"],"additionalProperties":false},"Notification":{"description":"A delivery rule: mentions matching the linked feed's filters are sent to the configured destinations.","type":"object","properties":{"id":{"description":"Notification ID.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"description":"Display name shown in the app and included in deliveries.","example":"Competitor mentions — Slack","type":"string"},"enabled":{"description":"When `false`, the notification is paused and nothing is delivered.","type":"boolean"},"feedId":{"description":"The feed whose filters determine which mentions trigger this notification.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"feedName":{"description":"Display name of the linked feed.","anyOf":[{"type":"string"},{"type":"null"}]},"destinations":{"description":"Configured delivery destinations (at most one per type).","type":"array","items":{"$ref":"#/components/schemas/DestinationOutput"}},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","name","enabled","feedId","feedName","destinations","createdAt","updatedAt"],"additionalProperties":false},"NotificationTestResponse":{"description":"Per-destination outcomes of a real test delivery of the latest matching mention.","type":"object","properties":{"notificationId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"results":{"description":"One entry per configured destination, in stored order.","type":"array","items":{"$ref":"#/components/schemas/NotificationTestResult"}}},"required":["notificationId","results"],"additionalProperties":false},"NotificationTestResult":{"description":"Outcome of one destination's test delivery.","type":"object","properties":{"type":{"$ref":"#/components/schemas/DestinationType"},"target":{"description":"The delivery target: email list, Slack channel IDs, or webhook URL.","type":"string"},"ok":{"description":"Whether the test delivery succeeded.","type":"boolean"},"status":{"description":"HTTP status returned by the target (webhook deliveries only).","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"error":{"description":"Failure detail when `ok` is `false`.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","target","ok"],"additionalProperties":false},"AgencyOverview":{"type":"object","properties":{"agencyId":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"plan":{"anyOf":[{"type":"string"},{"type":"null"}]},"isAnnualPlan":{"type":"boolean"},"mentions":{"type":"object","properties":{"used":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"poolCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"pool":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"resetAt":{"anyOf":[{"example":"2026-07-31T12:00:00.000Z","type":"string"},{"type":"null"}]}},"required":["used","poolCount","pool","resetAt"],"additionalProperties":false},"keywords":{"type":"object","properties":{"count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"pool":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["count","pool"],"additionalProperties":false},"workspaces":{"type":"object","properties":{"count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"max":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["count","max"],"additionalProperties":false}},"required":["agencyId","name","plan","isAnnualPlan","mentions","keywords","workspaces"],"additionalProperties":false},"AgencyWorkspaceListResponse":{"description":"All workspaces of the agency.","type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgencyWorkspace"}}},"required":["data"],"additionalProperties":false},"AgencyWorkspace":{"type":"object","properties":{"organizationId":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"plan":{"anyOf":[{"type":"string"},{"type":"null"}]},"count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"countFrom":{"anyOf":[{"example":"2026-07-31T12:00:00.000Z","type":"string"},{"type":"null"}]},"countResetAt":{"anyOf":[{"example":"2026-07-31T12:00:00.000Z","type":"string"},{"type":"null"}]},"createdAt":{"example":"2026-07-31T12:00:00.000Z","type":"string"},"mentionsLimitOverride":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"keywordsLimitOverride":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"flexPricingEnabled":{"type":"boolean"},"flexPricingBudgetCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["organizationId","name","plan","count","countFrom","countResetAt","createdAt","mentionsLimitOverride","keywordsLimitOverride","flexPricingEnabled","flexPricingBudgetCents"],"additionalProperties":false},"CreateAgencyWorkspaceResult":{"type":"object","properties":{"organizationId":{"type":"string"},"name":{"type":"string"},"companyEnriched":{"type":"boolean"}},"required":["organizationId","name","companyEnriched"],"additionalProperties":false},"DeleteAgencyWorkspaceResult":{"type":"object","properties":{"success":{"type":"boolean","const":true},"clerkOrgDeleted":{"type":"boolean"}},"required":["success","clerkOrgDeleted"],"additionalProperties":false},"AgencyWorkspaceMembers":{"type":"object","properties":{"members":{"type":"array","items":{"type":"object","properties":{"userId":{"anyOf":[{"type":"string"},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}]},"role":{"type":"string"}},"required":["userId","email","firstName","lastName","role"],"additionalProperties":false}},"invitations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"},"createdAt":{"example":"2026-07-31T12:00:00.000Z","type":"string"}},"required":["id","email","role","createdAt"],"additionalProperties":false}},"pendingGrants":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":"string"},"createdAt":{"example":"2026-07-31T12:00:00.000Z","type":"string"}},"required":["id","email","role","createdAt"],"additionalProperties":false}}},"required":["members","invitations","pendingGrants"],"additionalProperties":false},"AgencyInviteResult":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"orgId":{"type":"string"},"email":{"type":"string"},"success":{"type":"boolean"},"action":{"type":"string","enum":["granted","invited","queued"]},"error":{"type":"string"}},"required":["orgId","email","success"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false},"AgencyRemoveMemberResult":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"orgId":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"string"}},"required":["orgId","success"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false},"SimpleFiltersOutput":{"description":"Simple AND-combined filter list.","type":"object","properties":{"conditions":{"description":"Flat list of conditions, implicitly combined with AND (max 50).","maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/SimpleFilterConditionOutput"}}},"required":["conditions"],"additionalProperties":false},"SimpleFilterConditionOutput":{"type":"object","properties":{"field":{"description":"Field name to filter on. One of: `Keywords`, `Source`, `Sentiment`, `Language`, `Tags`, `RelevanceScore`, `Engaged`, `Bookmarked`, `RelevantOnly`, `TimeRange`, `TwitterFollowerCount`. Field names are case-sensitive (`Keywords`, not `Keyword`/`keywords`).","example":"Keywords","type":"string","enum":["Keywords","Source","Sentiment","Language","Tags","RelevanceScore","Engaged","Bookmarked","RelevantOnly","TimeRange","TwitterFollowerCount"]},"values":{"description":"Values to match. Most fields accept comma-separated values (IDs for `Keywords`, lowercase platform slugs for `Source` — e.g. `reddit,twitter` — and Title-case enum values for `Sentiment` — `Positive`, `Neutral`, `Negative`). `Source` and `Sentiment` values are normalized to their canonical case on write. Simple conditions always match inclusively (`IN`); to exclude values use an advanced filter with the `not in` operator.","example":"42,43","type":"string","maxLength":2000}},"required":["field","values"],"additionalProperties":false},"AdvancedFiltersOutput":{"description":"Nested filter expression — groups of conditions combined with AND/OR.","type":"object","properties":{"top_level_operator":{"description":"How the groups combine at the top level.","type":"string","enum":["AND","OR"]},"groups":{"description":"One or more filter groups (max 50).","maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/AdvancedFilterGroupOutput"}}},"required":["top_level_operator","groups"],"additionalProperties":false},"AdvancedFilterGroupOutput":{"type":"object","properties":{"group_operator":{"description":"How conditions *within* this group combine.","type":"string","enum":["AND","OR"]},"conditions":{"description":"Conditions inside this group (max 50).","maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/AdvancedFilterConditionOutput"}},"isSingleCondition":{"description":"UI hint. When `true`, the group was created from a single condition in the builder.","type":"boolean"}},"required":["group_operator","conditions"],"additionalProperties":false},"AdvancedFilterConditionOutput":{"type":"object","properties":{"field":{"description":"Field name to filter on. One of: `Keywords`, `Source`, `Sentiment`, `Language`, `Tags`, `RelevanceScore`, `Engaged`, `Bookmarked`, `RelevantOnly`, `TimeRange`, `TwitterFollowerCount`. Field names are case-sensitive (`Keywords`, not `Keyword`/`keywords`).","type":"string","enum":["Keywords","Source","Sentiment","Language","Tags","RelevanceScore","Engaged","Bookmarked","RelevantOnly","TimeRange","TwitterFollowerCount"]},"operator":{"description":"Comparison operator. `in`/`not in` work with comma-separated `values`; `equals`/`=`/`>=`/`<=` treat `values` as a single scalar.","type":"string","enum":["in","not in","equals",">=","<=","="]},"values":{"description":"Comma-separated values for `in`/`not in`, single value for comparisons.","example":"42,43","type":"string","maxLength":2000}},"required":["field","operator","values"],"additionalProperties":false},"DestinationOutput":{"description":"One delivery configuration. The relevant `*Destination` sub-object is populated based on `type`.","type":"object","properties":{"type":{"$ref":"#/components/schemas/DestinationType"},"frequency":{"$ref":"#/components/schemas/DestinationFrequency"},"deliveryMode":{"description":"`batch` sends one digest per period; `individual` sends one message per matching mention. Ignored on `WEBHOOK` (always individual).","type":"string","enum":["batch","individual"]},"time":{"description":"Wall-clock time for daily/weekly delivery, `HH:mm` 24-hour, zero-padded (`00:00`–`23:59`). Requests with any other format are rejected.","example":"09:00","type":"string","maxLength":16},"timezone":{"description":"IANA timezone (e.g. `Europe/Madrid`) or UTC offset (`UTC+2:00`) paired with `time`. Requests with unknown timezones are rejected.","example":"UTC","type":"string","maxLength":64},"dayOfWeek":{"description":"0 (Sunday) through 6 (Saturday). Required for `frequency=weekly`.","type":"integer","minimum":0,"maximum":6},"emailDestination":{"description":"Present when `type=EMAIL`.","type":"object","properties":{"emails":{"$ref":"#/components/schemas/EmailList"}},"required":["emails"],"additionalProperties":false},"slackDestination":{"description":"Present when `type=SLACK`.","type":"object","properties":{"channels":{"description":"Comma-separated Slack channel IDs, e.g. `C0123ABC,C0456DEF`. Max 191 characters (the column width).","type":"string","minLength":1,"maxLength":191},"channelNamesMap":{"description":"Optional comma-separated `id:name` pairs matching `channels` — used for UI display.","type":"string","maxLength":8000}},"required":["channels"],"additionalProperties":false},"webhookDestination":{"description":"Present when `type=WEBHOOK`.","type":"object","properties":{"url":{"example":"https://example.com/webhook","description":"HTTP(S) URL to POST events to.","$ref":"#/components/schemas/HttpUrl"}},"required":["url"],"additionalProperties":false}},"required":["type","frequency"],"additionalProperties":false},"ApiFiltersOutput":{"description":"Mention filter object. Accepts the simple flat form (`{ source: [...] }`) or the advanced group form (`{ operator: \"AND\", groups: [...] }`). Matches the shape returned by `POST /api/v2/ai/filter-wizard`, so the wizard output plugs straight into this field. `endDate` must be on or after `startDate`.","anyOf":[{"$ref":"#/components/schemas/SimpleApiFilters"},{"$ref":"#/components/schemas/AdvancedApiFiltersOutput"}]},"AdvancedApiFiltersOutput":{"description":"Groups of conditions combined with AND/OR. Use this shape when a simple field-to-values map isn't enough (e.g. mixed OR logic across fields).","type":"object","properties":{"operator":{"description":"How groups combine at the top level.","type":"string","enum":["AND","OR"]},"groups":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/ApiAdvancedGroupOutput"}},"minXFollowers":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxXFollowers":{"type":"integer","minimum":0,"maximum":9007199254740991},"startDate":{"description":"Inclusive ISO 8601 lower bound applied to all groups.","type":"string","maxLength":64},"endDate":{"description":"Inclusive ISO 8601 upper bound applied to all groups.","type":"string","maxLength":64}},"required":["operator","groups"],"additionalProperties":false},"ApiAdvancedGroupOutput":{"type":"object","properties":{"operator":{"description":"How conditions inside this group combine.","default":"AND","type":"string","enum":["AND","OR"]},"conditions":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/ApiAdvancedConditionOutput"}}},"required":["operator","conditions"],"additionalProperties":false},"ApiAdvancedConditionOutput":{"description":"A single condition inside an advanced filter group. Object with exactly one key.","anyOf":[{"$ref":"#/components/schemas/ApiArrayConditionOutput"},{"$ref":"#/components/schemas/ApiNumericCondition"},{"$ref":"#/components/schemas/ApiDateCondition"}]},"ApiArrayConditionOutput":{}},"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","description":"Clerk API key. Create one in Settings → API Keys. Pass as `Authorization: Bearer <key>`."}}}}