API reference
Every public SEOFix /v1 endpoint - method, path, parameters, response fields, credit cost and errors - grouped by resource.
Base URL: https://api.seofix.ai/v1. Every endpoint except /health and /device/* needs Authorization: Bearer sk_.... Requests and responses are JSON. Errors use one envelope, {"error": {"code", "message"}}; see Errors and rate limits.
Common rules:
- A key acts for one team: the team it was created for. Sites, audits, tasks and rechecks of other teams answer
404 not_found, never 403. - Every authenticated endpoint shares a limit of 60 requests per minute per key. Some have an extra limit, shown below.
- Invalid parameters answer
422 validation_failedwith afieldsobject. - Only audits and rechecks cost credits.
- Paginated lists return
{data: [...], meta: {current_page, per_page, total, last_page}}and takepageandper_page.
Overview
| Method | Path | Purpose | Credits |
|---|---|---|---|
| GET | /health |
Liveness check, no auth | |
| GET | /ping |
Check your key | |
| GET | /account |
Credit balances and usage | |
| GET | /crawls |
List audits | |
| POST | /crawls |
Start an audit | 1 per page |
| GET | /crawls/{id} |
Audit status | |
| DELETE | /crawls/{id} |
Cancel an audit | |
| GET | /crawls/{id}/report |
Full report | |
| GET | /crawls/{id}/issues |
Issues, paginated | |
| GET | /crawls/{id}/pages |
Crawled pages, paginated | |
| GET | /crawls/{id}/templates |
Issues by URL template | |
| GET | /crawls/{id}/diff |
Changes vs previous audit | |
| GET | /crawls/{id}/fix-prompt |
Markdown fix prompt | |
| GET | /sites |
List sites | |
| POST | /sites |
Register a site | |
| GET | /sites/{id} |
One site | |
| PATCH | /sites/{id} |
Update settings and monitoring | |
| DELETE | /sites/{id} |
Delete a site | |
| GET | /sites/{id}/verification |
Ownership methods | |
| POST | /sites/{id}/verify |
Check ownership | |
| POST | /sites/{id}/crawls |
Audit a registered site | 1 per page |
| GET | /tasks |
Team's top fix tasks | |
| GET | /sites/{id}/tasks |
A site's fix tasks | |
| GET | /sites/{id}/tasks/{key} |
One fix task with its URLs | |
| POST | /sites/{id}/recheck |
Recheck a task or URLs | 1 per URL |
| GET | /rechecks/{id} |
Recheck results | |
| GET | /fix-impact |
Team's fix impact | |
| GET | /sites/{id}/fix-impact |
A site's fix impact | |
| GET | /sites/{id}/index-triage |
Not-indexed triage | |
| GET | /sites/{id}/google |
Google indexing summary | |
| GET | /sites/{id}/google/not-indexed |
Not-indexed groups and URLs | |
| GET | /sites/{id}/google/url |
One URL's status | |
| GET | /sites/{id}/google/opportunities |
Search opportunities | |
| POST | /sites/{id}/google/inspect |
Fresh Google inspection | |
| GET | /sites/{id}/indexnow |
IndexNow status | |
| PATCH | /sites/{id}/indexnow |
Auto-submit on or off | |
| POST | /sites/{id}/indexnow/check-key |
Check the key file | |
| POST | /sites/{id}/indexnow/detect-key |
Look for an existing key | |
| PUT | /sites/{id}/indexnow/key |
Use an existing key | |
| POST | /sites/{id}/indexnow |
Submit URLs | |
| POST | /billing/checkout |
Checkout link for a credit pack | |
| POST | /billing/subscribe |
Checkout link for a plan | |
| POST | /billing/portal |
Stripe billing portal link (invoices, card, cancel) | |
| POST | /device/start |
Start a device login (CLI) | |
| POST | /device/token |
Poll a device login (CLI) |
Health and account
GET /health
No authentication, no rate limit. Returns {"ok": true}.
GET /ping
Returns {"user_id": <id>} for a valid key.
GET /account
curl -s https://api.seofix.ai/v1/account -H "Authorization: Bearer $SEOFIX_API_KEY"
| Field | Meaning |
|---|---|
balance |
your own credits; plain-URL audits (POST /crawls with url) are charged here |
team_id |
the team the key acts for |
team_credits |
the team owner's credits; site audits, scheduled audits and rechecks are charged here |
crawls_total |
audits you started |
credits_spent_30d |
your own spend over the last 30 days |
team_credits_spent_30d |
what this team's audits cost its owner over the last 30 days |
api_key_prefix |
the first 12 characters of the key used |
Audits (crawls)
POST /crawls
Start an audit of any URL, or of a registered site with site_id. Answers 202.
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
url |
string | unless site_id |
http or https, public address | |
site_id |
integer | no | a site of the key's team; uses the site's settings. With url, the URL must be on the site's origin. Cannot be combined with verify_token. |
|
max_pages |
integer | no | 10,000, capped by the plan; 500 without a verified site for the origin | 1-500,000, at most the plan's per-audit limit |
concurrency |
integer | no | 2 | 1-20 |
max_rps |
number | no | 2 | 0.5-10 requests per second per host; above 3 needs a verified site for the same origin |
webhook_url |
string | no | notified when the audit ends; see Webhooks | |
verify_token |
string | no | 16-128 characters of A-Z a-z 0-9 _ -; sent as a header so a firewall can allowlist the crawler; never stored or echoed |
curl -s -X POST https://api.seofix.ai/v1/crawls \
-H "Authorization: Bearer $SEOFIX_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com", "max_pages": 500}'
{"crawl_id": 4821, "estimated_credits": 500, "max_rps": 2}
Credits: estimated_credits (the page cap) is reserved at once. The audit costs 1 credit per page crawled and 0.2 per page that answered 304 Not Modified since the site's previous audit, rounded up once per audit; the rest is refunded when it ends. A plain-URL audit is charged to the key holder; a registered site's audit to the team owner.
Errors: 402 insufficient_credits, 403 plan_limit_max_pages, 409 site_not_verified (more than 500 pages without a verified site for the origin), 422 max_rps_requires_verified_site, 422 invalid_url, 422 site_url_mismatch, 503 dispatch_failed.
GET /crawls
The team's audits, newest first (plus your own audits from before teams). Rechecks are not listed.
| Query | Type | Default | Rules |
|---|---|---|---|
site_id |
integer | one site of the team (404 for another team's site) | |
page |
integer | 1 | |
per_page |
integer | 20 | 1-100 |
Each row: crawl_id, site_id, site_url, status, trigger, health_score (null until a report exists), pages_crawled, max_pages, created_at, finished_at, archived_at.
GET /crawls/{id}
| Field | Meaning |
|---|---|
crawl_id, site_id, site_url, trigger |
identity; trigger is for example api, ci, manual, schedule, free or preview |
status |
queued, running, finalizing, done, failed or cancelled |
pages_crawled, max_pages, credits_reserved |
progress and reservation |
created_at, started_at, finished_at |
ISO 8601 times |
archived_at |
set once issue and page detail were archived; report, diff and fix prompt remain |
failure_reason |
null, cancelled, dispatch_failed: ..., reaped: ..., setup_failed: <type> or internal_error |
verify_token_set |
whether a firewall token was sent |
max_rps, duration_s, pages_per_minute |
speed and timing |
DELETE /crawls/{id}
Cancel an audit that is queued, running or finalizing. Returns {"status": "cancelled"}. An audit that already ended answers 409 conflict.
GET /crawls/{id}/report
The full report. 404 report_not_ready until it exists (it can land a few seconds after done).
Key fields: crawl_id, health_score (0-100), partial (stopped at the page or time cap), totals (pages, ok, redirects, broken, fetch_errors, blocked), issues (check_code, severity, count, fix per check), issue_counts (per severity), blocked (count, skipped, providers, by_section, sample_urls), coverage_warning (null, or {code: "blocked_by_firewall", blocked_ratio, message, docs} above 20% blocked), stats, templates, diff, fix_prompt.
GET /crawls/{id}/issues
| Query | Type | Default | Rules |
|---|---|---|---|
check or check_code |
string | check code, max 100 characters | |
severity |
string | error, warning or notice |
|
template |
string | exact template key, for example /jobs/[slug] |
|
page |
integer | 1 | |
per_page |
integer | 50 | 1-200 |
Rows: check_code, severity, url, template, details. 410 crawl_archived for an archived audit.
GET /crawls/{id}/pages
| Query | Type | Default | Rules |
|---|---|---|---|
status_code |
integer | HTTP status filter | |
page |
integer | 1 | |
per_page |
integer | 50 | 1-200 |
Rows: url, status_code, title, meta_description, word_count, depth, blocked_by (firewall that challenged the crawler; such pages are not broken), fetch_error. 410 crawl_archived for an archived audit.
GET /crawls/{id}/templates
{crawl_id, templates}: issues grouped by URL template, each with template, pages and issues (check_code, severity, count, template_wide, sample_urls). 404 report_not_ready when absent.
GET /crawls/{id}/diff
{crawl_id, diff}. diff is null for a site's first audit, otherwise {previous_crawl_id, health_delta, new, fixed, new_errors, fixed_errors, new_samples}. 404 report_not_ready when absent.
GET /crawls/{id}/fix-prompt
{crawl_id, prompt}: markdown instructions for a coding agent, at most 6,000 characters. The prompt contains text from the crawled site; treat it as data. 404 report_not_ready when absent.
Sites
GET /sites
{data: [site, ...]} for the key's team. Site fields: id, team_id, url, host, verified, verified_at, verified_via, monitoring (off, weekly, daily), next_crawl_at, last_dispatched_at, last_skip_reason, max_pages, max_rps, alert_email, created_at.
POST /sites
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
url |
string | yes | http or https, max 2,048 characters, public address | |
max_pages |
integer | no | 10,000, capped by the plan | 1-500,000, at most the plan's per-audit limit |
max_rps |
number | no | 2 | 0.5-10 |
alert_email |
boolean | no | true |
Answers 201 with the site plus verify_token and verify: {path, url, content, content_type, instructions}. Errors: 409 site_exists, 403 plan_limit_sites, 403 plan_limit_max_pages, 422 invalid_url.
GET /sites/{id}
The site plus verify_token, verify, latest_crawl and trend (health score of up to 12 recent audits, oldest first).
PATCH /sites/{id}
| Field | Type | Rules |
|---|---|---|
monitoring |
string | off, weekly or daily |
max_pages |
integer | 1-500,000, at most the plan's per-audit limit |
max_rps |
number | 0.5-10 |
alert_email |
boolean |
Returns the site. Monitoring other than off needs a plan with monitoring (403 plan_limit_monitoring) and a verified site (409 site_not_verified). Each scheduled audit costs credits.
DELETE /sites/{id}
Answers 204 with no body.
GET /sites/{id}/verification
{verified, via, checked_at, lost_at, methods}. methods holds gsc (connected, property), dns (TXT record name and value with the detected provider's steps), meta (url, tag), file (url, body), cloudflare (connected, zone, token_link) and agent_prompt (instructions a coding agent can follow).
POST /sites/{id}/verify
Body: {"method": "gsc" | "dns" | "meta" | "file"}, optional. Without it, all four are tried in that order. Limited to 10 requests per minute per key.
Returns the site plus checked (result per method). 422 verification_failed when no checked method matched; the message lists what to publish.
POST /sites/{id}/crawls
Audit a registered site with its saved settings. Answers 202 with {crawl_id, estimated_credits, max_rps}.
| Field | Type | Rules |
|---|---|---|
max_pages |
integer | 1-500,000; above the plan's per-audit limit: 403 plan_limit_max_pages; above 500 on an unverified site: 409 site_not_verified |
max_rps |
number | 0.5-10, capped at the site's max_rps (and at 3 when unverified) |
trigger |
string | api (default) or ci, to label CI runs |
Charged to the team owner. On a verified site the crawler sends the site's crawler secret as the X-SEOFix-Verify header. Errors as for POST /crawls.
Fix tasks
A fix task is one check code on one URL template from the site's latest full audit. Its id is 16 hex characters.
GET /sites/{id}/tasks
| Query | Type | Default | Rules |
|---|---|---|---|
limit |
integer | 10 | 1-100 |
Returns {site_id, crawl_id, has_traffic_data, total, open_total, tasks}. Each task: id, code, template, severity, pages, clicks_28d, impressions_28d (null without Search Console data), impact, title, why, fix, sample_urls, acceptance (text, check, scope, template), recheckable, status (open, verifying, fixed, regressed). A limit of 3 or less leaves out fixed tasks and keeps at most one task per check code.
GET /tasks
The team's top tasks across its sites, never fixed ones. limit: 1-20, default 3. Returns {tasks}, each with site_id and site_host.
GET /sites/{id}/tasks/{key}
| Query | Type | Default | Rules |
|---|---|---|---|
page |
integer | 1 | 1-100,000 |
Returns {task, urls: {data: [{url, clicks_28d, impressions_28d, details}], page, per_page: 100, total}}. 404 not_found for an unknown key.
Rechecks
POST /sites/{id}/recheck
Re-fetch a task's URLs (or given URLs) to verify a fix. Answers 202.
| Field | Type | Rules |
|---|---|---|
task_key |
string | 16 hex characters; above 200 affected URLs, 200 are sampled (most clicks first) |
urls |
string[] | instead of task_key: 1-200 URLs on the site's host |
Give exactly one. Returns {recheck_id, status, urls_total, estimated_credits, sampled, tasks}.
- Credits: 1 per URL fetched, reserved per URL and charged to the team owner.
- Verified sites only (
409 site_not_verified). - 30 rechecks per hour per team:
429 recheck_rate_limitedwithRetry-After. A request refused with402 insufficient_creditsdoes not use a slot. 422 cannot_verifyfor a task whose check a recheck cannot judge (link-graph, sitemap, site-wide and Google-indexing checks); nothing is started.422 invalid_urlsfor URLs off the site's host or more than 200.404 not_foundfor an unknown task.
GET /rechecks/{id}
{recheck_id, site_id, status, parent_crawl_id, urls_total, pages_crawled, sampled, created_at, finished_at, time_capped, duplicate_baseline, urls, tasks}.
urls[]:url,status_code,result(fixed,still_failing,new_issue,not_checked,cannot_verify,passing),reason,redirected,redirect_to,issues,new_issues.tasks[]:task_key,code,template,urls_rechecked,pages,sampled,covers_task,result(fixed,still_failing,not_checked,cannot_verify),urls_failing,urls_passing,status(the task's state after the recheck).
Results are filled once status is done, failed or cancelled. Only a fixed result marks the task fixed.
Fix impact
GET /sites/{id}/fix-impact
limit: 1-200, default 50. Returns {site_id, total_delta_28d, measured_events, events, note, methodology}. Each event: id, task_key, code, template, recheck_id, urls_count, sampled, verified_at, baseline_clicks_28d, baseline_impressions_28d, d7, d7_at, d14, d14_at, d28, d28_at, d28_impressions, delta_28d, measured_at, status (measuring, measured, no_data). Figures are estimates, not seasonally adjusted.
GET /fix-impact
The team's total and latest events across its sites. limit: 1-50, default 10. Events also carry site_id and site_host.
Google Search Console
All Google endpoints need a verified site (409 site_not_verified). Every response has demo (true for locally generated demo data, not Google's).
GET /sites/{id}/google
Headline, indexing funnel (found, in_sitemap, indexed, with_impressions, with_clicks, indexed_outside_sitemap), trend, per-template table and sync status.
GET /sites/{id}/google/not-indexed
| Query | Type | Rules |
|---|---|---|
reason |
string | Google's coverage state, verbatim, max 500 characters |
template |
string | template key, max 500 characters |
page |
integer | 1-100,000 |
Without filters: groups (reason, template, pages, diagnosis, fix, sample_urls). With both reason and template: the URLs of that group.
GET /sites/{id}/google/url
Query url (required, max 2,048 characters, a URL of this site). Returns {url, crawl, google, search}. 422 url_not_in_site for other URLs.
GET /sites/{id}/google/opportunities
Pages with low click-through rate (low_ctr), striking-distance positions (striking_distance) and canonical mismatches (canonical_mismatch), plus counts.
POST /sites/{id}/google/inspect
Body: {"urls": [...]}, 1-20 URLs of this site. Limited to 10 requests per minute per key. Returns {results, budget: {used, limit, resets_at}, note}. Uses the property's daily URL Inspection budget, shared with the daily sync: when spent, 429 budget_exhausted with resets_at in the error body. 409 gsc_not_connected without a Search Console connection, 422 url_not_in_site for other URLs.
GET /sites/{id}/index-triage
Crawled pages Google reports as not indexed, grouped by URL pattern and value class (valuable_not_indexed, low_value_param, low_value_thin, duplicate, intentionally_excluded). Returns demo, summary, groups and total_groups. Each group has count, pattern_pages, coverage_states, impressions_28d, sample_urls, why and action (type, steps, text, task_code, task_id).
IndexNow
IndexNow notifies Bing, Yandex, Seznam, Naver and Yep, not Google. Status and key checks work on any site of the team; submitting needs a verified site and a verified key.
GET /sites/{id}/indexnow
key, key_source (generated or existing), key_location, key_file_url, key_file_content, key_verified, key_verified_at, auto, last_submit_at, today {used, limit}, recent, agent_prompt, detection, engines, google_supported, note.
detection is the last automatic key detection, or null if none ran: {status, key, key_file_url, source, platform, checked_at}. status is pending, found, none or managed.
PATCH /sites/{id}/indexnow
Body: {"auto": true | false} (required). Turns auto-submit of new and changed pages after each audit on or off. Returns the status.
POST /sites/{id}/indexnow/check-key
Fetches the key file now. Returns result, reason and the status. Limited to 10 requests per minute per key.
POST /sites/{id}/indexnow/detect-key
Looks for an IndexNow key the site already serves, in at most 10 requests: the site's own key, keys verified on the team's other sites (verified sites only), and root key files seen in the latest audit. Each key is confirmed by fetching https://<host>/<key>.txt. Returns the status with the new detection. A found key is only a suggestion: use it with PUT /sites/{id}/indexnow/key. Limited to 10 requests per minute per key.
PUT /sites/{id}/indexnow/key
Body: {"key": "...", "key_location": "..."}. key is 8-128 characters of A-Z a-z 0-9 -; key_location only when the file is not at https://<host>/<key>.txt (an https URL on the site's host ending in .txt). Verified sites only. Limited to 10 requests per minute per key. Returns the check result and the status. Errors: 422 invalid_indexnow_key, 422 invalid_indexnow_key_location, 409 site_not_verified.
POST /sites/{id}/indexnow
Body: {"urls": [...]}, 1-10,000 URLs, required. URLs on other hosts are skipped. Limited to 10 requests per minute per key.
Returns {submitted, requests, status, status_code, error_code, skipped, capped, batches, reason, today}. status is ok, accepted or partial (some requests failed; see batches).
Errors: 409 site_not_verified, 409 indexnow_key_unverified, 422 url_not_in_site (no URL on the host), 429 indexnow_cap (10,000 URLs per site per UTC day used up), 502 indexnow_rejected (IndexNow accepted none of them).
Billing
Only the team owner can use these (403 not_team_owner). Each returns {"url": "..."}: a Stripe link to open in a browser. Nothing is charged until checkout is completed there.
POST /billing/checkout
Body: {"pack": "small" | "large"}. A one-time credit pack: small is 5,000 credits for $9, large 25,000 for $29.
POST /billing/subscribe
Body: {"plan": "starter" | "growth", "tier": "10k" | "50k" | "150k" | "500k" | "1m" | "2.5m", "interval": "monthly" | "yearly"}. 422 price_not_configured when that combination is not offered. 409 subscription_exists when the account already has a subscription: each account has one, and plan or tier changes are made in Settings → Plan & billing (see Billing and subscriptions).
POST /billing/portal
No body. A Stripe customer portal link where the owner downloads invoices, changes the payment method and cancels. 409 no_billing_account before the first purchase.
Device login (CLI)
Used by npx seofix connect; no API key needed. See Connect your agent.
POST /device/start
Body: {"client_name": "..."}, optional, max 64 characters. Returns {device_code, user_code, verification_uri, verification_uri_complete, interval: 5, expires_in: 600}. Limited to 10 requests per hour per IP address.
POST /device/token
Body: {"device_code": "..."}. Poll at most every 5 seconds. Limited to 60 requests per minute per IP address.
| Answer | Meaning |
|---|---|
200 {api_key, team: {id, name}} |
approved; the key is returned exactly once |
428 authorization_pending |
not approved yet |
429 slow_down |
polled faster than every 5 seconds |
403 access_denied |
denied, or the approver is no longer in the team |
400 expired_token |
the code expired |
400 invalid_grant |
unknown or already used device code |
Related
More in REST API
Still stuck? Email [email protected] with your site and what you expected to see.