# API reference

> Every public SEOFix /v1 endpoint - method, path, parameters, response fields, credit cost and errors - grouped by resource.

Source: https://seofix.ai/help/api-reference · Category: REST API · Updated: 2026-10-08

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](https://seofix.ai/help/errors-and-rate-limits.md).

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_failed` with a `fields` object.
- Only audits and rechecks cost credits.
- Paginated lists return `{data: [...], meta: {current_page, per_page, total, last_page}}` and take `page` and `per_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

```bash
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](https://seofix.ai/help/webhooks.md) |
| `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 |

```bash
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}'
```

```json
{"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_limited` with `Retry-After`. A request refused with `402 insufficient_credits` does not use a slot.
- `422 cannot_verify` for a task whose check a recheck cannot judge (link-graph, sitemap, site-wide and Google-indexing checks); nothing is started. `422 invalid_urls` for URLs off the site's host or more than 200. `404 not_found` for 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](https://seofix.ai/help/billing-and-subscriptions.md#changing-plan-or-tier)).

### 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](https://seofix.ai/help/connect-your-agent.md).

### 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

- [REST API quickstart](https://seofix.ai/help/api-quickstart.md)
- [Errors and rate limits](https://seofix.ai/help/errors-and-rate-limits.md)
- [Webhooks](https://seofix.ai/help/webhooks.md)
- [MCP tools reference](https://seofix.ai/help/mcp-tools-reference.md)
