SEO Intelligence
Read reports, findings, keywords, links and code audits, and let your own model write the explanations, suggestions and fixes.
SEO Intelligence keeps a dated history of a website's Google Search Console data, works out every month what genuinely moved, tracks the links pointing at the site, and audits the repository the site is built from. The public API exposes all of it, and it is built so that an external model does the writing: an AI assistant connected through the hosted MCP server, or your own server holding an sk_* key, reads the numbers and writes the explanations, the keyword suggestions, the recommendations and the wording fixes. SoftSolz keeps doing the compute. Base path:
https://app.softsolz.uk/api/v1/services/seoNo endpoint spends SoftSolz AI credits
Every call on this API is free of SoftSolz AI credits, without exception. A monthly report started through the API runs the Search Console sync, the detectors and the ranking, but skips the SoftSolz narration step (the report carries data_quality_json.narration = "skipped_for_api"), so the explanations are yours to write with your own model and your own credits. Anything you write is marked source: "api" with your app or key name, and the dashboard shows a Written by <app> badge beside it.
Connecting Claude, ChatGPT, Gemini or Cursor takes one address and no code; see Connect an AI assistant. Everything below applies equally to a connected assistant (each endpoint is one tool) and to a server calling the REST API with a key.
Start with the guide
One call hands your model everything it needs to use the rest of this API well, and it is read straight from the running code rather than written by hand, so it cannot drift from what the API actually enforces:
GET https://app.softsolz.uk/api/v1/services/seo/guideIt returns:
- workflow - the call order for the whole loop, in sentences, each naming the exact next endpoint.
- hard_rules - what may never be claimed from this data. Read these before writing a single sentence for a customer.
- scoring - the opportunity score formula with its live weights, the effort multipliers and the published click-through curve.
- taxonomy - the families, statuses, intents, indexing states and readiness components, so you never have to guess a valid value.
- evidence_rules - what each kind of number does and does not prove.
The loop
The API is arranged around one question asked repeatedly: what should I target, what should I write, what should I fix, what should I earn links for, and did it work.
# 1. what to do next, already rankedGET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/opportunities
# 2. the evidence behind itGET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/opportunities/{opportunity_id}GET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/topicsGET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/content-gapsGET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/technicalGET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/on-pageGET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/competitors
# 3. your model thinks, with your tokens
# 4. write the decision backPOST https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/briefsPUT https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/briefs/{brief_id}PATCH https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/opportunities/{opportunity_id}
# 5. did it workPOST https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/experimentsPOST https://app.softsolz.uk/api/v1/services/seo/experiments/{experiment_id}/pagesPOST https://app.softsolz.uk/api/v1/services/seo/experiments/{experiment_id}/measureKeyword research with your own tools
Research keywords with your own model and data sources, then hand them to SoftSolz. Add up to 100 at once, each with an optional target page on the website, your notes, the source name and your own estimate of monthly searches and difficulty. Your estimates are stored and shown as yours, never mixed with Google's figures. Then write or improve each target page and record your suggestions. None of this spends SoftSolz AI credits.
POST https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/keywords/bulkPATCH https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/keywords/{keyword_id}GET https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/keywordsPUT https://app.softsolz.uk/api/v1/services/seo/sites/{site_id}/keywords/{keyword_id}/adviceWhat this API will not tell you, and why
These are refusals, not gaps. Each one is enforced in code, so writing around them means writing something the data does not support.
- There is no keyword volume and no keyword difficulty.Search Console publishes neither. Competition is reported only as how many of the workspace's own tracked competitors publish on a topic.
- Impressions are not indexing. Index state comes only from
/sites/{site_id}/index-state, is stamped with when it was checked, and saysNot checkedotherwise. - There is no single average position for a site or a topic, because aggregating it across pages is an average of averages.
- A competitor's traffic and rankings are never reported. Everything under
/competitorsis what a public page of theirs states. - AI search readiness scores page structure only. Nobody can verify whether an AI answer used a page, and this API does not pretend to.
- An experiment without enough comparison pages or days answers
inconclusiveand carries no p-value, rather than a weak number presented as a result.
What you can read
- Websites and their Search Console binding, plus performance totals, series, top pages and top keywords for any range.
- Monthly reports with every finding (detector, severity, the page or query, and the
evidence_jsonnumbers behind it) and every recommendation, ranked. - Tracked keywords with month-by-month positions and the evidence to write advice from.
- Links: backlinks with their verified status, an overview, what needs attention, link prospects, brand mentions, crawls and the internal link map.
- Code: connected repositories, audits with their pass and fail checks, and fix sets.
- Background tasks, so you can poll anything that runs asynchronously.
What your model can write
| Writes | Endpoint | Shows up as |
|---|---|---|
| An explanation for each finding | PUT /runs/{run_id}/narratives (up to 200 at once) or PUT /runs/{run_id}/findings/{finding_id}/narrative | The plain-English text on the finding card |
| Suggestions for a tracked keyword | PUT /sites/{site_id}/keywords/{keyword_id}/advice | Two to five suggestions on the keyword |
| A recommendation | POST /runs/{run_id}/recommendations | A card in the report, linked to the findings it rests on |
| An explanation for a code finding | PUT /code/audits/{audit_id}/findings/{finding_id}/narrative | The text on the audit finding |
| Wording and metadata fixes | POST /sites/{site_id}/code/repos/{repo_id}/fixes then POST /code/fixes/{fix_set_id}/push | A pull request on GitHub |
Write from the numbers, never from guesses. Every finding carries evidence_json with the current and prior metrics, every keyword advice endpoint returns the evidence next to the advice on file, and the dashboard promises its users that every explanation can be traced back to the data it came from.
What SoftSolz keeps doing
The Search Console sync, the detectors, the ranking of findings and recommendations, backlink verification, web searches for brand mentions, crawls, code audits, the design guard and the pull request itself all run on SoftSolz. None of them involve a model, which is why none of them cost credits from either side. Connecting a Google account or a GitHub account stays in the dashboard, because both need a person to sign in.
Worked flow: a monthly report your model explains
1. Start the report
The site needs a Search Console property bound to it (gsc_property_url on the site is not null). Leave out period_start to analyse the latest complete month.
curl -X POST https://app.softsolz.uk/api/v1/services/seo/sites/SITE_ID/runs \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: report-2026-08-acme" \ -d '{"period_start":"2026-08-01"}'{ "data": { "run": { "id": "3f2b9c44-8f4e-4d0a-9c1b-2a7d6e5f1a23", "site_id": "1ccbcc0f-8720-4d84-8053-18b198d0da36", "period_start": "2026-08-01", "period_end": "2026-08-31", "status": "queued", "credits_used": 0, "task_id": "9b1e6d2a-5c3f-4a8e-b7d1-0f2e4c6a8b9d" }, "task_id": "9b1e6d2a-5c3f-4a8e-b7d1-0f2e4c6a8b9d"}}409 run_exists means that month is already analysed, so read it instead. 409 gsc_not_connected means Search Console is not bound yet, and 400 no_period_available means there is no complete month of data.
2. Poll the task
curl https://app.softsolz.uk/api/v1/services/seo/tasks/9b1e6d2a-5c3f-4a8e-b7d1-0f2e4c6a8b9d \ -H "Authorization: Bearer sk_live_your_key"Poll every few seconds until status is completed, failed or cancelled. If you registered a webhook, seo.analysis.completed fires at the same moment.
3. Read the report
curl https://app.softsolz.uk/api/v1/services/seo/runs/3f2b9c44-8f4e-4d0a-9c1b-2a7d6e5f1a23 \ -H "Authorization: Bearer sk_live_your_key"The response holds the run, the site, the findings ordered by impact and the recommendations ordered by priority. Findings whose narrative is null still need an explanation; GET /runs/{run_id}/findings?only_missing=true lists just those.
{ "data": [ { "id": "a7c1e9d3-2b4f-4e6a-8c0d-1f3e5a7b9c2d", "detector": "striking_distance", "severity": "medium", "entity_type": "query", "query_text": "sourdough starter", "page_path": "/blog/sourdough-guide", "impact_score": 42.5, "evidence_json": { "current": { "clicks": 80, "impressions": 1900, "position": 11.2 }, "prior": { "clicks": 120, "impressions": 1850, "position": 8.9 } }, "narrative": null, "narrative_source": null} ]}4. Write the explanations
Two to four sentences per finding, at most 2000 characters, grounded in evidence_json. Ids that are not in this report, duplicates and blank text are skipped and listed in skipped.
curl -X PUT https://app.softsolz.uk/api/v1/services/seo/runs/3f2b9c44-8f4e-4d0a-9c1b-2a7d6e5f1a23/narratives \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "only_missing": true, "narratives": [ { "finding_id": "a7c1e9d3-2b4f-4e6a-8c0d-1f3e5a7b9c2d", "narrative": "Clicks fell from 120 to 80 because the page slipped from position 8.9 to 11.2, just off page one. Impressions held steady, so the demand is still there.", "confidence": 0.8 } ] }'{ "data": { "applied": 1, "skipped": [] }}5. Add a recommendation
Only a completed report accepts recommendations (409 run_not_completed), and every id in finding_ids must belong to it (400 finding_not_in_run). Members who manage recommendations are notified, and seo.recommendation.created fires.
curl -X POST https://app.softsolz.uk/api/v1/services/seo/runs/3f2b9c44-8f4e-4d0a-9c1b-2a7d6e5f1a23/recommendations \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "title": "Refresh the sourdough guide and add a starter recipe", "detail_markdown": "**Why:** the page lost 40 clicks a month as it slipped to position 11.2.\n\n**How:** add a step-by-step starter section near the top and update the date.", "estimated_effort": "medium", "opportunity_clicks": 180, "finding_ids": ["a7c1e9d3-2b4f-4e6a-8c0d-1f3e5a7b9c2d"] }'The recommendation appears in the report with play: "external_suggestion" and your name as author_label. Accepting, dismissing or reopening it is PATCH /recommendations/{recommendation_id}; turning it into a task stays in the dashboard, where a person approves it.
Keyword suggestions
Read the evidence first, then put the suggestions back. Send two to five, most valuable first; titles are cut at 90 characters and details at 420, and anything beyond five is dropped.
curl https://app.softsolz.uk/api/v1/services/seo/sites/SITE_ID/keywords/KEYWORD_ID/advice \ -H "Authorization: Bearer sk_live_your_key"{ "data": { "advice": null, "evidence": { "query_text": "sourdough starter", "period_start": "2026-08-01", "position": 11.2, "previous_position": 8.9, "best_position": 5.1, "months_seen": 9, "impressions": 1900, "clicks": 80, "ctr": 0.0421, "pages": [ { "url": "https://acmebakery.co.uk/blog/sourdough-guide", "clicks": 80, "impressions": 1900, "position": 11.2 } ] }}}curl -X PUT https://app.softsolz.uk/api/v1/services/seo/sites/SITE_ID/keywords/KEYWORD_ID/advice \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "suggestions": [ { "title": "Add a step-by-step starter recipe", "detail": "The page ranks 11th for a query with 1,900 monthly impressions; a recipe section answers the intent directly and is the shortest route back to page one.", "effort": "medium" }, { "title": "Answer the three common starter questions in the intro", "detail": "Impressions are flat while clicks fell, so the result is still shown but the snippet no longer matches what searchers want to know first.", "effort": "low" } ] }'The optional model is recorded as external:<model> so the workspace can see which model wrote the advice.
Code fixes that become a pull request
Connect a repository in the dashboard (it needs a GitHub sign-in), then audit it, read the failed checks, fetch the files from the repository yourself, rewrite only the wording and the head or metadata tags, and send the complete new content back. SoftSolz reads the original from the branch head, diffs it, runs the guard, and opens the pull request when you push.
1. Audit
curl -X POST https://app.softsolz.uk/api/v1/services/seo/sites/SITE_ID/code/repos/REPO_ID/audits \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{"branch":"main"}'Returns 202 with the audit. Poll GET /code/audits/{audit_id} until status is completed; the response lists failed checks first and ranks the audited pages by search traffic so fixes go where they count.
2. Create the fix set
One to fifty files per call, each with its full new content. Bodies are capped at 1 MB, so send big sets in batches: create with the first batch, then add more with POST /code/fixes/{fix_set_id}/files until the set is pushed.
curl -X POST https://app.softsolz.uk/api/v1/services/seo/sites/SITE_ID/code/repos/REPO_ID/fixes \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "base_branch": "main", "files": [ { "file_path": "src/pages/index.astro", "title": "Add a meta description", "reason": "The audit found no meta description on the home page.", "page_path": "/", "check_ids": ["description_present"], "after_content": "<html lang=\"en\"><head><title>Acme Bakery</title><meta name=\"description\" content=\"Sourdough, pastries and cakes baked every morning in Bradford.\">...</head>...</html>" }, { "file_path": "src/styles/global.css", "title": "Tidy the heading font", "after_content": "h1 { font-family: serif; }" } ] }'{ "data": { "set": { "id": "5d8f2a1c-7e3b-4c9a-a1f0-6b2d4e8c0a13", "status": "ready", "base_branch": "main", "source": "api", "author_label": "Claude", "credits_used": 0 }, "files": [ { "id": "c2e4a6b8-0d1f-4a3c-9e5b-7f9a1c3e5b7d", "file_path": "src/pages/index.astro", "change_kind": "update", "title": "Add a meta description", "included": true } ], "refused": [ { "file_path": "src/styles/global.css", "code": "fix_touches_design", "message": "\"src/styles/global.css\" controls how the site looks, so it is left alone." } ]}}The design guard
Every file passes the same guard the dashboard uses. Only wording and head or metadata tags may change; the layout fingerprint of an updated page must be identical to the original. Stylesheets, images, scripts, config files and framework settings are refused, and so is any change that removes content. Only robots.txt, sitemap.xml, llms.txt and security.txt may be created. A refused file never blocks the rest of the set: it comes back in refused with a code and a message you can act on, so fix it and send it again.
| Code | Why the file was refused |
|---|---|
fix_touches_design | A stylesheet, image or config file, or a change that alters the layout of the page. |
fix_adds_script | The new content adds a script. |
fix_adds_handler | The new content adds an event handler attribute. |
fix_removes_content | Wording that was on the page is gone from the new content. |
fix_create_not_allowed | A new file that is not one of the four permitted site files. |
fix_too_large | The file is bigger than the guard will read. |
You may also see fix_path_unsupported, fix_path_unsafe, fix_would_overwrite, fix_missing_original, fix_no_change and fix_duplicate_path, each with the same shape. A set where every file was refused has status: "empty".
3. Choose what goes in, then push
PATCH /code/fixes/{fix_set_id}/files/{file_id} with {"included": false} leaves a file out. Then push:
curl -X POST https://app.softsolz.uk/api/v1/services/seo/code/fixes/5d8f2a1c-7e3b-4c9a-a1f0-6b2d4e8c0a13/push \ -H "Authorization: Bearer sk_live_your_key"{ "data": { "fix_set": { "id": "5d8f2a1c-7e3b-4c9a-a1f0-6b2d4e8c0a13", "status": "pushed", "branch_name": "softsolz/seo-content-5d8f2a1c7e3b", "pull_request_url": "https://github.com/acme/bakery-site/pull/42", "pull_request_number": 42 }, "already_pushed": false}}A push never commits to the base branch. The included files land on a new softsolz/seo-content-* branch and a pull request is opened against the base branch, with your app credited in its first line. The team is notified and seo.fixes.pushed fires. Pushing a set that was already pushed returns the existing pull request with already_pushed: true; 400 nothing_selected means every file is excluded, and 409 fix_set_locked means you tried to add files to a set that is being pushed or already pushed. Merging is a human decision on GitHub.
Compute you can trigger
These endpoints do real work on SoftSolz and answer 202 Accepted with something to poll. None of them call a model.
| Endpoint | Does | Poll |
|---|---|---|
POST /sites/{site_id}/backfill | Imports up to 16 months of Search Console history | GET /tasks/{task_id} |
POST /sites/{site_id}/runs | Syncs, detects and ranks one month | GET /tasks/{task_id} |
POST /sites/{site_id}/links/verify | Re-opens every stale backlink | GET /tasks/{task_id} |
POST /sites/{site_id}/crawls | Crawls the site to map internal links, within the plan budget | GET /sites/{site_id}/crawls |
POST /sites/{site_id}/code/repos/{repo_id}/audits | Reads the branch and runs the checks | GET /code/audits/{audit_id} |
Single-page checks are synchronous instead: verifying one backlink or one prospect opens the page and answers in the same call, and POST /sites/{site_id}/mentions/find runs the web search and returns the counts straight away. POST /tasks/{task_id}/cancel stops a running task (409 cannot_cancel once it has finished).
What a key or an app can see
- A plain
sk_*key sees only websites and repositories shared with the whole workspace (visibility: "tenant"). Everything a website owns follows the website: its reports, findings, keywords and links. Everything a repository owns follows the repository. - A connected app sees exactly what the member who connected it sees, including records shared with that member by name or by role, and is limited to the scopes that member approved.
POST /sitesandPOST /sites/{site_id}/code/reposdefault toprivate, which a plain key cannot read back. Send"visibility": "tenant"when the API should keep seeing what it created.- A record outside your view answers
404, never403, so an id from another workspace reveals nothing.
Scopes
| Scope | Allows |
|---|---|
service.seo.view | Read websites, reports, findings, keywords, links and code audits. Every GET: sites, monthly reports with their findings and recommendations, tracked keywords and their evidence, backlinks, prospects, mentions, crawls, code audits and fix sets. |
service.seo.analysis.run | Start reports and write explanations. Start a monthly report or a history import, write finding narratives and keyword suggestions with your own model, cancel background tasks. Never spends SoftSolz AI credits. |
service.seo.recommendations.manage | Write and decide recommendations. Add recommendations to a completed report and accept or dismiss them. |
service.seo.export | Export reports and link files. Render a report as Markdown, download the backlinks CSV and the disavow file. |
service.seo.sites.manage | Add and edit websites and tracked keywords. Create and update websites, track and untrack keywords. |
service.seo.delete | Archive websites and delete reports. Archive a website or delete a monthly report. |
service.seo.links.manage | Manage backlinks, prospects and mentions. Record, import, verify and disavow backlinks, run prospects and mentions, change the link-checking settings. |
service.seo.crawl.run | Crawl the website. Start and cancel crawls that map internal links. |
service.seo.code.manage | Connect repositories and run code audits. Connect GitHub repositories, start audits and write explanations for code findings. |
service.seo.code.apply | Propose and push code fixes. Create fix sets from rewritten files, add files, choose what to include and open the pull request. |
A connected assistant only ever holds the scopes the member ticked on the consent screen, and a member can only grant scopes their own role already holds.
Events
| Event | When it fires |
|---|---|
seo.analysis.completed | A monthly report finished and its findings are ready to review. |
seo.analysis.failed | A monthly report could not be completed. |
seo.connection.reconnect_required | A Google connection stopped working and needs to be reconnected. |
seo.backlink.live | A link pointing at your website was found on the page that should carry it. |
seo.backlink.lost | A link that used to point at your website is no longer on the page. |
seo.crawl.completed | A crawl of your website finished and the internal link map is ready. |
seo.crawl.failed | A crawl of your website could not be completed. |
seo.prospect.link_live | A link you were chasing went live on the page you were promised. |
seo.recommendation.created | A connected app or API key added a recommendation to a completed report. |
seo.fixes.pushed | A set of content fixes was committed to a branch and opened as a pull request. |
seo.task.started | A background SEO task started. |
seo.task.completed | A background SEO task finished. |
seo.task.failed | A background SEO task could not be completed. |
seo.task.cancelled | A background SEO task was cancelled. |
{ "run_id": "3f2b9c44-8f4e-4d0a-9c1b-2a7d6e5f1a23", "site_id": "1ccbcc0f-8720-4d84-8053-18b198d0da36", "period_start": "2026-08-01", "finding_count": 14, "recommendation_count": 5}Failure events carry a stable error code, never a raw message, so you can branch on it safely. Every delivery is signed; follow Webhooks for the signature check before you trust a payload.
Sandbox
An sk_test_* key, or a connected app that chose the Sandbox workspace, acts on the sandbox twin: its own websites, reports, keywords and links, never the live ones. Everything above works there, which makes it the right place to develop a prompt or an integration. One thing to know: a repository connected in the sandbox is a real GitHub repository, so a push from the sandbox opens a real pull request on it. Point sandbox repositories at a fork or a test repository.
Next
Every endpoint, parameter, response shape and error code is generated into the API Reference, with samples in cURL, Node, Python and the SDKs. Connect an assistant in a minute with Connect an AI assistant, or read the product walkthrough on docs.softsolz.uk.