{"openapi":"3.0.0","paths":{"/v1/workspace":{"get":{"description":"Returns the brand and business context for the workspace your API key belongs to: brand name, domain, what the business does, who it sells to, tone of voice, industry, and the country and language it targets.\n\nThere is no path parameter. A key belongs to exactly one workspace, and this returns that one.\n\nRead this first if you are deciding what work is worth doing. A keyword, a cited page or a competitor backlink is only an opportunity in the context of a particular business, and every other endpoint assumes you already know what that business is.\n\nFields are read-only and any of them can be null. They shape article generation and the AI visibility analysis, so they are edited in the RankSpot dashboard rather than here.","operationId":"WorkspaceController_find","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceDto"}}}}},"security":[{"bearer":[]}],"summary":"Get the workspace","tags":["Workspace"]}},"/v1/keywords":{"post":{"description":"Adds one or more keywords to your workspace. Duplicates are silently skipped. Returns the count of newly inserted keywords.","operationId":"KeywordsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeywordsDto"}}}},"responses":{"201":{"description":"`{ count: number }` — number of newly inserted keywords (duplicates excluded)."}},"security":[{"bearer":[]}],"summary":"Add keywords","tags":["Keywords"]},"get":{"description":"Returns a paginated list of keywords. Defaults to the worklist: keywords with no article planned and none written (`type=new`), the same default the searches list uses. State is derived from the actions and articles linked to the keyword, so it moves on its own. Pass `type=planned`, `type=processed`, `type=all` for every non-archived keyword, or `type=archived`. Filter by `keyword` substring or `competitorId`. Sort by any scored field.","operationId":"KeywordsController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"keyword","required":false,"in":"query","description":"Filter by keyword substring","schema":{"type":"string"}},{"name":"competitorId","required":false,"in":"query","description":"Filter by competitor ID","schema":{"type":"string"}},{"name":"sortBy","required":false,"in":"query","schema":{"default":"compositeScore","type":"string","enum":["opportunityIndex","competitionIndex","searchVolume","compositeScore","aiScore"]}},{"name":"sortOrder","required":false,"in":"query","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"type","required":false,"in":"query","description":"new: the worklist, no article planned and none written (default) | planned: has a write_article action but no article yet | processed: has a linked article | all: every non-archived keyword | archived: archived keywords","schema":{"default":"new","type":"string","enum":["all","planned","processed","new","archived"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/KeywordDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List keywords","tags":["Keywords"]}},"/v1/keywords/{id}":{"delete":{"description":"Soft-deletes a keyword by setting its archived date. Archived keywords are excluded from the default list but can be retrieved with `type=archived`. Use `/unarchive` to restore.","operationId":"KeywordsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Keyword not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a keyword","tags":["Keywords"]}},"/v1/keywords/{id}/unarchive":{"patch":{"description":"Restores a previously archived keyword so it appears in the default list again.","operationId":"KeywordsController_unarchive","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Keyword not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Unarchive a keyword","tags":["Keywords"]}},"/v1/keywords/{id}/cluster":{"get":{"description":"Returns keywords that are semantically similar to the given seed keyword. Useful for grouping related keywords under a single content topic. Returns an empty array if the seed keyword has not been semantically indexed yet.","operationId":"KeywordsController_cluster","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClusterKeywordDto"}}}}},"404":{"description":"Keyword not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Get cluster keywords","tags":["Keywords"]}},"/v1/competitors":{"post":{"description":"Adds a competitor domain to your workspace. After adding, RankSpot will begin discovering keywords and backlinks for this competitor on the next background sync — results will not appear immediately.\n\nIf RankSpot already discovered the domain in AI answers (`scope=discovered`), this promotes that brand to tracked and keeps the mentions already attributed to it, rather than creating a second row.\n\nThe number of competitors you can add is limited by your subscription plan. Returns `400` with `type: \"competitor-limit-exceeded\"` if the limit is reached, or `type: \"competitor-already-exists\"` if the domain is already tracked.","operationId":"CompetitorsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompetitorDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompetitorDto"}}}},"400":{"description":"`competitor-limit-exceeded` — plan limit reached. `competitor-already-exists` — domain already tracked in this workspace."}},"security":[{"bearer":[]}],"summary":"Add a competitor","tags":["Competitors"]},"get":{"description":"Returns a paginated list of competitor domains in your workspace. `scope=tracked` (the default) is ordered by creation date descending; `discovered` and `all` are ordered by mention count descending.","operationId":"CompetitorsController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"scope","required":false,"in":"query","description":"`tracked` (default) returns the brands RankSpot is collecting keywords and backlinks for. `discovered` returns brands it found in AI answers but is not tracking, which is the set worth reviewing. `all` returns both.","schema":{"default":"tracked","type":"string","enum":["tracked","discovered","all"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/CompetitorDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List competitors","tags":["Competitors"]}},"/v1/competitors/{id}":{"delete":{"description":"Removes a competitor from your workspace: it stops being tracked and no longer appears in any list, at any scope. The AI answers already attributed to it are kept, so past visibility scores do not change — deleting the row would take them with it. Adding the same domain again restores it. Associated keywords and backlinks are preserved either way.","operationId":"CompetitorsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Competitor not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Remove a competitor","tags":["Competitors"]}},"/v1/forum-opportunities":{"post":{"description":"Adds a forum thread or community post as a link-building opportunity. The `url` must be unique per workspace — if the same URL is submitted again, the existing record is returned unchanged (idempotent). Optionally associate the opportunity with a tracked competitor via `competitorId`.","operationId":"ForumOpportunitiesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateForumOpportunityDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForumOpportunityDto"}}}}},"security":[{"bearer":[]}],"summary":"Create a forum opportunity","tags":["Forum Opportunities"]},"get":{"description":"Returns a paginated list of forum opportunities. Archived items are never included. Filter by `status` (comma-separated or repeated: `?status=new,processed`) or by `competitorId`.","operationId":"ForumOpportunitiesController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"status","required":false,"in":"query","description":"Filter by status. Accepts comma-separated or repeated params: ?status=new,processed","schema":{"type":"array","items":{"$ref":"#/components/schemas/ProcessedStatus"}}},{"name":"competitorId","required":false,"in":"query","description":"Filter by competitor ID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/ForumOpportunityDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List forum opportunities","tags":["Forum Opportunities"]}},"/v1/forum-opportunities/{id}":{"patch":{"description":"Updates the `status` of a forum opportunity. Set to `processed` once you have engaged with the thread, or back to `new` to requeue it.","operationId":"ForumOpportunitiesController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateForumOpportunityDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForumOpportunityDto"}}}},"404":{"description":"Forum opportunity not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update a forum opportunity","tags":["Forum Opportunities"]},"delete":{"description":"Soft-deletes the forum opportunity by setting its archived date. Archived items are excluded from the list endpoint.","operationId":"ForumOpportunitiesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Forum opportunity not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a forum opportunity","tags":["Forum Opportunities"]}},"/v1/people-also-ask":{"get":{"description":"Returns a paginated list of \"People also ask\" questions discovered from search results for your tracked keywords. Questions are auto-populated by RankSpot — they cannot be created via the API. Archived questions are never returned. Filter by `status` to separate new from already-processed questions.","operationId":"PeopleAlsoAskController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"status","required":false,"in":"query","description":"Filter by status. Accepts comma-separated or repeated params: ?status=new,processed","schema":{"type":"array","items":{"$ref":"#/components/schemas/ProcessedStatus"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/PeopleQuestionDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List questions","tags":["People Also Ask"]}},"/v1/people-also-ask/{id}":{"patch":{"description":"Updates the `status` of a question. Mark it `processed` once you have used or addressed the question (e.g. added it as an FAQ in an article), or set it back to `new` to reconsider it.","operationId":"PeopleAlsoAskController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePeopleQuestionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeopleQuestionDto"}}}},"404":{"description":"Question not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update a question","tags":["People Also Ask"]},"delete":{"description":"Soft-deletes the question by setting its archived date. Archived questions are excluded from the list endpoint.","operationId":"PeopleAlsoAskController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Question not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a question","tags":["People Also Ask"]}},"/v1/backlinks":{"get":{"description":"Returns a paginated list of backlinks. Use `type` to switch between competitor backlinks (default), your own backlinks, or archived ones. Combine `type=competitors` with `competitorId` to scope results to a single competitor. Filter by `domainFrom` substring to search for a specific linking domain.","operationId":"BacklinksController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"domainFrom","required":false,"in":"query","description":"Filter by domain_from substring","schema":{"type":"string"}},{"name":"sortBy","required":false,"in":"query","schema":{"default":"domainFromRank","type":"string","enum":["domainFromRank","firstSeen","backlinkSpamScore"]}},{"name":"sortOrder","required":false,"in":"query","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"competitorId","required":false,"in":"query","description":"Filter by competitor ID (UUID). Only applies when type is \"competitors\".","schema":{"type":"string"}},{"name":"type","required":false,"in":"query","description":"competitors: unprocessed competitor backlinks (default) | mine: own backlinks only | processed: processed backlinks | archived: archived backlinks","schema":{"default":"competitors","type":"string","enum":["competitors","mine","processed","archived"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/BacklinkDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List backlinks","tags":["Backlinks"]}},"/v1/backlinks/{id}":{"delete":{"description":"Soft-deletes a backlink by setting its archived date. Archived backlinks are excluded from the default list but can be retrieved with `type=archived`. Use `/unarchive` to restore.","operationId":"BacklinksController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Backlink not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a backlink","tags":["Backlinks"]}},"/v1/backlinks/{id}/unarchive":{"patch":{"description":"Restores a previously archived backlink so it appears in the default list again.","operationId":"BacklinksController_unarchive","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Backlink not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Unarchive a backlink","tags":["Backlinks"]}},"/v1/backlinks/{id}/update":{"patch":{"description":"Mark a backlink as processed or new. Use this to track which competitor backlink opportunities you have already acted on.","operationId":"BacklinksController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBacklinkDto"}}}},"responses":{"204":{"description":""},"404":{"description":"Backlink not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update backlink status","tags":["Backlinks"]}},"/v1/actions":{"post":{"description":"`type` is required and has no default, and it decides which other fields apply:\n\n- `write_article` — plan a piece of content, then call POST /v1/actions/:id/generate to write it. Requires no target.\n- `update_article` — any edit to an existing article, with the instruction in additionalInstructions. Requires `articleId`.\n- `earn_link` — go after a referring domain your competitors have. Requires `backlinkId`.\n- `get_cited` — get onto a page AI engines already cite. Requires `citationId`.\n- `reply_thread` — reply on a cited Reddit or Quora thread. Requires `citationId`.\n- `record_video` — make a video of your own, since you cannot be added to someone else's. Requires `citationId`.\n- `track_competitor` — start tracking a brand RankSpot discovered in AI answers. Requires `competitorId`.\n- `add_prompt` — track a question buyers ask that no existing prompt covers. Requires no target.\n- `publish_article` — push a finished article to the workspace integrations. Requires `articleId`.\n- `request_indexing` — ask Google to index a published page it has missed. Requires `articleId`.\n- `other` — anything you just want written down. Requires no target.\n\n`slug`, `categoryId` and `keywordIds` apply to `write_article` only; `fanoutQueryIds` to `write_article`, `update_article` and `add_prompt`. They are dropped rather than rejected on the other types. A missing or out-of-workspace target is a `400`. Actions are appended to the end of the internal ordering.\n\n`shortDescription` is the line shown under the title in a list and is usually the only text anyone reads — lead with the evidence rather than restating the title. `description` is the fuller explanation shown when the action is opened, except on `write_article` where it is the generation brief.","operationId":"ActionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateActionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDto"}}}},"400":{"description":"`action-title-already-exists` — the title is taken by another action. `action-slug-already-exists` — the slug is taken by an action or an article. `action-reference-not-found` — a target, category, keyword or fanout query id is not in this workspace. `action-target-occupied` — this type already has an open action for that target."}},"security":[{"bearer":[]}],"summary":"Create an action","tags":["Actions"]},"get":{"description":"Returns a paginated list of actions ordered by position then creation date. Filter by `status` and `types`, both of which accept comma-separated or repeated values, or a `search` substring matching the title or description. Archived actions are never returned.","operationId":"ActionsController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"search","required":false,"in":"query","description":"Search by title or description substring","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by status. Accepts comma-separated or repeated params: ?status=new,in_progress","schema":{"type":"array","items":{"$ref":"#/components/schemas/ActionStatus"}}},{"name":"types","required":false,"in":"query","description":"Filter by kind of work. Accepts comma-separated or repeated params: ?types=write_article. Omit for every type. Validated as strings rather than against the enum, so an unknown value returns nothing instead of a 400 once more types exist.","schema":{"type":"array","items":{"$ref":"#/components/schemas/ActionType"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/ActionDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List actions","tags":["Actions"]}},"/v1/actions/{id}":{"get":{"description":"Returns a single action by ID, including its linked keywords and the ID of the generated article (if one exists).","operationId":"ActionsController_findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDto"}}}},"404":{"description":"Action not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Get an action","tags":["Actions"]},"patch":{"description":"Edits an action's content — `title`, `slug`, `shortDescription`, `description`, `additionalInstructions`, `categoryId`, `keywordIds` — or its bookkeeping: `status` and `archived`. Passing `keywordIds` replaces the full set of linked keywords.\n\nContent can only be edited while `status` is `new`; once an executor has started the action is locked and a content edit returns `action-locked`. Bookkeeping stays open: `archived` at any point, and `status` on anything that is not currently `in_progress` — a running generator is reading the row, so reopening or closing it returns `action-in-progress`.\n\n`status: \"processed\"` stamps `completedAt` and marks the citation or backlink behind the action handled; `status: \"new\"` clears `completedAt`, puts that evidence back on the worklist and undoes a dismissal.","operationId":"ActionsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateActionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDto"}}}},"400":{"description":"`action-locked` — content can only be edited while the action is `new`. `action-in-progress` — `status` cannot be changed while an executor is running. `action-title-already-exists`, `action-slug-already-exists` and `action-reference-not-found` apply here as they do on create."},"404":{"description":"Action not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update an action","tags":["Actions"]},"delete":{"description":"Permanently deletes an action. This is a hard delete and cannot be undone. If an article was generated from it, the article is preserved and only the link is removed.","operationId":"ActionsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Action not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Delete an action","tags":["Actions"]}},"/v1/actions/{id}/generate":{"post":{"description":"Triggers AI article generation. The action must be `type: \"write_article\"` with `status: \"new\"`. Generation is asynchronous and takes ~5-10 minutes: the article is created up front with status `generating`, so this returns `{ success, articleId, slug, url }` immediately and `url` opens it in RankSpot, live while it is written. Poll `GET /actions/:id` until `status` is `processed`, or `GET /articles/:id` until the article is `generated`. Respects your plan's article generation limit.","operationId":"ActionsController_generateArticle","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateArticleResponseDto"}}}},"400":{"description":"`action-type-not-generatable` — only `write_article` produces an article. The action having already started, or the plan's article limit being reached, is reported by the generation backend."},"404":{"description":"Action not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Generate an article for an action","tags":["Actions"]}},"/v1/categories":{"post":{"description":"Creates a new category in your workspace. Category names must be unique within a workspace. A default color is assigned automatically.","operationId":"CategoriesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCategoryDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryDto"}}}},"400":{"description":"A category with this name already exists in the workspace."}},"security":[{"bearer":[]}],"summary":"Create a category","tags":["Categories"]},"get":{"description":"Returns a paginated list of all categories in your workspace, ordered by creation date descending.","operationId":"CategoriesController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/CategoryDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List categories","tags":["Categories"]}},"/v1/categories/{id}":{"patch":{"description":"Rename a category. The new name must be unique within the workspace.","operationId":"CategoriesController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCategoryDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryDto"}}}},"400":{"description":"A category with this name already exists in the workspace."},"404":{"description":"Category not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update a category","tags":["Categories"]},"delete":{"description":"Permanently deletes a category. Actions and articles that were assigned to this category will have their `categoryId` set to null.","operationId":"CategoriesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Category not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Delete a category","tags":["Categories"]}},"/v1/articles":{"get":{"description":"Returns a paginated list of articles in your workspace. Filter by `search` (title or description substring), `slug` (slug substring, which resolves a published URL back to its article), `status` or `categoryId`. The `contentHtml` field is omitted from list responses to keep payloads small — fetch a single article by ID to get the full HTML content.","operationId":"ArticlesController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"search","required":false,"in":"query","description":"Filter by title or description substring, case-insensitive. Use this to check whether an article on a subject already exists before writing a new one.","schema":{"example":"screen recording","type":"string"}},{"name":"slug","required":false,"in":"query","description":"Filter by slug substring, case-insensitive. A full slug matches exactly one article, since slugs are unique per workspace, and a partial one matches every article whose slug contains it.\n\nUse this to go from a published URL back to its article: pass the last path segment. Matching on a substring rather than the whole string means a trailing slash, a file extension or a date prefix in the URL path does not stop it resolving.","schema":{"example":"how-to-start-a-blog","type":"string"}},{"name":"indexed","required":false,"in":"query","description":"Filter by whether Google has indexed the page. `false` is the set worth acting on: pages that are live but invisible in search. Combine with `status=published`, since an article that never reached your site cannot be indexed and would otherwise pad the results.","schema":{"type":"boolean"}},{"name":"status","required":false,"in":"query","description":"Filter by status","schema":{"type":"string","enum":["draft","generating","generated","published"]}},{"name":"categoryId","required":false,"in":"query","description":"Filter by category ID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/ArticleDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List articles","tags":["Articles"]}},"/v1/articles/{id}":{"get":{"description":"Returns a single article including its full `contentHtml`. Articles are identified by UUID.","operationId":"ArticlesController_findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDto"}}}},"404":{"description":"Article not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Get an article","tags":["Articles"]},"patch":{"description":"Update metadata such as `title`, `description`, `slug`, `coverImageUrl`, or `categoryId`. The `slug` must be unique within your workspace. Article content and status are managed by RankSpot and cannot be changed via the API.","operationId":"ArticlesController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDto"}}}},"400":{"description":"Slug already taken in this workspace."},"404":{"description":"Article not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update an article","tags":["Articles"]},"delete":{"description":"Permanently deletes an article. This action cannot be undone. Associated action and keyword links are preserved.","operationId":"ArticlesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Article not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Delete an article","tags":["Articles"]}},"/v1/ai-visibility/prompts":{"get":{"description":"Returns the prompts asked on every AI visibility run. Archived prompts are excluded by default; pass `type=archived` for those instead.","operationId":"AiPromptsController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"type","required":false,"in":"query","description":"all: prompts currently being tracked (default) | archived: archived prompts","schema":{"default":"all","type":"string","enum":["all","archived"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/AiPromptDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List tracked prompts","tags":["AI Visibility"]},"post":{"description":"Adds a prompt to the daily run. Resubmitting a prompt you archived restores it rather than failing. Results are not available immediately: the prompt is asked on the next scheduled run.","operationId":"AiPromptsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAiPromptDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiPromptDto"}}}},"400":{"description":"`ai-prompt-limit-exceeded` when the plan allowance is used up, or `ai-prompt-already-exists` when the same text is already tracked."}},"security":[{"bearer":[]}],"summary":"Track a new prompt","tags":["AI Visibility"]}},"/v1/ai-visibility/prompts/{id}":{"patch":{"description":"Updates the `text` of a tracked prompt. The run history stays attached: it is the same tracked question, reworded. Add a new prompt for a different question.","operationId":"AiPromptsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAiPromptDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiPromptDto"}}}},"404":{"description":"Prompt not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Edit a prompt","tags":["AI Visibility"]},"delete":{"description":"Archives rather than deletes: the run history is the only source for the reporting, so removing it would rewrite past results. An archived prompt stops being run and frees its allowance slot.","operationId":"AiPromptsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Prompt not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a prompt","tags":["AI Visibility"]}},"/v1/ai-visibility/prompts/{id}/unarchive":{"patch":{"description":"Puts the prompt back into the daily run. It takes an allowance slot again, so it is refused when the workspace is already at its limit.","operationId":"AiPromptsController_unarchive","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"400":{"description":"`ai-prompt-limit-exceeded`."},"404":{"description":"Prompt not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Restore an archived prompt","tags":["AI Visibility"]}},"/v1/ai-visibility/responses":{"get":{"description":"Captured answers for your tracked prompts, newest first. Covers the full history unless you narrow it with `startDate` and `endDate`. Runs that failed or are still in flight are not returned, so an absent response is never counted as a miss.","operationId":"AiResponsesController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"startDate","required":false,"in":"query","description":"Inclusive start of the period, `YYYY-MM-DD` (UTC). Omit for no lower bound.","schema":{"example":"2026-07-01","type":"string"}},{"name":"endDate","required":false,"in":"query","description":"Inclusive end of the period, `YYYY-MM-DD` (UTC). Omit for no upper bound.","schema":{"example":"2026-07-28","type":"string"}},{"name":"platform","required":false,"in":"query","description":"Only these platforms. Repeat the parameter or pass a comma-separated list. Omit for all of them.","schema":{"type":"array","items":{"$ref":"#/components/schemas/AiPlatform"}}},{"name":"promptId","required":false,"in":"query","description":"Only responses to this tracked prompt.","schema":{"type":"string"}},{"name":"mentioned","required":false,"in":"query","description":"Filter on `ownBrandMentioned`. true: only responses that named your own brand | false: only those that did not. Omit for both.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/AiResponseDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List AI responses","tags":["AI Visibility"]}},"/v1/ai-visibility/responses/{id}":{"get":{"description":"The full answer as markdown, the pages it cited in order, the searches the platform ran, and every brand it named with its rank and sentiment.","operationId":"AiResponsesController_findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiResponseDetailDto"}}}},"404":{"description":"Response not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Get one AI response","tags":["AI Visibility"]}},"/v1/ai-visibility/citations":{"get":{"description":"Pages the AI platforms cited while answering your prompts, most-cited first. Defaults to the worklist: pages not yet acted on. Pass `type=processed` or `type=archived` for the others. Covers the full history unless you narrow it with `startDate` and `endDate`; filter by `domain` substring to look at one site. `citations` counts only what falls inside the requested period. Coverage is not uniform across platforms: each engine cites at its own rate.","operationId":"AiCitationsController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"startDate","required":false,"in":"query","description":"Inclusive start of the period, `YYYY-MM-DD` (UTC). Omit for no lower bound.","schema":{"example":"2026-07-01","type":"string"}},{"name":"endDate","required":false,"in":"query","description":"Inclusive end of the period, `YYYY-MM-DD` (UTC). Omit for no upper bound.","schema":{"example":"2026-07-28","type":"string"}},{"name":"domain","required":false,"in":"query","description":"Filter by domain substring, case-insensitive. `rankspot` matches `www.rankspot.ai` and `rankspot.co.uk`.","schema":{"example":"rankspot","type":"string"}},{"name":"type","required":false,"in":"query","description":"new: the worklist, pages not yet acted on (default) | processed: pages marked as handled | archived: pages hidden from the worklist","schema":{"default":"new","type":"string","enum":["new","processed","archived"]}},{"name":"sortBy","required":false,"in":"query","schema":{"default":"citations","type":"string","enum":["citations","lastSeenAt","firstSeenAt","domain"]}},{"name":"sortOrder","required":false,"in":"query","schema":{"default":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/AiCitationDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List cited pages","tags":["AI Visibility"]}},"/v1/ai-visibility/citations/{id}":{"patch":{"description":"Updates the `status` of a cited page. Set to `processed` once you have acted on the citation opportunity, or back to `new` to requeue it. Archiving is a separate axis: use `DELETE` and `/unarchive`. The returned `citations` and `platforms` are all-time totals, since a write has no period to count over.","operationId":"AiCitationsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAiCitationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiCitationDto"}}}},"404":{"description":"Cited page not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Update a citation","tags":["AI Visibility"]},"delete":{"description":"Hides a page from the default list; retrieve it with `type=archived` and restore it with `/unarchive`. It keeps counting towards the reporting, because archiving is a statement about your workflow and not about what the engines cited.","operationId":"AiCitationsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Cited page not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a cited page","tags":["AI Visibility"]}},"/v1/ai-visibility/citations/{id}/unarchive":{"patch":{"description":"Restores a previously archived page so it appears in the default list again.","operationId":"AiCitationsController_unarchive","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Cited page not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Unarchive a cited page","tags":["AI Visibility"]}},"/v1/ai-visibility/fanouts":{"get":{"description":"The searches the AI platforms ran while answering your prompts, most-run first. Where a citation tells you which page an engine trusted, a fanout query tells you what it decided it needed to know, so a query you rank for nowhere is a content gap stated in the engine's own words. Defaults to the worklist: queries with no article planned and none written. State is derived from the work linked to the query, the same rule keywords use, so it moves on its own as actions and articles appear. Pass `type=planned`, `type=processed`, `type=all` or `type=archived` for the others. Covers the full history unless you narrow it with `startDate` and `endDate`, which lists only the queries engines ran inside that period and counts `searches` over it; filter by `query` substring to look at one theme. Coverage is not uniform across platforms: not every engine reports its fanout.","operationId":"AiFanoutQueriesController_findAll","parameters":[{"name":"offset","required":false,"in":"query","schema":{"minimum":0,"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"startDate","required":false,"in":"query","description":"Inclusive start of the period, `YYYY-MM-DD` (UTC). Omit for no lower bound.","schema":{"example":"2026-07-01","type":"string"}},{"name":"endDate","required":false,"in":"query","description":"Inclusive end of the period, `YYYY-MM-DD` (UTC). Omit for no upper bound.","schema":{"example":"2026-07-28","type":"string"}},{"name":"query","required":false,"in":"query","description":"Filter by query substring, case-insensitive. Stored queries are already lower-cased, so casing here does not matter either.","schema":{"example":"screen recorder","type":"string"}},{"name":"type","required":false,"in":"query","description":"Derived from the work linked to the query, the same rule keywords use. new: the worklist, no article planned and none written (default) | planned: has a `write_article` action but no article yet | processed: has a linked article | all: every non-archived query | archived: queries hidden from the worklist","schema":{"default":"new","type":"string","enum":["all","planned","processed","new","archived"]}},{"name":"sortBy","required":false,"in":"query","schema":{"default":"searches","type":"string","enum":["searches","lastSeenAt","firstSeenAt","query"]}},{"name":"sortOrder","required":false,"in":"query","schema":{"default":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"data":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"$ref":"#/components/schemas/AiFanoutQueryDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List fanout queries","tags":["AI Visibility"]}},"/v1/ai-visibility/fanouts/{id}/cluster":{"get":{"description":"The searches that mean the same thing as this one, most-run first, with the seed leading. Engines phrase one intent many ways, so an action should carry the cluster rather than whichever phrasing you happened to pick: pass the ids straight to rankspot_create_action as `fanoutQueryIds`. Returns just the seed when nothing else is close, or when it has not been embedded yet.","operationId":"AiFanoutQueriesController_cluster","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AiFanoutQueryClusterDto"}}}}}},"security":[{"bearer":[]}],"summary":"Get semantically related searches","tags":["AI Visibility"]}},"/v1/ai-visibility/fanouts/{id}":{"delete":{"description":"Hides a query from the default list; retrieve it with `type=archived` and restore it with `/unarchive`. It keeps counting towards the reporting, because archiving is a statement about your workflow and not about what the engines ran.","operationId":"AiFanoutQueriesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Fanout query not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Archive a fanout query","tags":["AI Visibility"]}},"/v1/ai-visibility/fanouts/{id}/unarchive":{"patch":{"description":"Restores a previously archived query so it appears in the default list again.","operationId":"AiFanoutQueriesController_unarchive","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Fanout query not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Unarchive a fanout query","tags":["AI Visibility"]}},"/v1/ai-visibility/summary":{"get":{"description":"How your brand did in AI answers over a period: four headline scores, the split across platforms, and the ten most-mentioned brands with yours among them.\n\n**Requires:** `startDate` and `endDate`. Unlike the list endpoints, a summary reports on a bounded period.\n\n**Comparing periods:** call it twice with two equal-length periods and compare the scores. Equal length matters — a 30-day period against a 7-day one measures the calendar, not your visibility.\n\n**Reading a null:** every score is nullable and null means the period had no data to compute it from, which is not the same as zero. A workspace whose runs all failed scores null, not 0%. Check `responsesCounted` before quoting any of them.\n\n**What counts:** runs that failed or never completed are excluded throughout, so a provider outage does not read as a period of poor visibility.\n\n**Your own row:** `shareOfVoice` and `categoryRank` are your entry in `leaderboard`, lifted out. Your brand always appears there, even at zero mentions or outside the top ten.","operationId":"AiSummaryController_get","parameters":[{"name":"startDate","required":true,"in":"query","description":"Inclusive start of the period, `YYYY-MM-DD` (UTC).","schema":{"example":"2026-07-01","type":"string"}},{"name":"endDate","required":true,"in":"query","description":"Inclusive end of the period, `YYYY-MM-DD` (UTC).","schema":{"example":"2026-07-28","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiSummaryDto"}}}},"400":{"description":"Missing or malformed dates, or `invalid-date-range` when `startDate` is later than `endDate`."}},"security":[{"bearer":[]}],"summary":"Get AI visibility summary","tags":["AI Visibility"]}},"/v1/research/google":{"post":{"description":"A live Google search, returned as `items`: the blocks of the results page in page order, each with a `type` to branch on. `types` chooses which blocks come back and defaults to `organic` and `discussions_and_forums`; ask for `ai_overview`, `people_also_ask`, `video` or `related_searches` when you will actually read them, since those carry the full text of every answer and dwarf the organic results. Always the top 10; location and language come from the workspace so results match what the rest of RankSpot reports. Costs credits, the same whichever blocks you ask for. Returns 402 when the workspace has none left.","operationId":"ResearchController_google","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleSearchDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleSearchResultDto"}}}},"402":{"description":"`ai-credits-exhausted` — no AI credits left this billing period. The body carries `usage` with the counts behind that."},"502":{"description":"The search provider failed."}},"security":[{"bearer":[]}],"summary":"Search Google","tags":["Research"]}},"/v1/research/fetch":{"post":{"description":"Fetches one public URL and returns its main content as markdown. That is the whole response: `{ \"markdown\": \"# ...\" }`. Two providers are tried in turn, so pages that block the first one (Reddit, most notably) still come back, and which one answered is not something the caller has to care about. Costs credits at a flat rate per fetch however many providers it took, charged whenever a page comes back. Use it to settle a specific question rather than to crawl: one page, one charge. Returns 402 when the workspace has none left.","operationId":"ResearchController_fetch","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchPageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchPageResultDto"}}}},"402":{"description":"`ai-credits-exhausted` — no AI credits left this billing period. The body carries `usage` with the counts behind that."},"503":{"description":"No provider could read the page. Nothing is charged for it."}},"security":[{"bearer":[]}],"summary":"Fetch a page as text","tags":["Research"]}},"/v1/gsc/performance":{"post":{"description":"Returns clicks, impressions, CTR, and average position from Google Search Console for your connected property.\n\n**Requires:** Google Search Console connected from the RankSpot dashboard (Integrations → Google Search Console).\n\n**Dimensions:** Group results by `query` (search terms), `page` (landing pages), `country`, `device`, or `searchAppearance`. Combine up to 3 dimensions in a single request.\n\n**Date range:** Use ISO dates (`YYYY-MM-DD`). Data is available with ~2-3 day delay. Maximum range: 16 months.","operationId":"GscController_getPerformance","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GscPerformanceDto"}}}},"responses":{"200":{"description":"Performance data from Google Search Console","content":{"application/json":{"schema":{"example":{"siteUrl":"https://example.com/","startDate":"2026-01-01","endDate":"2026-01-31","dimensions":["query"],"rows":[{"keys":["rankspot seo tool"],"clicks":120,"impressions":980,"ctr":12.24,"position":3.2},{"keys":["seo keyword tracker"],"clicks":85,"impressions":1200,"ctr":7.08,"position":5.6}]}}}}}},"security":[{"bearer":[]}],"summary":"Get Search Console performance data","tags":["Search Console"]}},"/v1/gsc/inspect":{"post":{"description":"Inspects a single URL with the Google Search Console URL Inspection API and reports its index status.\n\n**Requires:** Google Search Console connected from the RankSpot dashboard (Integrations → Google Search Console). Works with the existing connection — no reconnect needed.\n\n**Reading the result:** the page is indexed when `indexStatusResult.verdict` is `PASS`. When it is not, check `indexStatusResult.coverageState` (e.g. \"Crawled - currently not indexed\", \"Discovered - currently not indexed\", \"URL is unknown to Google\") for the reason.\n\n**Quota:** The URL Inspection API is limited by Google to ~2,000 queries/day and 600 queries/minute per property.","operationId":"GscController_inspectUrl","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GscInspectUrlDto"}}}},"responses":{"200":{"description":"The raw `inspectionResult` from the URL Inspection API. The page is indexed when `indexStatusResult.verdict` is `PASS`; otherwise read `indexStatusResult.coverageState` for the reason.","content":{"application/json":{"schema":{"example":{"inspectionResultLink":"https://search.google.com/search-console/inspect?resource_id=sc-domain:example.com&id=...","indexStatusResult":{"verdict":"PASS","coverageState":"Submitted and indexed","robotsTxtState":"ALLOWED","indexingState":"INDEXING_ALLOWED","lastCrawlTime":"2026-06-21T15:58:20Z","pageFetchState":"SUCCESSFUL","googleCanonical":"https://www.example.com/blog","userCanonical":"https://www.example.com/blog","referringUrls":["https://www.example.com/blog/"],"crawledAs":"MOBILE"},"mobileUsabilityResult":{"verdict":"VERDICT_UNSPECIFIED"}}}}}}},"security":[{"bearer":[]}],"summary":"Check whether a page is indexed","tags":["Search Console"]}},"/v1/gsc/index":{"post":{"description":"Notifies Google via the Indexing API that a URL has been added/updated (`URL_UPDATED`) or removed (`URL_DELETED`).\n\n**Requires:** A Google Search Console connection with the `indexing` OAuth scope, and the connected account must be a **verified owner** of the property. If the current connection predates indexing support, reconnect from the RankSpot dashboard (Integrations → Google Search Console) to grant the scope.\n\n**Note:** A successful response means Google received the notification — it does not guarantee or immediately confirm indexing. Use `POST /gsc/inspect` afterwards to check status.","operationId":"GscController_requestIndexing","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GscIndexUrlDto"}}}},"responses":{"201":{"description":"The raw `urlNotificationMetadata` from the Indexing API. `latestUpdate`/`latestRemove` are only present once Google has processed prior notifications for the URL.","content":{"application/json":{"schema":{"example":{"url":"https://example.com/blog/my-post","latestUpdate":{"url":"https://example.com/blog/my-post","type":"URL_UPDATED","notifyTime":"2026-06-21T12:00:00Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Submit a page for indexing","tags":["Search Console"]}}},"info":{"title":"RankSpot Public API","description":"The RankSpot Public API gives you programmatic access to your workspace data — keywords, backlinks, competitors, planned actions, articles, and more.\n\n## Authentication\n\nAll endpoints require a Bearer API key in the `Authorization` header:\n\n```\nAuthorization: Bearer <your-api-key>\n```\n\nGenerate API keys from **Settings → API Keys** in the RankSpot dashboard. Each key is scoped to a single workspace.\n\n## Pagination\n\nList endpoints return a consistent envelope:\n\n```json\n{\n  \"data\": {\n    \"total\": 120,\n    \"offset\": 0,\n    \"limit\": 20,\n    \"count\": 20,\n    \"items\": [...]\n  }\n}\n```\n\nUse `offset` and `limit` query parameters to paginate. Default limit is **20**, maximum is **100**.\n\n## Status codes\n\n| Code | Meaning |\n|------|---------|\n| 200  | Success |\n| 201  | Created |\n| 204  | No content (delete / unarchive) |\n| 400  | Validation error or business rule violation |\n| 401  | Missing or invalid API key |\n| 404  | Resource not found or belongs to another workspace |","version":"1.0","contact":{}},"tags":[{"name":"AI Visibility","description":"How AI answer engines see your brand. Track prompts, read the answers ChatGPT, Perplexity and Google gave, and work through the pages they cited. Prompts are run on a daily schedule by RankSpot, so answers appear after the next run rather than on demand."},{"name":"Articles","description":"AI-generated blog articles. Articles are created automatically when a `write_article` action is sent to generation — they cannot be created directly via the API."},{"name":"Backlinks","description":"Backlinks discovered for your domain and your competitors' domains. Backlinks are auto-synced on a background interval and cannot be created via the API."},{"name":"Categories","description":"Categories for organising actions and articles within your workspace."},{"name":"Competitors","description":"Competitor domains tracked in your workspace. Once added, keywords and backlinks for each competitor are fetched asynchronously on a background sync interval."},{"name":"Forum Opportunities","description":"Forum threads and community posts surfaced as potential link-building or engagement opportunities."},{"name":"Keywords","description":"SEO keywords tracked in your workspace, enriched with search volume, competition, and AI-generated relevance scores."},{"name":"People Also Ask","description":"\"People also ask\" questions discovered from search results for your tracked keywords."},{"name":"Actions","description":"The SEO work planned for your workspace. Each action carries a `type` saying what kind of work it is and which target it points at — see `POST /v1/actions` for the full list. Only `write_article` can be turned into an article; the rest are a person's job. Create actions yourself or let RankSpot generate them."},{"name":"Research","description":"Live lookups against the open web: a Google search and a page fetch. The only endpoints here that spend credits per call. Neither response reports what it cost — the charge is real, and your remaining balance is visible in the RankSpot dashboard. Returns 402 when the workspace is out of credits."},{"name":"Search Console","description":"Google Search Console performance data — clicks, impressions, CTR, and average position for your connected property. Requires GSC to be connected from the RankSpot dashboard."},{"name":"Workspace","description":"Who the workspace is: brand, domain, what the business does and who it sells to. Read this first — the rest of the API returns opportunities that only mean something in the context of a particular business."}],"servers":[{"url":"https://api.rankspot.ai","description":"Production"}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"WorkspaceDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"name":{"type":"string","example":"Screen Studio","nullable":true,"description":"The workspace name, which is not necessarily the brand name."},"brandName":{"type":"string","example":"Screen Studio","nullable":true,"description":"How the business refers to itself. This is the name AI visibility counts mentions of."},"domain":{"type":"string","example":"https://screen.studio/","nullable":true,"description":"The domain this workspace is about. Articles are published here, and a citation or backlink pointing at it is your own rather than an opportunity.\n\nStored as it was entered, so it may be a bare hostname (`screen.studio`) or a full URL with a scheme and a trailing slash (`https://screen.studio/`). Elsewhere in the API a domain is always a bare hostname, so strip the scheme and any trailing slash before comparing."},"businessDescription":{"type":"string","example":"A macOS app that records your screen and turns the footage into a polished video automatically.","nullable":true,"description":"What the business does, in its own words."},"targetAudience":{"type":"string","example":"Indie developers, designers and founders who publish demo videos.","nullable":true,"description":"Who the business sells to."},"benefits":{"type":"string","example":"Automatic zooms, no editing skills needed, exports in 4K.","nullable":true,"description":"What the business argues is better about it."},"toneOfVoice":{"type":"string","example":"Direct and practical, no marketing filler.","nullable":true,"description":"How generated content should sound."},"industry":{"type":"string","example":"Software","nullable":true},"location":{"type":"string","example":"US","nullable":true,"description":"ISO 3166-1 alpha-2 country the workspace targets. Search results, keyword volumes and rankings are all reported for this market."},"language":{"type":"string","example":"en","nullable":true,"description":"ISO 639-1 language content is written in."}},"required":["id"]},"CreateKeywordsDto":{"type":"object","properties":{"keywords":{"description":"List of keywords to track","example":["seo tools","keyword research"],"type":"array","items":{"type":"string"}}},"required":["keywords"]},"PaginatedDataDto":{"type":"object","properties":{"total":{"type":"number","example":100},"offset":{"type":"number","example":0},"limit":{"type":"number","example":100},"count":{"type":"number","example":10},"items":{"type":"array","items":{"type":"string"}}},"required":["total","offset","limit","count","items"]},"PaginatedResponseDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PaginatedDataDto"}},"required":["data"]},"KeywordDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"keyword":{"type":"string","example":"start blog"},"competition":{"type":"string","example":"MEDIUM","nullable":true},"competitionIndex":{"type":"number","example":60,"nullable":true},"searchVolume":{"type":"number","example":27100,"nullable":true},"opportunityIndex":{"type":"number","example":10,"nullable":true},"aiScore":{"type":"number","example":74,"nullable":true},"compositeScore":{"type":"number","example":68,"nullable":true},"competitorId":{"type":"string","example":"clx...","nullable":true},"actionIds":{"example":["clx..."],"type":"array","items":{"type":"string"}},"articleIds":{"example":["clx..."],"type":"array","items":{"type":"string"}},"createdAt":{"format":"date-time","type":"string","example":"2026-10-03T12:03:43.558Z"},"updatedAt":{"format":"date-time","type":"string","example":"2026-10-03T12:03:43.558Z"}},"required":["id","keyword","competition","competitionIndex","searchVolume","opportunityIndex","aiScore","compositeScore","actionIds","articleIds","createdAt","updatedAt"]},"ClusterKeywordDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"keyword":{"type":"string","example":"start a blog for free"}},"required":["id","keyword"]},"CreateCompetitorDto":{"type":"object","properties":{"name":{"type":"string","example":"Ahrefs"},"domain":{"type":"string","example":"ahrefs.com"}},"required":["name","domain"]},"CompetitorDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"name":{"type":"string","example":"Ahrefs"},"domain":{"type":"string","example":"ahrefs.com"},"isTracked":{"type":"boolean","description":"Whether keywords and backlinks are being collected for it."},"autoDiscovered":{"type":"boolean","description":"Found by RankSpot in an AI answer rather than added by hand."},"mentionCount":{"type":"number","description":"How many AI answers have named this brand. The ranking signal."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","domain","isTracked","autoDiscovered","mentionCount","createdAt","updatedAt"]},"CreateForumOpportunityDto":{"type":"object","properties":{"title":{"type":"string","example":"How to start a blog in 2024"},"url":{"type":"string","example":"https://reddit.com/r/blogging/comments/abc123"},"competitorId":{"type":"string","example":"clx...","description":"Associate with a competitor"}},"required":["title","url"]},"ProcessedStatus":{"type":"string","enum":["new","processed"]},"ForumOpportunityDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"title":{"type":"string","example":"How to start a blog in 2024"},"url":{"type":"string","example":"https://reddit.com/r/blogging/comments/abc123"},"status":{"example":"new","allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]},"competitorId":{"type":"string","example":"clx...","nullable":true},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","title","url","status","createdAt","updatedAt"]},"UpdateForumOpportunityDto":{"type":"object","properties":{"status":{"allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]}},"required":["status"]},"PeopleQuestionDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"question":{"type":"string","example":"How do I start a blog?"},"status":{"example":"new","allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]},"articleId":{"type":"string","example":"clx...","nullable":true,"description":"The article that answers this question, once one exists."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","question","status","createdAt","updatedAt"]},"UpdatePeopleQuestionDto":{"type":"object","properties":{"status":{"allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]}},"required":["status"]},"BacklinkDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"domainFrom":{"type":"string","example":"producthunt.com"},"urlFrom":{"type":"string","example":"https://www.producthunt.com/products/rankspot"},"urlTo":{"type":"string","example":"https://www.rankspot.ai/"},"domainTo":{"type":"string","example":"www.rankspot.ai"},"rank":{"type":"number","example":14,"nullable":true},"pageFromRank":{"type":"number","example":29,"nullable":true},"domainFromRank":{"type":"number","example":88,"nullable":true},"dofollow":{"type":"boolean","example":true},"backlinkSpamScore":{"type":"number","example":0,"nullable":true},"anchor":{"type":"string","example":"Visit website","nullable":true},"firstSeen":{"type":"string","example":"2026-10-03T12:03:43.623Z","nullable":true},"attributes":{"example":["noopener","noreferrer"],"type":"array","items":{"type":"string"}},"competitorId":{"type":"string","example":"clx...","nullable":true},"status":{"type":"string","enum":["new","processed"],"example":"new"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","domainFrom","urlFrom","urlTo","domainTo","dofollow","attributes","status","createdAt","updatedAt"]},"UpdateBacklinkDto":{"type":"object","properties":{"status":{"type":"string","enum":["new","processed"]}},"required":["status"]},"ActionType":{"type":"string","enum":["write_article","update_article","earn_link","get_cited","reply_thread","record_video","track_competitor","add_prompt","publish_article","request_indexing","other"],"description":"What kind of work an action is, and what else the body needs:\n\n- `write_article` — plan a piece of content, then call POST /v1/actions/:id/generate to write it. Requires no target.\n- `update_article` — any edit to an existing article, with the instruction in additionalInstructions. Requires `articleId`.\n- `earn_link` — go after a referring domain your competitors have. Requires `backlinkId`.\n- `get_cited` — get onto a page AI engines already cite. Requires `citationId`.\n- `reply_thread` — reply on a cited Reddit or Quora thread. Requires `citationId`.\n- `record_video` — make a video of your own, since you cannot be added to someone else's. Requires `citationId`.\n- `track_competitor` — start tracking a brand RankSpot discovered in AI answers. Requires `competitorId`.\n- `add_prompt` — track a question buyers ask that no existing prompt covers. Requires no target.\n- `publish_article` — push a finished article to the workspace integrations. Requires `articleId`.\n- `request_indexing` — ask Google to index a published page it has missed. Requires `articleId`.\n- `other` — anything you just want written down. Requires no target."},"CreateActionDto":{"type":"object","properties":{"type":{"example":"write_article","description":"What kind of work an action is, and what else the body needs:\n\n- `write_article` — plan a piece of content, then call POST /v1/actions/:id/generate to write it. Requires no target.\n- `update_article` — any edit to an existing article, with the instruction in additionalInstructions. Requires `articleId`.\n- `earn_link` — go after a referring domain your competitors have. Requires `backlinkId`.\n- `get_cited` — get onto a page AI engines already cite. Requires `citationId`.\n- `reply_thread` — reply on a cited Reddit or Quora thread. Requires `citationId`.\n- `record_video` — make a video of your own, since you cannot be added to someone else's. Requires `citationId`.\n- `track_competitor` — start tracking a brand RankSpot discovered in AI answers. Requires `competitorId`.\n- `add_prompt` — track a question buyers ask that no existing prompt covers. Requires no target.\n- `publish_article` — push a finished article to the workspace integrations. Requires `articleId`.\n- `request_indexing` — ask Google to index a published page it has missed. Requires `articleId`.\n- `other` — anything you just want written down. Requires no target.","allOf":[{"$ref":"#/components/schemas/ActionType"}]},"title":{"type":"string","example":"How to Start a Blog in 2024","description":"What to do. Shown as the card title. Required for every type."},"shortDescription":{"type":"string","example":"Cited in 6 ChatGPT answers — you are in none.","description":"The one line shown under the title in a list, and for most actions the only text anyone reads. Lead with the evidence and the numbers behind it, do not restate the title. Keep it to a single short sentence."},"description":{"type":"string","example":"A comprehensive guide for beginners.","description":"For write_article this is the brief handed to the generator. For every other type it is the fuller explanation shown when the action is opened, so it can run to a short paragraph."},"additionalInstructions":{"type":"string","example":"Focus on WordPress.","description":"Extra guidance. For update_article this carries the actual instruction, which is why every kind of article edit is one type rather than several."},"slug":{"type":"object","example":"how-to-start-a-blog","description":"write_article only; dropped on other types. Slug for the article generated from this action. If omitted or null the slug is derived from the title at generation time. Validated, not rewritten, and must be unique across actions and articles."},"categoryId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","description":"write_article only; dropped on other types. Category to file the article under."},"keywordIds":{"example":["6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"],"description":"write_article only; dropped on other types. Tracked keywords to link, which broadens the article's semantic coverage.","type":"array","items":{"type":"string"}},"articleId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","description":"update_article, publish_article and request_indexing only, and required for them. The article this is about, from GET /v1/articles. This is the same `articleId` the response returns."},"citationId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","description":"get_cited, reply_thread and record_video only, and required for them. The cited page or thread this is about."},"backlinkId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","description":"earn_link only, and required for it. The referring domain to go after."},"competitorId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","description":"track_competitor only, and required for it. The brand to start tracking."},"fanoutQueryIds":{"example":["6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"],"description":"Optional. write_article, update_article and add_prompt only; dropped on other types. The fanout searches that justified this, from rankspot_list_ai_fanout_queries. Send the whole cluster, not one phrasing of it: engines ask the same thing a dozen ways and one article answers all of them. Unlike the fields above this is not a target and not exclusive with them, so send `keywordIds` too.","type":"array","items":{"type":"string"}}},"required":["type","title"]},"ActionStatus":{"type":"string","enum":["new","in_progress","processed"],"description":"new: not started · in_progress: an executor is running · processed: finished. Same vocabulary as backlinks, citations and people-also-ask, with the extra middle state only actions can have."},"ActionCitationDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"url":{"type":"string","example":"https://zapier.com/blog/best-screen-recorders/"},"domain":{"type":"string","example":"zapier.com"},"title":{"type":"string","example":"The 12 best screen recorders","nullable":true},"isOwnDomain":{"type":"boolean","example":false,"description":"The page is on your own domain."}},"required":["id","url","domain","isOwnDomain"]},"ActionBacklinkDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"domainFrom":{"type":"string","example":"ahrefs.com"},"urlFrom":{"type":"string","example":"https://ahrefs.com/blog/seo-tools/","description":"The page carrying the competitor link."},"domainFromRank":{"type":"number","example":91,"nullable":true},"dofollow":{"type":"boolean","example":true}},"required":["id","domainFrom","urlFrom","dofollow"]},"ActionCompetitorDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"name":{"type":"string","example":"Ahrefs"},"domain":{"type":"string","example":"ahrefs.com"}},"required":["id","name","domain"]},"ActionFanoutQueryDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"query":{"type":"string","example":"best screen recording software for mac 2026"}},"required":["id","query"]},"ActionKeywordDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"keyword":{"type":"string","example":"start a blog"}},"required":["id","keyword"]},"ActionDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"type":{"example":"write_article","description":"What kind of work this is.","allOf":[{"$ref":"#/components/schemas/ActionType"}]},"title":{"type":"string","example":"How to Start a Blog in 2024"},"slug":{"type":"string","example":"how-to-start-a-blog","nullable":true},"shortDescription":{"type":"string","example":"Cited in 6 ChatGPT answers — you are in none.","nullable":true,"description":"The one line shown under the title in a list. Null on actions created before this field existed."},"description":{"type":"string","example":"A comprehensive guide for beginners.","nullable":true,"description":"The fuller explanation, shown when the action is opened. On `write_article` this is the brief handed to the generator instead."},"additionalInstructions":{"type":"string","example":"Focus on WordPress.","nullable":true},"status":{"example":"new","description":"new: not started · in_progress: an executor is running · processed: finished. Same vocabulary as backlinks, citations and people-also-ask, with the extra middle state only actions can have.","allOf":[{"$ref":"#/components/schemas/ActionStatus"}]},"completedAt":{"type":"string","nullable":true,"description":"When the work finished."},"categoryId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","nullable":true},"articleId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","nullable":true,"description":"The article this action produced (write_article) or edits (update_article)."},"citationId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","nullable":true,"description":"The cited page or thread, on get_cited and reply_thread. Null once the citation itself is gone, which does not remove the action."},"backlinkId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","nullable":true,"description":"The referring domain to go after, on earn_link."},"competitorId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94","nullable":true,"description":"The brand to start tracking, on track_competitor."},"citation":{"nullable":true,"description":"The cited page itself, on get_cited and reply_thread. Carries the `url` to open and the citation count the reason is built from.","type":"object","allOf":[{"$ref":"#/components/schemas/ActionCitationDto"}]},"backlink":{"nullable":true,"description":"The referring domain itself, on earn_link. `urlFrom` is the page carrying the competitor link.","type":"object","allOf":[{"$ref":"#/components/schemas/ActionBacklinkDto"}]},"competitor":{"nullable":true,"description":"The brand itself, on track_competitor.","type":"object","allOf":[{"$ref":"#/components/schemas/ActionCompetitorDto"}]},"fanoutQueries":{"description":"The fanout searches themselves, on the article types. Provenance rather than a target: they are what justified the work, not where it happens. A set, because engines phrase one intent many ways.","type":"array","items":{"$ref":"#/components/schemas/ActionFanoutQueryDto"}},"keywords":{"type":"array","items":{"$ref":"#/components/schemas/ActionKeywordDto"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","type","title","status","fanoutQueries","keywords","createdAt","updatedAt"]},"UpdateActionDto":{"type":"object","properties":{"title":{"type":"string","example":"How to Start a Blog in 2024"},"slug":{"type":"object","example":"how-to-start-a-blog","description":"Optional slug for the article generated from this action. Pass null to clear it and fall back to deriving the slug from the title at generation time. Must already be a valid slug (lowercase letters, numbers and single hyphens) — it is validated, not rewritten. Must be unique across actions and articles."},"shortDescription":{"type":"string","example":"Cited in 6 ChatGPT answers — you are in none.","description":"The one line shown under the title in a list."},"description":{"type":"string","example":"A comprehensive guide for beginners."},"additionalInstructions":{"type":"string","example":"Focus on WordPress."},"categoryId":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"keywordIds":{"example":["6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"],"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["new","processed"],"example":"processed","description":"Where the action stands. `processed` closes it and marks the page or link behind it handled; `new` reopens it and puts that page or link back on the worklist, undoing a dismissal too. `in_progress` is set by the executor, not here, and while an action is in it neither value is accepted: the generator is reading the row."},"archived":{"type":"boolean","example":true,"description":"Take the action off the board without deciding it, which also hides the page or link behind it from the worklist. `false` puts both back. The row and its evidence are kept either way; nothing is deleted."}}},"GenerateArticleResponseDto":{"type":"object","properties":{"success":{"type":"boolean","example":true},"articleId":{"type":"string","description":"The article being written. It exists as soon as this returns, with status `generating`.","format":"uuid"},"slug":{"type":"string","example":"how-to-choose-a-crm"},"url":{"type":"string","description":"Opens the article in RankSpot, live while it is written.","example":"https://app.rankspot.ai/articles/how-to-choose-a-crm"}},"required":["success","articleId","slug","url"]},"CreateCategoryDto":{"type":"object","properties":{"name":{"type":"string","example":"SEO"}},"required":["name"]},"CategoryDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"name":{"type":"string","example":"SEO"},"color":{"type":"string","example":"violet"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","color","createdAt","updatedAt"]},"UpdateCategoryDto":{"type":"object","properties":{"name":{"type":"string","example":"Content Marketing"}}},"ArticleDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"title":{"type":"string","example":"How to Start a Blog"},"description":{"type":"string","example":"A comprehensive guide.","nullable":true},"contentHtml":{"type":"string","example":"<h1>How to Start a Blog</h1>...","nullable":true,"description":"Only returned by GE../articles/:slug"},"status":{"type":"string","example":"generated","enum":["draft","generating","generated","published"],"description":"Derived from the list rather than repeated here, so a new status cannot be returned while the docs still deny it exists. `generated` means the article has never left RankSpot; `published` means it reached your site."},"slug":{"type":"string","example":"how-to-start-a-blog"},"coverImageUrl":{"type":"string","example":"https://cdn.example.com/cover.jpg","nullable":true},"categoryId":{"type":"string","example":"clx...","nullable":true},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string","description":"Last write of any kind, including a status change or a metadata edit. This is not a reliable answer to \"when was the content last refreshed\" — use `lastPublishedAt` for that."},"firstPublishedAt":{"type":"string","nullable":true,"description":"When the article first went live. Null means it has never been published. This is the article's publication date: unlike `lastPublishedAt` it does not move on a re-publish, so it is what a page should show as \"published on\" and what answers \"how many articles went out in this window\"."},"lastPublishedAt":{"type":"string","nullable":true,"description":"When the article was last pushed to your site. Null means it has never been published. This is the field to check when deciding whether a page losing traffic is stale, and the one to show as \"last updated\": it moves only when the content actually reached readers, unlike `updatedAt`."},"isIndexed":{"type":"boolean","description":"Whether Google Search Console reports the page as indexed. Refreshed by a weekly job, so it reflects the last check rather than this instant: read `indexCheckedAt` to know how fresh it is."},"indexCoverageState":{"type":"string","nullable":true,"example":"Crawled - currently not indexed","description":"Search Console's own reason, verbatim, when the page is not indexed. This is the field that decides what to do about it: \"Crawled - currently not indexed\" means Google looked and declined, so the page needs work; \"Discovered - currently not indexed\" means it has not been crawled yet; a canonical or noindex reason means neither requesting nor rewriting will help."},"indexCheckedAt":{"type":"string","nullable":true,"description":"When the index status above was last checked. Null means never checked, which is not the same as not indexed."},"indexRequestedAt":{"type":"string","nullable":true,"description":"When indexing was last requested for this page. Null means it has never been submitted. Requests are throttled to one per page per week."}},"required":["id","title","status","slug","createdAt","updatedAt","isIndexed"]},"UpdateArticleDto":{"type":"object","properties":{"title":{"type":"string","example":"How to Start a Blog in 2024"},"description":{"type":"string","example":"A comprehensive guide."},"slug":{"type":"string","example":"how-to-start-a-blog-2024"},"coverImageUrl":{"type":"string","example":"https://cdn.example.com/cover.jpg"},"categoryId":{"type":"string","example":"clx..."}}},"AiPromptDto":{"type":"object","properties":{"id":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"text":{"type":"string","example":"best seo tools for small business"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","text","createdAt","updatedAt"]},"CreateAiPromptDto":{"type":"object","properties":{"text":{"type":"string","example":"best seo tools for small business","maxLength":2000,"description":"The question to ask the AI platforms on every run."}},"required":["text"]},"UpdateAiPromptDto":{"type":"object","properties":{"text":{"type":"string","example":"best seo tools for small business","maxLength":2000,"description":"The question to ask the AI platforms on every run."}},"required":["text"]},"AiPlatform":{"type":"string","enum":["chatgpt","perplexity","google_ai_overview","google_ai_mode"]},"MentionedBrandDto":{"type":"object","properties":{"name":{"type":"string","example":"RankSpot"},"domain":{"type":"string","example":"rankspot.ai"}},"required":["name","domain"]},"AiResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"promptId":{"type":"string","example":"9a8b7c6d-5e4f-4321-8765-1a2b3c4d5e6f"},"promptText":{"type":"string","example":"best seo tools for small business"},"platform":{"allOf":[{"$ref":"#/components/schemas/AiPlatform"}]},"runDate":{"type":"string","example":"2026-07-26","description":"The UTC day the prompt was run, `YYYY-MM-DD`."},"ownBrandMentioned":{"type":"boolean","nullable":true,"description":"Whether the answer named your own brand. null when the answer has not been analysed yet. Read `brands` for where it placed and how the answer spoke of it."},"citationsCount":{"type":"number","example":12},"brands":{"description":"Brands the answer named, most prominent first.","type":"array","items":{"$ref":"#/components/schemas/MentionedBrandDto"}}},"required":["id","promptId","promptText","platform","runDate","ownBrandMentioned","citationsCount","brands"]},"AnsweredBrandDto":{"type":"object","properties":{"name":{"type":"string","example":"RankSpot"},"domain":{"type":"string","example":"rankspot.ai"},"competitorId":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20","description":"The competitor this brand resolved to. Addresses the same record as the Competitors endpoints, so a brand named in an answer can be looked up there."},"nameAsWritten":{"type":"string","example":"Rankspot","description":"The surface form the answer used, kept for audit."},"position":{"type":"number","example":1,"nullable":true,"description":"Where the answer placed this brand, 1-based."},"sentiment":{"type":"number","example":85,"nullable":true,"description":"How favourably the answer spoke, 0-100. Read it in bands if you need one: 0-33 negative, 34-66 neutral, 67-100 positive."},"isOwnBrand":{"type":"boolean","example":true}},"required":["name","domain","competitorId","nameAsWritten","position","sentiment","isOwnBrand"]},"AnswerCitationDto":{"type":"object","properties":{"id":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"url":{"type":"string","example":"https://rankspot.ai/blog/ai-seo-tools/"},"domain":{"type":"string","example":"rankspot.ai"},"title":{"type":"string","nullable":true,"example":"The 12 best AI SEO tools"},"position":{"type":"number","example":1,"description":"Where the answer placed this source, 1-based."},"isOwnDomain":{"type":"boolean","example":true},"status":{"allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]}},"required":["id","url","domain","title","position","isOwnDomain","status"]},"AiResponseDetailDto":{"type":"object","properties":{"id":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"promptId":{"type":"string","example":"9a8b7c6d-5e4f-4321-8765-1a2b3c4d5e6f"},"promptText":{"type":"string","example":"best seo tools for small business"},"platform":{"allOf":[{"$ref":"#/components/schemas/AiPlatform"}]},"runDate":{"type":"string","example":"2026-07-26","description":"The UTC day the prompt was run, `YYYY-MM-DD`."},"ownBrandMentioned":{"type":"boolean","nullable":true,"description":"Whether the answer named your own brand. null when the answer has not been analysed yet. Read `brands` for where it placed and how the answer spoke of it."},"citationsCount":{"type":"number","example":12},"brands":{"description":"Brands the answer named, most prominent first.","type":"array","items":{"$ref":"#/components/schemas/AnsweredBrandDto"}},"answerMarkdown":{"type":"string","nullable":true,"description":"The full answer as markdown."},"citations":{"type":"array","items":{"$ref":"#/components/schemas/AnswerCitationDto"}},"fanoutQueries":{"example":["best seo tools 2026"],"description":"The searches the platform ran while composing the answer.","type":"array","items":{"type":"string"}}},"required":["id","promptId","promptText","platform","runDate","ownBrandMentioned","citationsCount","brands","answerMarkdown","citations","fanoutQueries"]},"AiCitationDto":{"type":"object","properties":{"id":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"url":{"type":"string","example":"https://rankspot.ai/blog/ai-seo-tools/"},"domain":{"type":"string","example":"rankspot.ai"},"title":{"type":"string","nullable":true,"example":"The 12 best AI SEO tools","description":"Title as the citing platform reported it."},"isOwnDomain":{"type":"boolean","example":true,"description":"The page is on your own domain."},"status":{"allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]},"firstSeenAt":{"format":"date-time","type":"string","description":"When this page was first and last cited, ever. Not clipped to the requested period."},"lastSeenAt":{"format":"date-time","type":"string"},"citations":{"type":"number","example":14,"description":"Responses in the period that cited this page. One response can cite a page at most once, so this is both the citation count and the number of answers behind it."},"platforms":{"type":"array","description":"Platforms that cited this page in the period.","items":{"$ref":"#/components/schemas/AiPlatform"}}},"required":["id","url","domain","title","isOwnDomain","status","firstSeenAt","lastSeenAt","citations","platforms"]},"UpdateAiCitationDto":{"type":"object","properties":{"status":{"description":"Mark the page as handled, or put it back on the worklist. Archiving is a separate axis: use `DELETE` and `/unarchive`.","allOf":[{"$ref":"#/components/schemas/ProcessedStatus"}]}},"required":["status"]},"AiFanoutQueryDto":{"type":"object","properties":{"id":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"query":{"type":"string","example":"best screen recording software for mac 2026","description":"The search the engine ran. Normalised on write — trimmed, inner whitespace collapsed, lower-cased — so the same search in two casings is one row rather than two."},"actionIds":{"example":["3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"],"description":"The `write_article` actions planned for this search. Non-empty with an empty `articleIds` is what `type=planned` selects.","type":"array","items":{"type":"string"}},"articleIds":{"example":["3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"],"description":"The articles written for this search. Non-empty is what `type=processed` selects, whatever the actions say.","type":"array","items":{"type":"string"}},"firstSeenAt":{"format":"date-time","type":"string","description":"When this query was first and last run, ever. Not clipped to the requested period."},"lastSeenAt":{"format":"date-time","type":"string"},"searches":{"type":"number","example":14,"description":"Responses in the period whose engine ran this query. An engine runs a query at most once per answer, so this is both the fanout count and the number of answers behind it."},"platforms":{"type":"array","description":"Platforms that ran this query in the period.","items":{"$ref":"#/components/schemas/AiPlatform"}}},"required":["id","query","actionIds","articleIds","firstSeenAt","lastSeenAt","searches","platforms"]},"AiFanoutQueryClusterDto":{"type":"object","properties":{"id":{"type":"string"},"query":{"type":"string","example":"best mac screen recorder with editing"},"searches":{"type":"number","example":6,"description":"How many answers ran it, all time. The seed reports 0."}},"required":["id","query","searches"]},"LeaderboardEntryDto":{"type":"object","properties":{"competitorId":{"type":"string","example":"3f1c2b4e-8d5a-4a2f-9c3e-7b1d6a5e4c20"},"name":{"type":"string","example":"RankSpot"},"domain":{"type":"string","example":"rankspot.ai"},"isOwnBrand":{"type":"boolean","example":true},"mentions":{"type":"number","example":31,"description":"Times the answers named this brand."},"shareOfVoice":{"type":"number","example":27.7,"description":"This brand as a share of all mentions in the period, 0-100. Measured against every brand named, not only the ten returned, so the rows here need not sum to 100."},"position":{"type":"number","example":1,"description":"1-based, most mentioned first."},"sentiment":{"type":"number","example":82,"nullable":true,"description":"Mean of how favourably the answers spoke of this brand, 0-100. null when no answer carried a sentiment for it."}},"required":["competitorId","name","domain","isOwnBrand","mentions","shareOfVoice","position","sentiment"]},"AiSummaryDto":{"type":"object","properties":{"startDate":{"type":"string","example":"2026-07-01"},"endDate":{"type":"string","example":"2026-07-28"},"visibilityScore":{"type":"number","example":42.9,"nullable":true,"description":"Share of answers that named your brand, 0-100, as an unweighted mean of the per-platform scores. null when nothing ran in the period, which is not the same as 0."},"shareOfVoice":{"type":"number","example":22.5,"nullable":true,"description":"Your mentions as a share of all brand mentions, 0-100. Your own row in `leaderboard`, lifted out. null when no brand was mentioned at all."},"citationShare":{"type":"number","example":8.1,"nullable":true,"description":"Citations pointing at your own domain as a share of all citations, 0-100."},"categoryRank":{"type":"number","example":3,"nullable":true,"description":"Your 1-based position in `leaderboard`. null when the workspace has no own brand set."},"responsesCounted":{"type":"number","example":112,"description":"Answers the scores are computed from. Read it before quoting the others: 100% visibility across two answers is not the same claim as 100% across two hundred."},"platforms":{"type":"object","additionalProperties":{"type":"number","nullable":true},"example":{"chatgpt":66.7,"perplexity":25,"google_ai_overview":null,"google_ai_mode":0},"description":"Visibility per platform, 0-100. null means the platform produced no answer in the period; 0 means it answered and never named you. `visibilityScore` is the unweighted mean of the non-null entries."},"leaderboard":{"description":"The ten most-mentioned brands, most first, with your own always included even when it falls outside the ten or was never mentioned. Brands hidden in the dashboard are excluded, including from the share-of-voice denominator.","type":"array","items":{"$ref":"#/components/schemas/LeaderboardEntryDto"}}},"required":["startDate","endDate","visibilityScore","shareOfVoice","citationShare","categoryRank","responsesCounted","platforms","leaderboard"]},"GoogleSearchDto":{"type":"object","properties":{"query":{"type":"string","example":"best screen recording software for mac","description":"What to search Google for."},"types":{"type":"array","default":["organic","discussions_and_forums"],"description":"Which blocks of the results page to return. Everything else Google showed is dropped before the response is built. Ask for what you will actually read: `ai_overview` and `people_also_ask` carry the full text of every answer and are many times the size of the organic results.","items":{"type":"string","enum":["ai_overview","organic","people_also_ask","discussions_and_forums","video","related_searches"]}}},"required":["query"]},"GoogleResultItemDto":{"type":"object","properties":{"type":{"type":"string","example":"organic","description":"Block type: organic, ai_overview, people_also_ask, discussions_and_forums, video, related_searches."},"rank":{"type":"number","example":4,"description":"organic only: position among the organic results. Not the page-wide position, which moves whenever Google adds a block above them."},"page":{"type":"number","example":1,"description":"organic only: which page of results this came from."},"domain":{"type":"string","example":"screen.studio","description":"organic only."},"title":{"type":"string","example":"Screen Studio — Professional screen recorder"},"url":{"type":"string","example":"https://screen.studio"},"website_name":{"type":"string","example":"Microsoft Community Hub","description":"organic only: the site's own name for itself, when it has one."},"description":{"type":"string","description":"Snippet text, when Google showed one."},"timestamp":{"type":"string","example":"2025-03-26 00:00:00 +00:00","description":"organic only: when the page was published, when Google dated it. Absent means undated, not new."},"rank_group":{"type":"number","description":"Every type except organic: position among blocks of the same type, and across the whole page. Organic reports `rank` instead."},"rank_absolute":{"type":"number"},"items":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Nested entries for container blocks: the questions under people_also_ask, the threads under discussions_and_forums, the terms under related_searches."},"markdown":{"type":"string","description":"ai_overview only: the overview Google generated, as markdown."}},"required":["type"]},"GoogleSearchResultDto":{"type":"object","properties":{"query":{"type":"string","example":"best screen recorder mac"},"location":{"type":"string","example":"United States","description":"Resolved from the workspace's settings."},"language":{"type":"string","example":"en","description":"Resolved from the workspace's settings."},"items":{"description":"The blocks Google showed, in page order, filtered to the types asked for. Empty when Google returned nothing, or nothing of those types.","type":"array","items":{"$ref":"#/components/schemas/GoogleResultItemDto"}}},"required":["query","location","language","items"]},"FetchPageDto":{"type":"object","properties":{"url":{"type":"string","example":"https://example.com/blog/post","description":"The page to fetch. Must be a public http(s) URL."}},"required":["url"]},"FetchPageResultDto":{"type":"object","properties":{"markdown":{"type":"string","example":"# Best screen recorders for Mac\n\nIf you are looking for...","description":"The page as markdown, main content only. Never empty: a page that extracts to nothing is a 503, not a successful fetch of \"\"."}},"required":["markdown"]},"GscDimensionFilterDto":{"type":"object","properties":{"dimension":{"type":"string","enum":["country","device","page","query","searchAppearance"],"example":"country"},"expression":{"type":"string","example":"ind","description":"Value to match against. Country codes are ISO 3166-1 alpha-3 (e.g. ind, usa). Device values: DESKTOP, MOBILE, TABLET."},"operator":{"type":"string","enum":["equals","notEquals","contains","notContains","includingRegex","excludingRegex"],"default":"equals"}},"required":["dimension","expression"]},"GscDimensionFilterGroupDto":{"type":"object","properties":{"filters":{"type":"array","items":{"$ref":"#/components/schemas/GscDimensionFilterDto"}}},"required":["filters"]},"GscPerformanceDto":{"type":"object","properties":{"startDate":{"type":"string","example":"2024-01-01","description":"Start date in YYYY-MM-DD format"},"endDate":{"type":"string","example":"2024-01-31","description":"End date in YYYY-MM-DD format"},"dimensions":{"type":"array","description":"Dimensions to group by. Pass multiple times for combined grouping (e.g. query + page).","items":{"type":"string","enum":["query","page","country","device","date","searchAppearance"]},"example":["query"]},"dimensionFilterGroups":{"description":"Filter groups to narrow results. Filters within a group are ANDed together.","type":"array","items":{"$ref":"#/components/schemas/GscDimensionFilterGroupDto"}},"startRow":{"type":"number","description":"Zero-based index of the first row to return. Use with rowLimit for pagination.","example":0},"rowLimit":{"type":"number","description":"Max rows to return (1–25000). Defaults to GSC API default (1000) when omitted."}},"required":["startDate","endDate"]},"GscInspectUrlDto":{"type":"object","properties":{"url":{"type":"string","example":"https://example.com/blog/my-post","description":"The fully-qualified URL to inspect. Must belong to the connected Search Console property."},"languageCode":{"type":"string","example":"en-US","description":"BCP-47 language code for the inspection result messages. Defaults to en-US."}},"required":["url"]},"GscIndexUrlDto":{"type":"object","properties":{"url":{"type":"string","example":"https://example.com/blog/my-post","description":"The fully-qualified URL to notify Google about. Must belong to a property you own."},"type":{"type":"string","enum":["URL_UPDATED","URL_DELETED"],"default":"URL_UPDATED","description":"URL_UPDATED to (re)index a new or changed page, URL_DELETED to request removal."}},"required":["url"]}}}}