{"openapi":"3.1.0","info":{"title":"Seoxpert Public API","version":"1.0.0","description":"Programmatic access to Seoxpert scans, reports, and domains. Authenticate with a Pro+ API token (`Authorization: Bearer sxp_live_...`). Rate-limited to 60 req/min on Pro, 600 req/min on Agency. See /api-tokens for token management.","contact":{"name":"Seoxpert support","email":"support@seoxpert.io","url":"https://seoxpert.io/contact"},"license":{"name":"Proprietary","url":"https://seoxpert.io/terms"}},"servers":[{"url":"https://seoxpert.io","description":"Production"}],"security":[{"ApiToken":[]}],"tags":[{"name":"Scans","description":"Trigger and inspect scans."},{"name":"Reports","description":"Download scan reports as JSON, CSV, PDF, Markdown, or HTML."},{"name":"Domains","description":"Manage the registered-domain list scoped to your token's workspace."}],"components":{"securitySchemes":{"ApiToken":{"type":"http","scheme":"bearer","bearerFormat":"sxp_live_<32 base32 chars>","description":"Mint at /account?tab=api. Pro+ only. Required scopes documented per endpoint."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Stable string (UUID, content-hash, etc.) to dedup retries. The server records the (customer, key) → scanId mapping for 24h. Subsequent calls with the same key return the original scanId without consuming credit. Max 255 chars.","schema":{"type":"string","maxLength":255}}},"headers":{"RateLimitLimit":{"description":"Per-minute request cap for the calling token (plan-derived).","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests remaining in the current 60s window.","schema":{"type":"integer"}},"RateLimitReset":{"description":"Unix epoch seconds at which the current window resets.","schema":{"type":"integer"}},"RetryAfter":{"description":"Seconds to wait before the next request is allowed (only set on 429).","schema":{"type":"integer"}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable message."},"code":{"type":"string","description":"Stable identifier for programmatic branching."},"details":{"type":"object","additionalProperties":true}}},"Scan":{"type":"object","properties":{"id":{"type":"string"},"rootUrl":{"type":"string","format":"uri"},"serviceId":{"type":"string","enum":["full-scan","seo-content-scan","security-performance-scan"]},"crawlMode":{"type":"string","enum":["auto","html","rendered"]},"status":{"type":"string","enum":["queued","running","completed","failed"]},"overallHealthScore":{"type":"integer","minimum":0,"maximum":100,"nullable":true},"findingsCount":{"type":"integer"},"pagesCrawled":{"type":"integer"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true},"errorMessage":{"type":"string","nullable":true}}},"ScanCreateRequest":{"type":"object","required":["rootUrl"],"properties":{"rootUrl":{"type":"string","format":"uri"},"serviceId":{"type":"string","enum":["full-scan","seo-content-scan","security-performance-scan"],"default":"full-scan"},"crawlMode":{"type":"string","enum":["auto","html","rendered"],"default":"rendered"},"includeSubdomains":{"type":"boolean","default":true},"maxPages":{"type":"integer","minimum":1,"maximum":500,"default":50},"maxDepth":{"type":"integer","minimum":1,"maximum":10,"default":5}}},"ScanCreateResponse":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued"]},"replayed":{"type":"boolean","description":"true when the response replayed a prior Idempotency-Key match (no new scan created, no credit consumed)."},"workspace":{"type":"object","properties":{"slug":{"type":"string"},"project":{"type":"string"}}}}},"ScanList":{"type":"object","properties":{"scans":{"type":"array","items":{"$ref":"#/components/schemas/Scan"}},"total":{"type":"integer"},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}},"Domain":{"type":"object","properties":{"id":{"type":"string"},"rootUrl":{"type":"string","format":"uri"},"label":{"type":"string"},"addedAt":{"type":"string","format":"date-time"},"deployTokenPrefix":{"type":"string","nullable":true,"description":"First few chars of a registered deploy token (e.g. \"sxp_deploy_a3f7\"); null if no deploy hook is configured."},"deployTokenCreatedAt":{"type":"string","format":"date-time","nullable":true}}},"DomainList":{"type":"object","properties":{"domains":{"type":"array","items":{"$ref":"#/components/schemas/Domain"}},"maxDomains":{"type":"integer","nullable":true,"description":"Plan-derived cap. null = unlimited (legacy / internal)."}}}},"responses":{"Unauthorized":{"description":"Missing, malformed, revoked, or expired token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Token authenticated but lacks the required scope, or workspace plan no longer permits API access.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Per-token rate limit exceeded. The Retry-After header indicates seconds to wait.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"Monthly scan quota reached. Upgrade or wait for the next cycle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/scans":{"post":{"tags":["Scans"],"summary":"Trigger a scan","description":"Queue a scan against a registered domain. Required scope: `scans:write`. Pass an `Idempotency-Key` header to safely retry on network failure — the same key replays the original scan id for 24 hours. Sets `triggeredBy: \"api\"` on the resulting scan record.","security":[{"ApiToken":["scans:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanCreateRequest"}}}},"responses":{"200":{"description":"Scan queued (or replayed via Idempotency-Key).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanCreateResponse"}}}},"400":{"description":"Validation error (bad URL, schema mismatch, oversized Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"tags":["Scans"],"summary":"List scans","description":"List scans scoped to the token's workspace. Required scope: `scans:read`.","security":[{"ApiToken":["scans:read"]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string"},"description":"Filter by status (queued, running, completed, failed, …)."},{"name":"serviceId","in":"query","schema":{"type":"string"},"description":"Filter by service id."},{"name":"crawlMode","in":"query","schema":{"type":"string","enum":["auto","html","rendered"]}},{"name":"search","in":"query","schema":{"type":"string"},"description":"Substring match against rootUrl."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated list of scans.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/scans/{id}/report":{"get":{"tags":["Reports"],"summary":"Download a scan report","description":"Download a scan report in the requested format. Required scope: `reports:read`. The PDF format respects white-label branding for Agency-tier scans.","security":[{"ApiToken":["reports:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Scan id (returned by POST /api/scans)."},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv","markdown","md","html","pdf"],"default":"json"},"description":"`md` is accepted as an alias for `markdown`."},{"name":"view","in":"query","schema":{"type":"string","enum":["issues","pages"],"default":"issues"},"description":"For CSV format only — switch between the per-finding view (default) and the per-page view."}],"responses":{"200":{"description":"Report contents in the requested format.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"X-Seoxpert-Findings-Truncated":{"description":"Set to \"true\" when the response was clipped by the plan-derived export cap.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"text/csv":{"schema":{"type":"string"}},"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}},"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Unsupported format.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Scan not found in the token's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/domains":{"get":{"tags":["Domains"],"summary":"List registered domains","description":"List all domains registered to the token's workspace owner. Required scope: `domains:read`.","security":[{"ApiToken":["domains:read"]}],"responses":{"200":{"description":"List of domains and the plan's cap.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Domains"],"summary":"Register a domain","description":"Add a domain to the token's workspace. Required scope: `domains:write`. Idempotent — re-registering an existing domain returns the existing row. Rejects when the workspace already has `maxDomains` entries.","security":[{"ApiToken":["domains:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rootUrl"],"properties":{"rootUrl":{"type":"string","format":"uri","minLength":1,"maxLength":500},"label":{"type":"string","maxLength":120,"default":""}}}}}},"responses":{"200":{"description":"Domain registered (or already existed — idempotent).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"string"},"domain":{"$ref":"#/components/schemas/Domain"}}}}}},"400":{"description":"Invalid URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan domain cap reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}