{"openapi":"3.0.0","paths":{"/v1/workspaces":{"get":{"description":"Every workspace your credential reaches. A personal API key or connection reaches every workspace in every organization you belong to, including clients that invited you; a key or connection made for one workspace gets a list of one.\n\nEvery other endpoint is scoped to one workspace: put its id in the path, as in `/v1/workspaces/{workspaceId}/keywords`.","operationId":"WorkspacesController_findAll","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceDto"}}}}}},"security":[{"bearer":[]}],"summary":"List workspaces","tags":["Workspace"]}},"/v1/workspaces/{workspaceId}":{"get":{"description":"Who the workspace is: brand name, domain, what the business does, who it sells to, tone of voice, industry, and the country and language it targets. Read 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. Fields are read-only and any of them can be null; they are edited in the RankSpot dashboard.","operationId":"WorkspacesController_findOne","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceDto"}}}},"404":{"description":"Not a workspace this credential reaches"}},"security":[{"bearer":[]}],"summary":"Get a workspace","tags":["Workspace"]}},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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","description":"competitorCount: domains linking to the most competitors first (default) | domainFromRank | firstSeen. Ties fall back to domainFromRank, highest first.","schema":{"default":"competitorCount","type":"string","enum":["competitorCount","domainFromRank","firstSeen"]}},{"name":"sortOrder","required":false,"in":"query","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"competitorId","required":false,"in":"query","description":"Only domains that link to this competitor (UUID).","schema":{"type":"string"}},{"name":"type","required":false,"in":"query","description":"competitors: unprocessed domains linking to competitors but not to you (default) | mine: domains linking to you | processed: processed domains not yet linking to you | archived: archived domains","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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/articles/{id}":{"get":{"description":"Returns a single article including its full `contentHtml` and the same body as `contentMarkdown`, the form to edit and send back on `PATCH`. 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`, and/or replace the whole body with `contentMarkdown`. The `slug` must be unique within your workspace. Every change is kept in the revision history (`GET /articles/:id/revisions`) and nothing is published until `POST /articles/:id/publish`. Status is managed by RankSpot.","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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/articles/{id}/publish":{"post":{"description":"Pushes the article as it is now to a connected integration: published there for the first time, or updated in place when it already is. Works exactly like the Publish button in the RankSpot editor.","operationId":"ArticlesController_publish","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishArticleDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDto"}}}},"400":{"description":"The integration is not connected, rejected the post, or does not support updating yet."},"404":{"description":"Article not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"Publish an article","tags":["Articles"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/articles/{id}/revisions":{"get":{"description":"The last 10 revisions, newest first. The first one is always the article as it is now. Each editing session (the RankSpot editor, or API writes sharing a `sessionId`) is one revision. `live` marks the content last published to your site.","operationId":"ArticlesController_listRevisions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleRevisionDto"}}}}},"404":{"description":"Article not found or belongs to another workspace."}},"security":[{"bearer":[]}],"summary":"List article revisions","tags":["Articles"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/articles/{id}/revisions/{revisionId}/restore":{"post":{"description":"Writes an older revision's title, description and body back onto the article, as a new revision, so the restore can be undone too. Nothing is published.","operationId":"ArticlesController_restoreRevision","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"revisionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDto"}}}},"404":{"description":"Article or revision not found."}},"security":[{"bearer":[]}],"summary":"Restore an article revision","tags":["Articles"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/find-contact":{"post":{"description":"Reads the page, works out who wrote it, and finds and checks their email. Returns the author and up to 5 addresses, best first: email the first, keep the rest as backups. Takes up to a minute. Costs credits (about 10 to 40 per page, depending on how far it has to look). To see whether the site or person was already contacted, list opportunities with `domain` or `email`.","operationId":"OutreachController_findContact","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindContactDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactLookupDto"}}}},"402":{"description":"`ai-credits-exhausted`: no AI credits left this billing period. The body carries `usage` with the counts behind that."}},"security":[{"bearer":[]}],"summary":"Find who to email about a page","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/find-email":{"post":{"description":"For when you already know who to write to: finds and checks the address of `firstName lastName` at `domain`. `email` is null when nothing deliverable was found. Costs credits (fewer than find-contact, since no page is read).","operationId":"OutreachController_findEmail","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindEmailDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailLookupDto"}}}},"402":{"description":"`ai-credits-exhausted`: no AI credits left this billing period. The body carries `usage` with the counts behind that."}},"security":[{"bearer":[]}],"summary":"Find a person's email on a domain","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/opportunities":{"get":{"description":"Newest activity first; with `pending=true`, soonest next email first. Use `domain` or `email` to check whether a site or person was already contacted, and `needsReply=true` (or `since`) to find what is new.","operationId":"OutreachController_list","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":"scheduled: nothing sent yet, the only time it can be edited or deleted. sent: the first email went out, no reply. replied: a person answered. processed: closed, read-only.","schema":{"type":"string","enum":["scheduled","sent","replied","processed"]}},{"name":"pending","required":false,"in":"query","description":"Only conversations with an email waiting to go out, follow-ups included, soonest first","schema":{"type":"boolean"}},{"name":"needsReply","required":false,"in":"query","description":"Only replied conversations whose last human message is theirs","schema":{"type":"boolean"}},{"name":"since","required":false,"in":"query","description":"Only conversations with activity after this time","schema":{"format":"date-time","type":"string"}},{"name":"domain","required":false,"in":"query","description":"A domain or any URL; matches its root domain","schema":{"example":"acme.com","type":"string"}},{"name":"email","required":false,"in":"query","description":"Matches any of the contact emails","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Matches domain, page title or contact name","schema":{"type":"string"}},{"name":"mailboxId","required":false,"in":"query","description":"Only conversations sent from this mailbox","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/OpportunityListItemDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List outreach conversations","tags":["Outreach"]},"post":{"description":"Creates the opportunity with the emails you wrote and schedules them. The first email waits at least an hour, so it can still be edited or deleted. 400 lists every problem with the copy at once (links, unknown variables, a missing sender sign-off...).","operationId":"OutreachController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOpportunityDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityDto"}}}},"400":{"description":"The copy breaks a rule; `message` lists each problem"},"409":{"description":"`outreach-address-contacted` or `outreach-site-contacted` (unless `allowContactedSite`): `opportunities` lists the earlier conversations. `outreach-no-mailbox`: connect a mailbox in the RankSpot dashboard first."}},"security":[{"bearer":[]}],"summary":"Pitch a page","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/opportunities/{id}":{"get":{"description":"The emails as written (`sequence`, each with its status and estimate) and everything exchanged so far (`thread`).","operationId":"OutreachController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityDto"}}}},"404":{"description":"Not in this workspace"}},"security":[{"bearer":[]}],"summary":"Get an opportunity","tags":["Outreach"]},"patch":{"description":"Any of the fields, until the first email is sent (`status: scheduled`). `sequence` replaces the whole list. The send time stays as it was.","operationId":"OutreachController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOpportunityDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityDto"}}}},"409":{"description":"`outreach-already-sent`, `outreach-processed` or `outreach-address-contacted`"}},"security":[{"bearer":[]}],"summary":"Edit an opportunity","tags":["Outreach"]},"delete":{"description":"Only before anything was sent. Once a conversation started, stop it instead.","operationId":"OutreachController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"409":{"description":"`outreach-already-sent`"}},"security":[{"bearer":[]}],"summary":"Delete an opportunity","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/opportunities/{id}/stop":{"post":{"description":"Cancels every scheduled email. The thread and any replies stay.","operationId":"OutreachController_stop","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityDto"}}}}},"security":[{"bearer":[]}],"summary":"Stop the emails still waiting","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/opportunities/{id}/reply":{"post":{"description":"Sends right away from the conversation's mailbox to whoever wrote last, threaded under the exchange. Does not count towards the daily limit, and cancels follow-ups still waiting. Unlike the automated emails, it may contain links.","operationId":"OutreachController_reply","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplyDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutreachMessageDto"}}}},"409":{"description":"`outreach-nothing-sent`, `outreach-mailbox-unavailable` (reconnect it) or `outreach-processed`"}},"security":[{"bearer":[]}],"summary":"Reply in the conversation","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/opportunities/{id}/process":{"post":{"description":"Closes the conversation, whatever came of it: anything waiting is cancelled and it becomes read-only. A late reply is still recorded.","operationId":"OutreachController_process","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityDto"}}}}},"security":[{"bearer":[]}],"summary":"Mark as processed","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/stats":{"get":{"description":"Emails sent (first emails and each follow-up), replies, automatic replies, bounces, reply rate (of the sites first emailed in the period, how many answered), bounce rate, which email the replies answered, median time to a reply, and the same counts per day. Days are US Central, like the sending window; the default is the last 30 days.","operationId":"OutreachController_stats","parameters":[{"name":"startDate","required":false,"in":"query","description":"First day, YYYY-MM-DD, counted on US Central time like the sending window. Default: 29 days before endDate","schema":{"example":"2026-09-11","type":"string"}},{"name":"endDate","required":false,"in":"query","description":"Last day, inclusive. Default: today. At most 366 days in all","schema":{"example":"2026-10-10","type":"string"}},{"name":"mailboxId","required":false,"in":"query","description":"Only emails of this mailbox","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutreachStatsDto"}}}}},"security":[{"bearer":[]}],"summary":"Outreach numbers for a period","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/mailboxes":{"get":{"description":"Each connected mailbox with what it sent today and how many emails are bound to it (`queued`). Check this before queueing a lot: each mailbox sends 10 automated emails per day. Mailboxes are connected in the dashboard.","operationId":"OutreachController_mailboxes","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MailboxCapacityDto"}}}}}},"security":[{"bearer":[]}],"summary":"List the sending mailboxes","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/links":{"get":{"description":"The paid, exchanged and free links logged for this workspace, newest first. Statuses are set by hand: nothing checks that a link is really live.","operationId":"OutreachController_listLinks","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":"Their link to us. pending: not placed yet, also when we already placed ours. live: it is up. lost: they removed it.","schema":{"type":"string","enum":["pending","live","lost"]}},{"name":"type","required":false,"in":"query","schema":{"type":"string","enum":["paid","exchange","free"]}},{"name":"outreachId","required":false,"in":"query","description":"Only links from this opportunity","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/OutreachLinkDto"}}}}}}}}}},"security":[{"bearer":[]}],"summary":"List link deals","tags":["Outreach"]},"post":{"description":"Only `type` is required. It starts `pending` until their link is up. For an exchange, `url` is their page linking to us and `ourUrl` our page linking to them.","operationId":"OutreachController_createLink","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOutreachLinkDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutreachLinkDto"}}}},"404":{"description":"`outreachId` is not in this workspace"}},"security":[{"bearer":[]}],"summary":"Log a link deal","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/outreach/links/{id}":{"patch":{"description":"Any of the fields. Null clears one, except `type`, `status` and `dofollow`.","operationId":"OutreachController_updateLink","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOutreachLinkDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutreachLinkDto"}}}},"404":{"description":"Not in this workspace"}},"security":[{"bearer":[]}],"summary":"Edit a link deal","tags":["Outreach"]},"delete":{"description":"Removes the record only. To remember a link they dropped, set `status` to `lost` instead.","operationId":"OutreachController_removeLink","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""},"404":{"description":"Not in this workspace"}},"security":[{"bearer":[]}],"summary":"Delete a link deal","tags":["Outreach"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]},"/v1/workspaces/{workspaceId}/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"]},"parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"One of the workspaces from `GET /v1/workspaces`"}]}},"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. A key is personal: it reaches every workspace in every organization you belong to, so each call names one in its path:\n\n```\nGET /v1/workspaces/{workspaceId}/keywords\n```\n\nList the workspaces with `GET /v1/workspaces`. Keys created before organizations, and connections made through OAuth, are tied to one workspace: they also work without the `/workspaces/{workspaceId}` part, and get 403 if the path names a different 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":"Keywords","description":"SEO keywords tracked in your workspace, enriched with search volume, competition, and AI-generated relevance scores."},{"name":"Outreach","description":"Email outreach from the workspace's connected mailboxes. Find who to email about a page, pitch it with emails you write, then read and answer the replies. Emails are scheduled straight away: the first waits at least an hour (it can be edited or deleted until it goes), then goes out at the next free slot, 10 a day per mailbox, every day 8:00 to 20:00 US Central, with follow-ups 3 and 7 days later until someone replies. No automated email may contain a link; your own replies may. The two lookups spend credits per call and return 402 when the workspace is out of credits. Mailboxes are connected in the RankSpot dashboard. Link deals (paid, exchanged or free) are logged by hand under `/outreach/links`; nothing checks them automatically."},{"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":{"WorkspaceOrganizationDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","example":"Acme Inc."}},"required":["id","name"]},"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."},"organization":{"nullable":true,"description":"The organization that owns the workspace. A credential can reach several, such as your own and a client that invited you.","type":"object","allOf":[{"$ref":"#/components/schemas/WorkspaceOrganizationDto"}]},"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-11-08T15:58:00.972Z"},"updatedAt":{"format":"date-time","type":"string","example":"2026-11-08T15:58:00.972Z"}},"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"]},"BacklinkCompetitorDto":{"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"]},"BacklinkLinkDto":{"type":"object","properties":{"competitor":{"nullable":true,"description":"The competitor this link points to, or null when it points to your site.","type":"object","allOf":[{"$ref":"#/components/schemas/BacklinkCompetitorDto"}]},"urlFrom":{"type":"string","example":"https://example.com/best-seo-tools"},"urlTo":{"type":"string","example":"https://ahrefs.com/"},"domainTo":{"type":"string","example":"ahrefs.com"},"anchor":{"type":"string","example":"Ahrefs","nullable":true},"dofollow":{"type":"boolean","example":true},"pageFromTitle":{"type":"string","example":"12 Best SEO Tools","nullable":true},"pageFromRank":{"type":"number","example":41,"nullable":true},"rank":{"type":"number","example":18,"nullable":true},"backlinkSpamScore":{"type":"number","example":2,"nullable":true},"attributes":{"example":["noopener","noreferrer"],"type":"array","items":{"type":"string"}},"firstSeen":{"type":"string","example":"2026-11-08T15:58:01.009Z","nullable":true},"lastSeen":{"type":"string","example":"2026-11-08T15:58:01.010Z","nullable":true}},"required":["competitor","urlFrom","urlTo","domainTo","dofollow","attributes"]},"BacklinkDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"domainFrom":{"type":"string","example":"example.com"},"domainFromRank":{"type":"number","example":72,"nullable":true},"linksToYou":{"type":"boolean","example":false,"description":"This domain already links to your site."},"competitorCount":{"type":"number","example":3,"description":"How many of your competitors this domain links to."},"firstSeen":{"type":"string","example":"2026-11-08T15:58:01.010Z","nullable":true,"description":"The earliest first-seen date across its links."},"status":{"type":"string","enum":["new","processed"],"example":"new"},"links":{"description":"Your link first (competitor: null), then competitor links by page rank.","type":"array","items":{"$ref":"#/components/schemas/BacklinkLinkDto"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","domainFrom","linksToYou","competitorCount","status","links","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"]},"ActionBacklinkCompetitorDto":{"type":"object","properties":{"name":{"type":"string","example":"Ahrefs"},"domain":{"type":"string","example":"ahrefs.com"}},"required":["name","domain"]},"ActionBacklinkLinkDto":{"type":"object","properties":{"urlFrom":{"type":"string","example":"https://example.com/best-seo-tools","description":"The page carrying the competitor link."},"dofollow":{"type":"boolean","example":true},"competitor":{"nullable":true,"type":"object","allOf":[{"$ref":"#/components/schemas/ActionBacklinkCompetitorDto"}]}},"required":["urlFrom","dofollow","competitor"]},"ActionBacklinkDto":{"type":"object","properties":{"id":{"type":"string","example":"6b3c1f2e-9d4a-4a7e-b2c1-5f0e8a7d3c94"},"domainFrom":{"type":"string","example":"example.com"},"domainFromRank":{"type":"number","example":72,"nullable":true},"competitorCount":{"type":"number","example":3,"description":"How many competitors this domain links to."},"links":{"description":"Its competitor links, strongest page first; the first is the page to pitch.","type":"array","items":{"$ref":"#/components/schemas/ActionBacklinkLinkDto"}}},"required":["id","domainFrom","competitorCount","links"]},"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, with the competitor links it carries. `links[0].urlFrom` is the page to pitch.","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"]},"ProcessedStatus":{"type":"string","enum":["new","processed"]},"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"]},"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"},"contentMarkdown":{"type":"string","example":"## Why start a blog\n\nA blog gives your site...","nullable":true,"description":"The body as Markdown, the form to edit and send back as `contentMarkdown` on `PATCH /articles/:id`. Only returned by `GET /articles/:id`. Null when the article has no body yet, or the conversion was briefly unavailable (`contentHtml` is still returned then)."},"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..."},"contentMarkdown":{"type":"string","example":"## Why start a blog\n\nA blog gives your site...","description":"The whole article body as Markdown, replacing the current one. Read it first from `GET /articles/:id` (`contentMarkdown`) and send back the edited text: sections can be added, removed, reordered or rewritten. Blocks you leave unchanged keep their images, sizes and embeds. Lines of HTML are content Markdown cannot express (videos and similar): keep them as they are. Links are `[text](url)` and follow the site policy (internal dofollow, external nofollow); write `<a href=\"https://example.com\" rel=\"dofollow\">text</a>` only for an external link that must be dofollow, or `rel=\"nofollow\"` for an internal one that must not be. The edit is saved in RankSpot and appears in the article's revision history, but is not published until you call `POST /articles/:id/publish`. Refused while the article is still generating."},"sessionId":{"type":"string","example":"chat-2026-11-07-pricing","description":"Any string naming one editing session, e.g. one agent conversation or one script run. Writes with the same `sessionId` as the latest revision update that revision instead of adding a new one, so the session shows up as one entry in the history. Reuse it for every write in the session; omit it and every write is its own revision."}}},"PublishArticleDto":{"type":"object","properties":{"integration":{"type":"string","enum":["wordpress","shopify","shopify_custom","ghost","blogbowl","framer","wix","webflow","webhook","hubspot"],"example":"wordpress","description":"The connected integration to publish to. An article already published there is updated in place."}},"required":["integration"]},"ArticleRevisionDto":{"type":"object","properties":{"id":{"type":"string","example":"clx..."},"source":{"type":"string","enum":["editor","api","generation","restore","baseline"],"description":"Who made it: `editor` (the RankSpot editor), `api` (this API or MCP), `generation` (the first draft), `restore` (an older revision written back) or `baseline` (the article as it was before history was recorded)."},"sessionId":{"type":"string","nullable":true},"title":{"type":"string","example":"How to Start a Blog"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string","description":"Last write in this revision's session."},"live":{"type":"boolean","description":"Whether this revision is the content last published to your site."}},"required":["id","source","sessionId","title","createdAt","updatedAt","live"]},"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"]},"FindContactDto":{"type":"object","properties":{"url":{"type":"string","example":"https://blog.acme.com/best-seo-tools"}},"required":["url"]},"PageAuthorDto":{"type":"object","properties":{"fullName":{"type":"string","example":"Jane Doe"},"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe","description":"Empty when only one name is given"}},"required":["fullName","firstName","lastName"]},"ContactCandidateDto":{"type":"object","properties":{"email":{"type":"string","example":"jane@acme.com"},"name":{"type":"string","nullable":true,"example":"Jane Doe"},"position":{"type":"string","nullable":true,"example":"Head of Content"},"deliverability":{"type":"string","enum":["valid","risky","unknown","not_checked"],"description":"valid: it can receive mail. risky: the domain accepts every address, so it cannot be confirmed. unknown: the check could not tell. not_checked: a backup address that was not checked."},"confidence":{"type":"number","nullable":true,"example":96,"description":"How sure the lookup is that this is their address, 0 to 100"}},"required":["email","name","position","deliverability","confidence"]},"ContactLookupDto":{"type":"object","properties":{"domain":{"type":"string","example":"acme.com","description":"The root domain"},"pageTitle":{"type":"string","nullable":true,"example":"The 11 Best SEO Tools in 2026","description":"The page's main heading"},"author":{"nullable":true,"description":"Who the page credits as its author","type":"object","allOf":[{"$ref":"#/components/schemas/PageAuthorDto"}]},"candidates":{"description":"Best first, at most 5. Email the first; pass the rest as backups in `contactEmails`. Addresses found undeliverable are left out. Empty when nothing was found.","type":"array","items":{"$ref":"#/components/schemas/ContactCandidateDto"}}},"required":["domain","pageTitle","author","candidates"]},"FindEmailDto":{"type":"object","properties":{"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"},"domain":{"type":"string","example":"acme.com","description":"The domain, or any URL on it"}},"required":["firstName","lastName","domain"]},"EmailLookupDto":{"type":"object","properties":{"email":{"type":"string","nullable":true,"example":"jane@acme.com","description":"Null when no deliverable address was found"},"deliverability":{"type":"string","enum":["valid","risky","unknown","not_checked"],"nullable":true,"description":"valid: it can receive mail. risky: the domain accepts every address, so it cannot be confirmed. unknown: the check could not tell. not_checked: a backup address that was not checked."},"confidence":{"type":"number","nullable":true,"example":96}},"required":["email","deliverability","confidence"]},"SentFromDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","example":"alex@yourbrand.com"}},"required":["id","email"]},"NextEmailDto":{"type":"object","properties":{"position":{"type":"number","description":"0 is the first email, 1 and 2 the follow-ups"},"sendsAfter":{"format":"date-time","type":"string","nullable":true,"description":"Not before this time: the 1-hour hold on a first email, the follow-up delay after it"},"waitingReason":{"type":"string","nullable":true,"description":"Why it cannot go out, such as a paused mailbox. Null when nothing blocks it"}},"required":["position","sendsAfter","waitingReason"]},"MessagePreviewDto":{"type":"object","properties":{"direction":{"type":"string","enum":["outbound","inbound"]},"kind":{"type":"string","enum":["initial","follow_up","reply"]},"isAutoReply":{"type":"boolean"},"snippet":{"type":"string","description":"The start of the text, quotes removed"},"at":{"format":"date-time","type":"string"}},"required":["direction","kind","isAutoReply","snippet","at"]},"OpportunityListItemDto":{"type":"object","properties":{"id":{"type":"string"},"domain":{"type":"string","example":"acme.com","description":"The root domain"},"pageUrl":{"type":"string"},"pageTitle":{"type":"string"},"contactName":{"type":"string","nullable":true},"status":{"type":"string","enum":["scheduled","sent","replied","processed"],"description":"scheduled: nothing sent yet, the only time it can be edited or deleted. sent: the first email went out, no reply. replied: a person answered. processed: closed, read-only."},"error":{"type":"string","nullable":true,"description":"Why it is stuck, such as every contact bouncing"},"needsReply":{"type":"boolean","description":"The last message a person wrote is theirs: your turn"},"sentFrom":{"nullable":true,"description":"The mailbox the thread goes out from; null until the first email is sent","type":"object","allOf":[{"$ref":"#/components/schemas/SentFromDto"}]},"sentCount":{"type":"number","description":"Sequence emails already sent"},"next":{"nullable":true,"description":"The next email waiting, if any","type":"object","allOf":[{"$ref":"#/components/schemas/NextEmailDto"}]},"lastMessageAt":{"format":"date-time","type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"},"contactEmail":{"type":"string","description":"Who the thread goes to"},"lastMessage":{"nullable":true,"type":"object","allOf":[{"$ref":"#/components/schemas/MessagePreviewDto"}]}},"required":["id","domain","pageUrl","pageTitle","contactName","status","error","needsReply","sentFrom","sentCount","next","lastMessageAt","createdAt","contactEmail","lastMessage"]},"SequenceEmailDto":{"type":"object","properties":{"body":{"type":"string","example":"Hi {{first_name}},\n\nLoved the comparison table in your roundup...\n\nBest,\n{{sender_name}}","description":"Plain text; **bold** and *italic* are the only formatting. No links or domain names anywhere. Variables, filled when it is sent: {{first_name}} (the contact's first name, or \"there\"), {{sender_name}} and {{sender_first_name}} (the sending mailbox, picked at send time). The first email must sign off with a sender variable."}},"required":["body"]},"CreateOpportunityDto":{"type":"object","properties":{"pageUrl":{"type":"string","example":"https://blog.acme.com/best-seo-tools"},"pageTitle":{"type":"string","example":"The 11 Best SEO Tools in 2026"},"contactEmails":{"example":["jane@acme.com","editor@acme.com"],"description":"In order, 1 to 5: the first is emailed, the next is used only if it bounces","type":"array","items":{"type":"string"}},"contactName":{"type":"string","example":"Jane Doe","description":"Fills {{first_name}}. Without it the greeting reads \"there\""},"subject":{"type":"string","example":"Quick question about your SEO tools list","description":"One line, up to 150 characters. Follow-ups reply as \"Re: subject\""},"sequence":{"description":"1 to 3 emails: the first, then follow-ups 3 and 7 days later in the same thread, until someone replies","type":"array","items":{"$ref":"#/components/schemas/SequenceEmailDto"}},"backlinkId":{"type":"string","description":"Link it to this backlink"},"aiCitationId":{"type":"string","description":"Link it to this AI citation"},"allowContactedSite":{"type":"boolean","default":false,"description":"Pitch a site that already has outreach, such as another author at the same publisher. An address already contacted is refused either way."}},"required":["pageUrl","pageTitle","contactEmails","subject","sequence"]},"SequenceItemDto":{"type":"object","properties":{"position":{"type":"number"},"body":{"type":"string","description":"As written, variables unfilled"},"status":{"type":"string","enum":["scheduled","sending","sent","cancelled","failed"],"nullable":true,"description":"cancelled: after a reply or a stop. failed: it bounced or was blocked"},"toEmail":{"type":"string","nullable":true},"sendsAfter":{"format":"date-time","type":"string","nullable":true},"sentAt":{"format":"date-time","type":"string","nullable":true}},"required":["position","body","status","toEmail","sendsAfter","sentAt"]},"OutreachMessageDto":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["outbound","inbound"]},"kind":{"type":"string","enum":["initial","follow_up","reply"]},"position":{"type":"number","nullable":true,"description":"Which sequence email this was; null for replies"},"status":{"type":"string","enum":["sent","received","failed"],"description":"failed: it bounced"},"fromEmail":{"type":"string","nullable":true},"toEmail":{"type":"string","nullable":true},"subject":{"type":"string"},"body":{"type":"string","description":"For replies, only the new text: quoted earlier emails removed"},"fullBody":{"type":"string","description":"The email as sent or received"},"isAutoReply":{"type":"boolean"},"sentAt":{"format":"date-time","type":"string","nullable":true},"error":{"type":"string","nullable":true}},"required":["id","direction","kind","position","status","fromEmail","toEmail","subject","body","fullBody","isAutoReply","sentAt","error"]},"OpportunityDto":{"type":"object","properties":{"id":{"type":"string"},"domain":{"type":"string","example":"acme.com","description":"The root domain"},"pageUrl":{"type":"string"},"pageTitle":{"type":"string"},"contactName":{"type":"string","nullable":true},"status":{"type":"string","enum":["scheduled","sent","replied","processed"],"description":"scheduled: nothing sent yet, the only time it can be edited or deleted. sent: the first email went out, no reply. replied: a person answered. processed: closed, read-only."},"error":{"type":"string","nullable":true,"description":"Why it is stuck, such as every contact bouncing"},"needsReply":{"type":"boolean","description":"The last message a person wrote is theirs: your turn"},"sentFrom":{"nullable":true,"description":"The mailbox the thread goes out from; null until the first email is sent","type":"object","allOf":[{"$ref":"#/components/schemas/SentFromDto"}]},"sentCount":{"type":"number","description":"Sequence emails already sent"},"next":{"nullable":true,"description":"The next email waiting, if any","type":"object","allOf":[{"$ref":"#/components/schemas/NextEmailDto"}]},"lastMessageAt":{"format":"date-time","type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"},"contactEmails":{"type":"array","items":{"type":"string"}},"backlinkId":{"type":"string","nullable":true},"aiCitationId":{"type":"string","nullable":true},"subject":{"type":"string"},"sequence":{"description":"The emails as written, each with how far it got","type":"array","items":{"$ref":"#/components/schemas/SequenceItemDto"}},"thread":{"description":"What actually went back and forth, oldest first","type":"array","items":{"$ref":"#/components/schemas/OutreachMessageDto"}},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","domain","pageUrl","pageTitle","contactName","status","error","needsReply","sentFrom","sentCount","next","lastMessageAt","createdAt","contactEmails","backlinkId","aiCitationId","subject","sequence","thread","updatedAt"]},"UpdateOpportunityDto":{"type":"object","properties":{"pageTitle":{"type":"string","example":"The 11 Best SEO Tools in 2026"},"contactEmails":{"example":["jane@acme.com","editor@acme.com"],"description":"In order, 1 to 5: the first is emailed, the next is used only if it bounces","type":"array","items":{"type":"string"}},"contactName":{"type":"string","example":"Jane Doe","description":"Fills {{first_name}}. Without it the greeting reads \"there\""},"subject":{"type":"string","example":"Quick question about your SEO tools list","description":"One line, up to 150 characters. Follow-ups reply as \"Re: subject\""},"sequence":{"description":"1 to 3 emails: the first, then follow-ups 3 and 7 days later in the same thread, until someone replies","type":"array","items":{"$ref":"#/components/schemas/SequenceEmailDto"}}}},"ReplyDto":{"type":"object","properties":{"body":{"type":"string","description":"Plain text, links allowed. Sent right away from the conversation's mailbox"}},"required":["body"]},"SequenceCountsDto":{"type":"object","properties":{"initial":{"type":"number","description":"First emails"},"followUp1":{"type":"number"},"followUp2":{"type":"number"},"total":{"type":"number"}},"required":["initial","followUp1","followUp2","total"]},"OutreachStatsDayDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-10-09"},"sent":{"type":"number","description":"Sequence emails sent"},"replies":{"type":"number","description":"Replies from a person"},"bounces":{"type":"number"}},"required":["date","sent","replies","bounces"]},"OutreachStatsDto":{"type":"object","properties":{"startDate":{"type":"string","example":"2026-09-11"},"endDate":{"type":"string","example":"2026-10-10"},"sent":{"description":"Sequence emails sent in the period (a bounced email still went out)","allOf":[{"$ref":"#/components/schemas/SequenceCountsDto"}]},"yourReplies":{"type":"number","description":"Replies you wrote, sent in the period"},"replies":{"type":"number","description":"Replies from a person, received in the period"},"autoReplies":{"type":"number","description":"Out-of-office and other automatic replies"},"bounces":{"type":"number","description":"Emails found undeliverable in the period"},"contacted":{"type":"number","description":"Sites whose first email went out in the period"},"replied":{"type":"number","description":"Of those, how many a person answered (at any time since)"},"replyRate":{"type":"number","nullable":true,"example":0.12,"description":"replied / contacted, 0 to 1; null when nothing was contacted"},"bounceRate":{"type":"number","nullable":true,"example":0.02,"description":"bounces / sent.total, 0 to 1; null when nothing was sent"},"repliesTo":{"description":"Which email the first replies of the period answered","allOf":[{"$ref":"#/components/schemas/SequenceCountsDto"}]},"medianHoursToReply":{"type":"number","nullable":true,"example":26.5,"description":"Median hours from the first email to the first reply"},"scheduledNow":{"type":"number","description":"Conversations with an email waiting to go out, right now"},"daily":{"description":"Every day of the period, oldest first","type":"array","items":{"$ref":"#/components/schemas/OutreachStatsDayDto"}}},"required":["startDate","endDate","sent","yourReplies","replies","autoReplies","bounces","contacted","replied","replyRate","bounceRate","repliesTo","medianHoursToReply","scheduledNow","daily"]},"MailboxCapacityDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","example":"alex@yourbrand.com"},"displayName":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","paused","error"]},"lastError":{"type":"string","nullable":true},"dailyLimit":{"type":"number","example":10,"description":"Automated emails it sends per day"},"sentToday":{"type":"number","description":"Automated emails sent since midnight US Central"},"queued":{"type":"number","description":"Emails waiting that are bound to it, mostly follow-ups. A first email is bound once a mailbox picks it"},"nextSendAt":{"format":"date-time","type":"string","nullable":true,"description":"The earliest it may send its next email"}},"required":["id","email","displayName","status","lastError","dailyLimit","sentToday","queued","nextSendAt"]},"OutreachLinkDto":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["paid","exchange","free"]},"status":{"type":"string","enum":["pending","live","lost"],"description":"Their link to us. pending: not placed yet, also when we already placed ours. live: it is up. lost: they removed it."},"url":{"type":"string","nullable":true},"ourUrl":{"type":"string","nullable":true},"contactEmail":{"type":"string","nullable":true},"priceUsd":{"type":"number","nullable":true},"dofollow":{"type":"boolean"},"note":{"type":"string","nullable":true},"outreachId":{"type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","type","status","url","ourUrl","contactEmail","priceUsd","dofollow","note","outreachId","createdAt","updatedAt"]},"CreateOutreachLinkDto":{"type":"object","properties":{"type":{"type":"string","enum":["paid","exchange","free"]},"status":{"type":"string","enum":["pending","live","lost"],"default":"pending","description":"Their link to us. pending: not placed yet, also when we already placed ours. live: it is up. lost: they removed it."},"dofollow":{"type":"boolean","default":false,"description":"Whether the link was agreed as dofollow"},"url":{"type":"string","nullable":true,"example":"https://blog.acme.com/best-seo-tools","description":"Their page that links to us"},"ourUrl":{"type":"string","nullable":true,"example":"https://example.com/blog/seo-guide","description":"Our page that links to them, for exchanges"},"contactEmail":{"type":"string","nullable":true,"example":"jane@acme.com"},"priceUsd":{"type":"number","nullable":true,"example":150,"description":"Whole US dollars"},"note":{"type":"string","nullable":true},"outreachId":{"type":"string","nullable":true,"description":"The outreach opportunity the deal came from"}},"required":["type"]},"UpdateOutreachLinkDto":{"type":"object","properties":{"url":{"type":"string","nullable":true,"example":"https://blog.acme.com/best-seo-tools","description":"Their page that links to us"},"ourUrl":{"type":"string","nullable":true,"example":"https://example.com/blog/seo-guide","description":"Our page that links to them, for exchanges"},"contactEmail":{"type":"string","nullable":true,"example":"jane@acme.com"},"priceUsd":{"type":"number","nullable":true,"example":150,"description":"Whole US dollars"},"note":{"type":"string","nullable":true},"outreachId":{"type":"string","nullable":true,"description":"The outreach opportunity the deal came from"},"type":{"type":"string","enum":["paid","exchange","free"]},"status":{"type":"string","enum":["pending","live","lost"],"description":"Their link to us. pending: not placed yet, also when we already placed ours. live: it is up. lost: they removed it."},"dofollow":{"type":"boolean","description":"Whether the link was agreed as dofollow"}}},"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"]}}}}