{"openapi":"3.1.0","info":{"title":"SerpDino REST API","version":"1.0.0","summary":"Programmatic access to keyword rankings, SERP data, keyword research, competitor analysis, and Core Web Vitals.","description":"Programmatic access to keyword rankings, SERP data, keyword research, competitor analysis, and Core Web Vitals.\n\n## Authentication\nSend `Authorization: Bearer sd_your_key_here` on every request. Create keys in Dashboard → Settings → API Keys.\n\n## Error handling\n- All error responses are JSON. The error message lives in the `message` field (NOT `error`).\n- Some responses include `success: false` alongside `message`; account/auth errors may omit `success` entirely. Clients should treat any non-2xx status OR `success === false` as an error.\n- `/api/tools/keyword-research` returns HTTP 200 with `{ success: false, message }` for all expected failures (bad seed, unsupported geo/lang, upstream timeout). Always check `success` in addition to HTTP status.\n- `/api/projects/keyword-ideas` returns HTTP 200 with `{ success: true, ideas: [] }` even when the upstream scraper is unreachable — an empty `ideas` array means \"no data available right now,\" not necessarily success.\n- Write endpoints that mutate project data (`POST/DELETE` on `/api/projects/keywords`, `/api/scrape/new-keywords`, `POST/DELETE` on `/api/projects/notes`, `/api/tools/keyword-research`, `/api/projects/keyword-ideas`, `/api/projects/keyword-suggestions`, folder writes) require an **active subscription** — return HTTP 403 `{ message: \"Account suspended\", suspended: true }` if the account is suspended.\n- Many read endpoints bypass auth entirely when the target project has `shared: true` (public dashboard links). Affected: `/api/projects/:id`, `keyword-updates`, `position-history`, `keyword-volumes`, `competitor-positions`, `pages`, `pages-pagespeed`, `notes` (GET), `export`, `export-agent`.\n- These domain-level read endpoints are fully **public** (no API key required): `/api/projects/competitors-filtered`, `/api/projects/pagespeed`, `/api/projects/crux-history`, `/api/projects/similarweb`. They read from our DB cache only — no auth needed. Each is rate-limited to 60 req / min (600 / hour) per IP.\n\n## MCP\nRemote MCP endpoint: https://serpdino.com/api/mcp. Works with Claude, ChatGPT, Cursor, and any MCP-compatible client.","contact":{"email":"support@serpdino.com","url":"https://serpdino.com"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://serpdino.com"}],"tags":[{"name":"Projects","description":"Create and manage SEO tracking projects."},{"name":"Folders","description":"Organise projects into folders."},{"name":"Keywords","description":"Track keywords inside a project."},{"name":"Ranking Data","description":"Read keyword positions, SERP snapshots, and volume data."},{"name":"Keyword Research","description":"Discover new keywords and run live SERP / traffic checks."},{"name":"Competitors","description":"Compare competitor rankings and traffic."},{"name":"Performance","description":"Page-level rankings and Core Web Vitals."},{"name":"Notes","description":"Timeline annotations for a project."},{"name":"Account","description":"Check usage and limits."},{"name":"Export","description":"Export project data as Markdown or CSV."},{"name":"API Keys","description":"Create and revoke API keys programmatically."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"sd_xxxxxxxx","description":"API key with `sd_` prefix. Send as `Authorization: Bearer sd_your_key_here`."}}},"security":[{"ApiKeyAuth":[]}],"paths":{"/api/projects":{"get":{"tags":["Projects"],"summary":"List all projects","operationId":"get_api_projects","responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"projects":[{"_id":"65f1c2a8e1234567890abcde","name":"Acme Corp","domain":"acme.com","aliases":[],"competitors":["competitor.com"],"folder":null,"updateFrequency":"daily","serpTop":50,"createdDate":"2025-01-15T08:32:11.000Z"}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"post":{"tags":["Projects"],"summary":"Create a new project","description":"Returns HTTP 201 on success. Fires fire-and-forget triggers for keyword suggestions, SimilarWeb, PageSpeed, and competitor analysis.","operationId":"post_api_projects","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Project name"},"domain":{"type":"string","description":"Target domain (e.g. example.com). Lowercased server-side."},"folder":{"type":"string","description":"Folder ID to place project in"},"icon":{"type":"string","description":"Icon identifier or URL"},"aliases":{"type":"string","description":"Comma/newline-separated alternative domains (treated as same property)"},"competitors":{"type":"array","items":{"type":"string"},"description":"Competitor domains (PUT only — POST ignores this field)"},"shared":{"type":"boolean","description":"Whether project is shared via public link (default: false)"},"updateFrequency":{"type":"string","description":"daily | every3days | weekly | biweekly | custom (default: daily)","enum":["daily","every3days","weekly","biweekly","custom"]},"customUpdateDays":{"type":"array","items":{"type":"number"},"description":"Required when updateFrequency=custom. Array of weekday numbers 0–6 (0=Sunday)."},"updateTime":{"type":"number","description":"Hour of day 0–23 to run updates (default: 12)"},"updateTimezone":{"type":"string","description":"IANA timezone name (default: UTC)"},"serpTop":{"type":"number","description":"50 | 100 — depth of SERP scrape (default: 50)","enum":[50,100]}},"required":["name","domain"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"example":{"success":true,"project":{"_id":"65f1c2a8e1234567890abcde","name":"My Project","domain":"example.com","updateFrequency":"daily","serpTop":50},"message":"Project created successfully"}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"put":{"tags":["Projects"],"summary":"Update project settings","description":"_id, name, and domain are ALL required (the handler rejects with 400 if any is missing). To change a single field, send the existing values for the others.","operationId":"put_api_projects","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","description":"Project ID"},"name":{"type":"string","description":"Project name (required even if unchanged)"},"domain":{"type":"string","description":"Target domain (required even if unchanged)"},"competitors":{"type":"array","items":{"type":"string"},"description":"Array of competitor domains"},"folder":{"type":"string","description":"Folder ID, or empty string to remove from folder"},"icon":{"type":"string","description":"Icon identifier or URL"},"aliases":{"type":"string","description":"Comma/newline-separated alias domains"},"shared":{"type":"boolean","description":"Public sharing toggle"},"updateFrequency":{"type":"string","description":"daily | every3days | weekly | biweekly | custom","enum":["daily","every3days","weekly","biweekly","custom"]},"customUpdateDays":{"type":"array","items":{"type":"number"},"description":"Required when updateFrequency=custom. Weekday numbers 0–6."},"updateTime":{"type":"number","description":"Hour 0–23"},"updateTimezone":{"type":"string","description":"IANA timezone"},"serpTop":{"type":"number","description":"50 | 100","enum":[50,100]}},"required":["_id","name","domain"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"project":{"_id":"65f1c2a8e1234567890abcde","name":"Acme","domain":"acme.com","competitors":["competitor.com"]},"message":"Project updated successfully"}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"delete":{"tags":["Projects"],"summary":"Delete a project and all its data","operationId":"delete_api_projects","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","description":"Project ID"}},"required":["_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"message":"Project deleted successfully","projects":[],"folders":[]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get project details","description":"Includes keywords[], domain, competitors[], and full settings.","operationId":"get_api_projects_id","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Project ID (Mongo ObjectId)"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"project":{"_id":"65f1c2a8e1234567890abcde","name":"Acme Corp","domain":"acme.com","competitors":["competitor.com"],"keywords":[{"_id":"65f1c2a8e1234567890fffff","keyword":"seo tools","geoCode":"US","langCode":"en"}]}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/folders":{"get":{"tags":["Folders"],"summary":"List folders","operationId":"get_api_projects_folders","responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"folders":[{"_id":"FOLDER_ID","name":"Clients","position":0}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"post":{"tags":["Folders"],"summary":"Create folder","description":"Returns HTTP 409 `{ message: \"Folder with this name already exists\" }` if a folder with that name (case-insensitive) already exists for this user. Requires active subscription.","operationId":"post_api_projects_folders","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Folder name"}},"required":["name"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"example":{"success":true,"folder":{"_id":"FOLDER_ID","name":"My Folder","position":0},"message":"Folder created successfully"}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"put":{"tags":["Folders"],"summary":"Rename folder","description":"Returns HTTP 409 on name conflict. Requires active subscription.","operationId":"put_api_projects_folders","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"},"description":"Folder ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"New folder name"}},"required":["name"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"folder":{"_id":"FOLDER_ID","name":"New Name"},"message":"Folder updated successfully"}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"delete":{"tags":["Folders"],"summary":"Delete folder","description":"Detaches all projects in the folder (they remain, just unassigned), then deletes the folder. Returns the refreshed folders[] and projects[] lists. Requires active subscription.","operationId":"delete_api_projects_folders","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"},"description":"Folder ID"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"message":"Folder \"My Folder\" deleted successfully","folders":[],"projects":[]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/keywords":{"post":{"tags":["Keywords"],"summary":"Add keywords to track in a project","description":"Max 3500 keywords per project. Keywords appear in rankings after the next scheduled SERP check. Requires active subscription. Duplicates within the request and against existing project keywords are silently deduped. When the limit is exceeded, returns HTTP 400 with extended fields `{ error: \"KEYWORDS_LIMIT_EXCEEDED\", currentCount, maxAllowed, availableSlots }`.","operationId":"post_api_projects_keywords","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","description":"Project ID"},"keywords":{"type":"array","items":{"type":"string"},"description":"Array of keyword strings (lowercased server-side)"},"geoCode":{"type":"string","description":"ISO-2 country code (e.g. US). Required — no default."},"langCode":{"type":"string","description":"ISO-2 language code (e.g. en). Required — no default."}},"required":["projectId","keywords","geoCode","langCode"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"message":"Added 2 new keywords","addedCount":2,"project":{"_id":"PROJECT_ID","keywords":["KW_ID_1","KW_ID_2"]}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"delete":{"tags":["Keywords"],"summary":"Remove tracked keywords","operationId":"delete_api_projects_keywords","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","description":"Project ID"},"keywordIds":{"type":"array","items":{"type":"string"},"description":"Array of keyword IDs to remove"}},"required":["projectId","keywordIds"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"removedCount":2,"project":{"_id":"PROJECT_ID"}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/scrape/new-keywords":{"post":{"tags":["Keywords"],"summary":"Trigger a fresh SERP check for keywords","description":"Async — returns immediately (HTTP 200, status: \"processing\") while scraping happens in the background. Costs 1 balance credit per keyword (×2 for serpTop=100); rejects with HTTP 400 \"Insufficient balance\" if `keywordIds.length > virtualBalance`. Pass all project keyword IDs to refresh everything. Requires active subscription. All `keywordIds` must belong to the given project.","operationId":"post_api_scrape_new_keywords","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","description":"Project ID"},"keywordIds":{"type":"array","items":{"type":"string"},"description":"Keyword IDs to refresh (must belong to projectId)"}},"required":["projectId","keywordIds"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"message":"Accepted 10 keywords for processing. Position updates started in background mode.","status":"processing","summary":{"total":10,"accepted":10,"errors":0}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/keyword-updates":{"get":{"tags":["Ranking Data"],"summary":"Get keyword position history","description":"Public when project is shared. Response.data is keyed by keyword ID; each entry contains the full Keywords document plus an `updates` array of dated check results. When startDate/endDate are omitted, defaults to last 30 days.","operationId":"get_api_projects_keyword_updates","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"},{"name":"startDate","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD (default: 30 days ago)"},{"name":"endDate","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD (default: today)"},{"name":"aggregation","in":"query","required":false,"schema":{"type":"string","enum":["daily","weekly","monthly"]},"description":"daily | weekly | monthly (default: daily)"},{"name":"search","in":"query","required":false,"schema":{"type":"string"},"description":"Filter keywords by substring (case-insensitive)"},{"name":"geoCode","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by country code (uppercased)"},{"name":"langCode","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by language code (lowercased)"},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","enum":["keyword","position","volume"]},"description":"keyword | position | volume"},{"name":"sortOrder","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"]},"description":"asc | desc (default: desc)"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":{"65f1c2a8e1234567890fffff":{"keyword":{"_id":"65f1c2a8e1234567890fffff","value":"seo tools","geoCode":"US","langCode":"en","avgMonthlySearches":8100},"updates":[{"_id":"UPD_ID","date":"2025-01-16T12:00:00.000Z","status":"success","position":{"position":9,"url":"https://example.com/seo-tools","title":"...","domain":"example.com"},"aiSerpId":null},{"_id":"UPD_ID2","date":"2025-01-15T12:00:00.000Z","status":"success","position":{"position":12,"url":"https://example.com/seo-tools","title":"...","domain":"example.com"},"aiSerpId":null}]}}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/position-history":{"get":{"tags":["Ranking Data"],"summary":"Get full SERP snapshot for a keyword check","description":"Returns top 30 results plus the project domain itself if it ranks beyond 30. Each result includes movement vs the previous successful check (`up`/`down`/`same`/`new`). Public when project is shared.","operationId":"get_api_projects_position_history","parameters":[{"name":"keywordUpdateId","in":"query","required":true,"schema":{"type":"string"},"description":"Keyword update ID (from keyword-updates response)"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":{"keywordId":"65f1c2a8e1234567890fffff","keywordValue":"seo tools","date":"2025-01-16T12:00:00.000Z","resultList":[{"position":1,"title":"...","domain":"example.com","url":"https://example.com","previousPosition":2,"movement":{"type":"up","value":1}}],"droppedDomains":[{"domain":"old-competitor.com","previousPosition":15,"movement":{"type":"gone","value":null}}],"hasPreviousData":true,"aiOverviewResults":[{"position":1,"title":"...","domain":"wikipedia.org","url":"https://...","isOurDomain":false}],"aiContainsOurDomain":false}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/keyword-volumes":{"get":{"tags":["Ranking Data"],"summary":"Get search volume, CPC, and competition data","description":"Two modes with different response shapes. Bulk (no keywordId) uses compact field names to minimise payload size. Single mode (with keywordId) returns the full per-keyword detail. CPC values are pre-converted to the project currency. Public when project is shared.","operationId":"get_api_projects_keyword_volumes","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"},{"name":"keywordId","in":"query","required":false,"schema":{"type":"string"},"description":"Single keyword ID — when present, returns full volume history for one keyword instead of the bulk map"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":{"65f1c2a8e1234567890fffff":{"v":"seo tools","g":"US","l":"en","s":"google","lv":8100,"tr":[6600,7200,7800,8000,8100,8100,7900,8000,8100,8200,8000,8100],"am":8100,"ci":72,"cl":3.1,"ch":5.4}}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/tools/keyword-research":{"post":{"tags":["Keyword Research"],"summary":"Research keyword ideas with volumes, CPC, competition, and trends","description":"Requires active subscription. Rate limited for API-key callers: 10 req / min, 100 req / hour per account — HTTP 429 with `retryAfter` (seconds) when exceeded. All expected failures (bad seed, unsupported geo/lang, upstream timeout, scraper error) are returned as HTTP 200 with `{ success: false, message, error? }` — always check `success`. 60s upstream timeout. `pageSize` is clamped 10–500, `trendsForTopN` is clamped 0–200.","operationId":"post_api_tools_keyword_research","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"seed":{"type":"string","description":"Seed keyword, URL, or domain (mode-dependent)"},"mode":{"type":"string","description":"keyword | url | domain (default: keyword)","enum":["keyword","url","domain"]},"geo":{"type":"string","description":"ISO-2 country code (default: US)"},"lang":{"type":"string","description":"ISO-2 language code (default: en)"},"pageSize":{"type":"number","description":"10–500 (default: 100)"},"trendsForTopN":{"type":"number","description":"0–200 (default: 60) — number of top ideas to enrich with monthly trend data"},"enrichWithLlm":{"type":"boolean","description":"Apply LLM intent/cluster tagging (default: true)"}},"required":["seed"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"ideas":[{"keyword":"best seo tools","avgMonthlySearches":8100,"competition":"HIGH","cpc":4.21,"intent":"commercial","cluster":"tools"}],"meta":{"totalResults":100,"geo":"US","lang":"en"}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/tools/serp-check":{"post":{"tags":["Keyword Research"],"summary":"Live SERP check — returns top results and search volume for a keyword","description":"Rate limited: 5 req / 60s. When called with an API key (`Authorization: Bearer sd_...`), the limit is **per API user**. When called from the web (Turnstile token), the limit is per client IP. 429 response body includes `retryAfter` (seconds), `limit`, `window`. Returns HTTP 502 if both upstream SERP and volume calls fail.","operationId":"post_api_tools_serp_check","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"keyword":{"type":"string","description":"Search query"},"geo":{"type":"string","description":"ISO-2 country code (e.g. US)"},"lang":{"type":"string","description":"ISO-2 language code (e.g. en)"}},"required":["keyword","geo","lang"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":{"serp":[{"position":1,"url":"https://...","title":"...","snippet":"..."}],"volume":{"value":1300,"cpc":2.5,"competition":"MEDIUM"}},"remaining":4}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/tools/traffic-check":{"post":{"tags":["Keyword Research"],"summary":"Domain traffic check — returns SimilarWeb stats and PageSpeed data","description":"Rate limited: 5 req / 60s — per API user when called with an API key, per IP when called from the web. Triggers a live scraper fetch on DB cache miss for either SimilarWeb or PageSpeed (slow path), or returns cached data immediately. `data` may be null if SimilarWeb has no info on the domain. Domain is validated and normalised server-side (strips protocol, `www.`, path).","operationId":"post_api_tools_traffic_check","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","description":"Target domain (e.g. example.com or https://www.example.com/path — normalised server-side)"}},"required":["domain"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"domain":"example.com","data":{"stats":{"domain":"example.com","totalVisits":1234567,"engagementMetrics":{"bounceRate":0.42,"avgVisitDuration":134},"fetchedAt":"2025-01-16T08:00:00.000Z"},"monthlyVisits":[{"domain":"example.com","month":"2025-01-01","visits":1234567}]},"pagespeed":{"domain":"example.com","performanceScore":86,"cruxScore":78,"largestContentfulPaint":1.8,"cumulativeLayoutShift":0.05,"fetchedAt":"2025-01-16T08:00:00.000Z"},"remaining":4}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/keyword-suggestions":{"get":{"tags":["Keyword Research"],"summary":"AI-generated keyword suggestions for a project","description":"Polling endpoint — `ready: false` (with no other fields) means the LLM pipeline is still running. Retry every few seconds for ~2 minutes max. Generation is auto-triggered at project creation, so this is just a reader over the cached suggestions doc. Requires active subscription. Each locale ships up to 30 phrases, sorted by lastVolume desc.","operationId":"get_api_projects_keyword_suggestions","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"ready":true,"domain":"example.com","primary":{"lang":"en","geo":"US"},"locales":[{"key":"en-US","lang":"en","region":"US","geo":"US","lastUpdatedAt":"2025-01-16T08:00:00.000Z","keywords":[{"phrase":"best seo tools","lastVolume":8100,"volume":[{"year":2024,"months":[{"month":12,"value":8100}]}]}]}],"pages":[],"meta":{"seedUsed":"domain"},"runCount":1,"lastRunAt":"2025-01-16T08:00:00.000Z","lastError":null}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/keyword-ideas":{"post":{"tags":["Keyword Research"],"summary":"Keyword ideas based on a project's domain","description":"Soft-fail endpoint: always returns HTTP 200 with `success: true` when authorised. On upstream errors returns `{ success: true, ideas: [], error: \"upstream_error\" | \"service_unavailable\" }` — an empty `ideas` array does NOT mean failure, it means \"no data available right now\". 30s timeout on the upstream call. Requires active subscription. Rate limited for API-key callers: 10 req / min, 100 req / hour per account — HTTP 429 with `retryAfter` when exceeded.","operationId":"post_api_projects_keyword_ideas","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","description":"Project ID"},"geo":{"type":"string","description":"Country code (default: US, uppercased)"},"lang":{"type":"string","description":"Language code (default: en, lowercased)"}},"required":["projectId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"ideas":[{"keyword":"seo tools comparison","avgMonthlySearches":1000,"competition":"MEDIUM"}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/competitor-positions":{"get":{"tags":["Competitors"],"summary":"Get a competitor's ranking positions across all tracked keywords","description":"Searches the project's tracked keywords' SERP history for results matching the competitor domain. `position: 0` means the competitor didn't appear in the top 30 results for that check. Public when project is shared. Deduped to one entry per calendar day per keyword.","operationId":"get_api_projects_competitor_positions","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"},{"name":"competitorDomain","in":"query","required":true,"schema":{"type":"string"},"description":"Competitor domain (e.g. competitor.com — normalised server-side)"},{"name":"startDate","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD"},{"name":"endDate","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"keywordHistory":{"65f1c2a8e1234567890fffff":{"keyword":{"_id":"65f1c2a8e1234567890fffff","value":"seo tools","geoCode":"US","langCode":"en"},"updates":[{"_id":"UPD_ID","date":"2025-01-15T12:00:00.000Z","status":"success","position":{"position":4,"domain":"competitor.com","title":"-","url":"-"},"aiSerpId":null}]}}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/competitors-filtered":{"get":{"tags":["Competitors"],"summary":"Compare traffic and performance across a domain and its competitors","description":"PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from the SimilarWeb / PageSpeed cache only (no live fetching). Auto-filters competitors to a balanced sample of 6 (3 above, 3 below main domain traffic) when more than 6 are supplied. `data` is keyed by domain.","operationId":"get_api_projects_competitors_filtered","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string"},"description":"Main domain"},{"name":"competitors","in":"query","required":true,"schema":{"type":"string"},"description":"Comma-separated competitor domains, or repeated query param. Max 50 accepted (HTTP 400 above); auto-filtered to 6 in the response."}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":{"example.com":{"stats":{"domain":"example.com","totalVisits":1234567,"fetchedAt":"2025-01-16T08:00:00.000Z"},"monthlyVisits":[{"month":"2025-01-01","visits":1234567}],"pageSpeed":{"cruxScore":78}}},"filtered":["competitor.com"],"mainDomainVisits":1234567,"totalCompetitors":1}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/pages":{"get":{"tags":["Performance"],"summary":"Page-level ranking data: which URLs rank, average position, trend sparklines","description":"Aggregated by the projectPagesPoller. Each page includes 14-point trend data for the last 90 days, bucketed weekly (or daily if there is not enough data for a week-bucket). Public when project is shared. Sorted worst-first by avgPosition.","operationId":"get_api_projects_pages","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":[{"url":"https://example.com/blog/post","path":"/blog/post","keywordCount":12,"avgPosition":8.4,"bestPosition":3,"keywords":[{"keywordId":"KW_ID","value":"seo tools","position":5}],"avgPositionTrend":[{"date":"2025-01-06T00:00:00.000Z","avgPosition":9.2,"keywordCount":12}],"avgPositionTrendGranularity":"week","weeklyAvgPositions":[{"weekStart":"2025-01-06T00:00:00.000Z","avgPosition":9.2,"keywordCount":12}]}],"lastPagesUpdate":"2025-01-16T08:00:00.000Z"}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/pagespeed":{"get":{"tags":["Performance"],"summary":"PageSpeed Insights (Core Web Vitals) for a domain","description":"PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from DB cache only (never triggers a live PageSpeed run). Returns `data: null` if domain not in cache. Domain is normalised (strips `www.`, lowercased).","operationId":"get_api_projects_pagespeed","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string"},"description":"Target domain"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":{"domain":"example.com","performanceScore":86,"accessibilityScore":92,"bestPracticesScore":90,"seoScore":95,"cruxScore":78,"largestContentfulPaint":1.8,"firstContentfulPaint":1,"cumulativeLayoutShift":0.05,"totalBlockingTime":120,"cruxFieldData":{"LCP":{"p75":1900},"CLS":{"p75":0.05},"INP":{"p75":180}},"fetchedAt":"2025-01-16T08:00:00.000Z"}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/pages-pagespeed":{"get":{"tags":["Performance"],"summary":"Per-page Lighthouse lab metrics for all tracked pages in a project","description":"Public when project is shared. `pages` is keyed by URL. `baseline` is the home-page record when available, falling back to the domain-level snapshot (with `source: \"domain\"`) so the UI can still compute deltas. Returns `baseline: null` if neither exists.","operationId":"get_api_projects_pages_pagespeed","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"baseline":{"url":"https://example.com/","path":"/","domain":"example.com","performanceScore":86,"largestContentfulPaint":1.8,"firstContentfulPaint":1,"cumulativeLayoutShift":0.04,"totalBlockingTime":120,"source":"page"},"pages":{"https://example.com/blog/post":{"url":"https://example.com/blog/post","path":"/blog/post","performanceScore":80,"largestContentfulPaint":2.1,"source":"page"}}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/crux-history":{"get":{"tags":["Performance"],"summary":"Chrome UX Report (CrUX) real-user performance history for a domain","description":"PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from DB cache only. `data` is the origin-level series (CrUX records with no specific URL); `pages` is per-page records keyed by URL with parsed `path`.","operationId":"get_api_projects_crux_history","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string"},"description":"Target domain"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":[{"domain":"example.com","date":"2025-01-01","lcp":{"p75":1900},"cls":{"p75":0.05}}],"pages":[{"url":"https://example.com/blog/","path":"/blog/","data":[{"date":"2025-01-01","lcp":{"p75":2100}}]}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/similarweb":{"get":{"tags":["Performance"],"summary":"SimilarWeb traffic stats and monthly visit history for a domain","description":"PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from DB cache only. `exists: false` and `data: null` when the domain has never been scraped. `monthlyVisits` capped at 12 months, oldest first.","operationId":"get_api_projects_similarweb","parameters":[{"name":"domain","in":"query","required":true,"schema":{"type":"string"},"description":"Target domain"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"exists":true,"data":{"stats":{"domain":"example.com","totalVisits":1234567,"fetchedAt":"2025-01-16T08:00:00.000Z"},"monthlyVisits":[{"domain":"example.com","month":"2024-02-01","visits":1100000},{"domain":"example.com","month":"2025-01-01","visits":1234567}]}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/notes":{"get":{"tags":["Notes"],"summary":"List timeline notes for a project","operationId":"get_api_projects_notes","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"}],"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"data":[{"_id":"NOTE_ID","date":"2025-01-15","text":"Google algorithm update","color":"warning"}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"post":{"tags":["Notes"],"summary":"Add or update a note on a project timeline","description":"Upsert by `(projectId, date)`: if a note already exists for this date, it is updated in place and the response is HTTP 200 with `message: \"Note updated\"`. Otherwise a new note is created and the response is HTTP 201 with `message: \"Note saved\"`. Requires active subscription.","operationId":"post_api_projects_notes","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","description":"Project ID"},"date":{"type":"string","description":"YYYY-MM-DD (interpreted as 00:00:00 UTC)"},"text":{"type":"string","description":"Note content"},"color":{"type":"string","description":"info | warning | success | danger (default: info, only set on new notes)","enum":["info","warning","success","danger"]}},"required":["projectId","date","text"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"example":{"success":true,"message":"Note saved","data":{"_id":"NOTE_ID","projectId":"PROJECT_ID","date":"2025-01-15T00:00:00.000Z","text":"Google algorithm update","color":"warning"}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"delete":{"tags":["Notes"],"summary":"Delete a note","description":"Returns the remaining notes for the project in `data` after deletion. Requires active subscription.","operationId":"delete_api_projects_notes","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"noteId":{"type":"string","description":"Note ID"}},"required":["noteId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"message":"Note deleted","data":[{"_id":"OTHER_NOTE_ID","date":"2025-01-10T00:00:00.000Z","text":"...","color":"info"}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/user/capacity":{"get":{"tags":["Account"],"summary":"Check account usage and limits","operationId":"get_api_user_capacity","responses":{"200":{"description":"Success","content":{"application/json":{"example":{"total":1000,"booked":320,"available":680,"projects":4,"keywords":320,"isTrial":false}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/export-agent":{"get":{"tags":["Export"],"summary":"Generate a comprehensive Markdown report for a project","description":"Returns plain Markdown text (Content-Type: text/markdown), NOT JSON. Includes rankings, traffic, competitors, and performance data in a single document — designed for feeding to LLMs.","operationId":"get_api_projects_export_agent","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"}],"responses":{"200":{"description":"Success","content":{"text/markdown":{"schema":{"type":"string"},"example":"# Acme Corp — SEO Report\n\nDomain: acme.com\n\n## Rankings\n..."}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/projects/export":{"get":{"tags":["Export"],"summary":"CSV export of ranking data","description":"Returns CSV with UTF-8 BOM and semicolon (`;`) separator — opens correctly in Excel without conversion. `Content-Disposition` header forces download with a generated filename. `full` format: one row per keyword with a date column per checked day. `summary` format: one row per keyword with aggregated stats (current/best/worst/avg position, change). Public when project is shared.","operationId":"get_api_projects_export","parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string"},"description":"Project ID"},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["full","summary"]},"description":"full | summary (default: full)"},{"name":"startDate","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD"},{"name":"endDate","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Success","content":{"text/csv; charset=utf-8":{"schema":{"type":"string"},"example":"Keyword;SERP;Volume;URL;Change;2025-01-16;2025-01-15\nseo tools;en-US;8100;https://example.com/seo-tools;-3;9;12\n"}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/api/user/api-keys":{"get":{"tags":["API Keys"],"summary":"List your API keys","description":"Returns metadata only — secrets are never returned after creation. Each entry includes _id, name, keyPrefix (masked, first 4 chars only), lastUsedAt, createdAt.","operationId":"get_api_user_api_keys","responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true,"apiKeys":[{"_id":"KEY_ID","name":"My Integration","keyPrefix":"sd_a••••","lastUsedAt":"2025-01-16T10:00:00.000Z","createdAt":"2025-01-10T08:00:00.000Z"}]}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"post":{"tags":["API Keys"],"summary":"Create a new API key","description":"`key` is the full secret — shown only once, store it immediately. `apiKey` is metadata. Max 5 active keys per user. Names limited to 100 chars.","operationId":"post_api_user_api_keys","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Key name (max 100 chars)"}},"required":["name"]}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"example":{"success":true,"key":"sd_abcd1234567890abcdef1234567890abcdef1234","apiKey":{"_id":"KEY_ID","name":"My Integration","keyPrefix":"sd_a••••","createdAt":"2025-01-16T10:00:00.000Z"}}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]},"delete":{"tags":["API Keys"],"summary":"Revoke an API key","description":"Soft-revokes by setting `revokedAt`. Accepts `id` from either body or query string. Returns 404 if the key does not exist or already revoked.","operationId":"delete_api_user_api_keys","parameters":[{"name":"id","in":"query","required":false,"schema":{"type":"string"},"description":"API key ID (one of body or query is required)"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"API key ID (one of body or query is required)"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"example":{"success":true}}}},"400":{"description":"Bad Request — Missing required parameter or invalid value","content":{"application/json":{"example":{"success":false,"message":"Missing required parameter or invalid value"}}}},"401":{"description":"Unauthorized — Missing or invalid API key. Body: { message } or { success: false }","content":{"application/json":{"example":{"success":false,"message":"Missing or invalid API key. Body: { message } or { success: false }"}}}},"403":{"description":"Forbidden — Account suspended. Body: { message: \"Account suspended\", suspended: true }","content":{"application/json":{"example":{"success":false,"message":"Account suspended. Body: { message: \"Account suspended\", suspended: true }"}}}},"404":{"description":"Not Found — Resource (project, folder, note) does not exist or belongs to another user","content":{"application/json":{"example":{"success":false,"message":"Resource (project, folder, note) does not exist or belongs to another user"}}}},"429":{"description":"Too Many Requests — Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.","content":{"application/json":{"example":{"success":false,"message":"Rate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set."}}}},"500":{"description":"Server Error — Internal server error","content":{"application/json":{"example":{"success":false,"message":"Internal server error"}}}}},"security":[{"ApiKeyAuth":[]}]}}}}