# Errors and rate limits

> The SEOFix API error envelope, every error code with its HTTP status, the rate limits per key and endpoint, insufficient credits, and when to retry.

Source: https://seofix.ai/help/errors-and-rate-limits · Category: REST API · Updated: 2026-10-08

Every SEOFix API error has the same shape: `{"error": {"code": "...", "message": "..."}}` with a matching HTTP status. Branch on `code`, not on `message`. Each API key may make 60 requests per minute; some endpoints have an extra, lower limit. Retry only `429`, `503` and `report_not_ready`, and honour `Retry-After`.

## The error envelope

```json
{
  "error": {
    "code": "site_not_verified",
    "message": "Rechecks need a verified site. Verify ownership first: see GET /v1/sites/{id}/verification."
  }
}
```

- `code` is stable and machine-readable. `message` is for people and can change.
- Validation errors add `fields`, the failing parameters with their messages:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The max rps field must not be greater than 10.",
    "fields": {"max_rps": ["The max rps field must not be greater than 10."]}
  }
}
```

- `budget_exhausted` adds `resets_at` (ISO 8601).
- Unexpected server errors answer `500 internal_error` and never include internal details.

Over MCP, a failed tool returns the same code as text: `code: message`, with `isError: true`.

## Error codes

### Any endpoint

| Status | Code | Meaning | What to do |
| --- | --- | --- | --- |
| 401 | `unauthenticated` | No key, a wrong key, or a revoked or rotated key | Send `Authorization: Bearer sk_...` with a current key |
| 403 | `team_access_revoked` | The key holder is no longer a member of the key's team | Issue a new key for a team you belong to |
| 404 | `not_found` | Unknown id, unknown path, or a resource of another team | Check the id and the key's team |
| 422 | `validation_failed` | A parameter is missing or out of range; see `fields` | Fix the request |
| 429 | `rate_limited` | A rate limit was hit | Wait for `Retry-After` seconds |
| 500 | `internal_error` | Unexpected server error | Retry later with backoff; email hello@seofix.ai if it persists |

### Credits and plans

| Status | Code | Meaning |
| --- | --- | --- |
| 402 | `insufficient_credits` | The balance is below what the request reserves (the page cap of an audit, or 1 per URL of a recheck) |
| 403 | `plan_limit_max_pages` | `max_pages` is above the plan's per-audit page limit (a quarter of the monthly page tier: at least 10,000 on Starter and 250,000 on Growth, at most 500,000). The message states your limit. |
| 403 | `plan_limit_sites` | The plan's site limit is reached |
| 403 | `plan_limit_monitoring` | The plan does not include scheduled monitoring |

### Audits

| Status | Code | Meaning |
| --- | --- | --- |
| 422 | `invalid_url` | The URL is not a public http(s) address |
| 422 | `max_rps_requires_verified_site` | `max_rps` above 3 without a verified site for that origin |
| 422 | `site_url_mismatch` | With `site_id`, the `url` is not on the site's origin |
| 409 | `site_not_verified` | More than 500 pages, rechecks, monitoring, Google data or IndexNow need a verified site |
| 404 | `report_not_ready` | The report, diff, templates or fix prompt does not exist yet |
| 410 | `crawl_archived` | Issue and page detail of this audit were archived; report, diff and fix prompt remain |
| 409 | `conflict` | Cancel on an audit that already ended |
| 503 | `dispatch_failed` | The audit could not be queued; its reserved credits are refunded automatically |

### Sites

| Status | Code | Meaning |
| --- | --- | --- |
| 409 | `site_exists` | The team already has a site for that host |
| 422 | `verification_failed` | No ownership proof matched; the message lists what to publish |

### Fix tasks and rechecks

| Status | Code | Meaning |
| --- | --- | --- |
| 404 | `not_found` | No fix task with this key in the site's latest audit |
| 422 | `cannot_verify` | The task's check cannot be judged by a recheck (site-wide, link-graph, sitemap or Google-indexing checks); nothing was started. Run a full audit instead. |
| 422 | `invalid_urls` | A URL is not on the site's host, or there are not 1-200 URLs |
| 429 | `recheck_rate_limited` | More than 30 rechecks this hour for the team; see `Retry-After` |

### Google Search Console

| Status | Code | Meaning |
| --- | --- | --- |
| 409 | `gsc_not_connected` | No active Search Console connection for the site |
| 409 | `gsc_forbidden` | The connected Google account lost access to the property |
| 422 | `url_not_in_site` | A URL is not part of the site's Search Console property |
| 422 | `gsc_request_rejected` | Search Console rejected the request |
| 429 | `budget_exhausted` | Today's URL Inspection budget for the property is used up; see `resets_at` |
| 503 | `google_unavailable` | Search Console is temporarily unavailable |

### IndexNow

| Status | Code | Meaning |
| --- | --- | --- |
| 409 | `indexnow_key_unverified` | The key file has not been verified yet; serve it, then check it |
| 422 | `url_not_in_site` | None of the URLs is on the site's host |
| 422 | `invalid_indexnow_key` | The key is not 8-128 characters of `A-Z a-z 0-9 -` |
| 422 | `invalid_indexnow_key_location` | `key_location` is not an https `.txt` URL on the site's host |
| 429 | `indexnow_cap` | The site's 10,000 IndexNow URLs for today (UTC) are used up |
| 502 | `indexnow_rejected` | IndexNow accepted none of the URLs, or could not be reached |

### Billing

| Status | Code | Meaning |
| --- | --- | --- |
| 403 | `not_team_owner` | Only the team owner can buy credits or plans |
| 409 | `subscription_exists` | The account already has a subscription; change it in Settings → Plan & billing |
| 409 | `no_billing_account` | Nothing bought yet, so there is no billing portal to open |
| 422 | `price_not_configured` | That plan, volume and interval is not offered |
| 503 | `billing_not_configured` | Billing is not available on this server |

### Device login (`npx seofix connect`)

| Status | Code | Meaning |
| --- | --- | --- |
| 428 | `authorization_pending` | Not approved yet; keep polling |
| 429 | `slow_down` | Polled faster than every 5 seconds |
| 403 | `access_denied` | The request was denied |
| 400 | `expired_token` | The code expired (after 10 minutes) |
| 400 | `invalid_grant` | Unknown or already used device code |

### MCP-only codes

The MCP server adds three codes of its own:

| Code | Meaning |
| --- | --- |
| `unauthenticated` (`No API key provided.`) | The MCP request had no `Authorization` header; nothing was sent to the API |
| `network_error` | The MCP server could not reach the API |
| `http_<status>` | The API answered an error without the usual envelope |

## Rate limits

| Scope | Limit | Keyed by |
| --- | --- | --- |
| Every authenticated `/v1` endpoint, combined | 60 requests per minute | API key |
| `POST /v1/sites/{id}/verify` | 10 per minute | API key |
| `POST /v1/sites/{id}/google/inspect` | 10 per minute | API key |
| `POST /v1/sites/{id}/indexnow` | 10 per minute | API key |
| `POST /v1/sites/{id}/indexnow/check-key` | 10 per minute | API key |
| `PUT /v1/sites/{id}/indexnow/key` | 10 per minute | API key |
| `POST /v1/sites/{id}/recheck` | 30 rechecks per hour | team |
| IndexNow submissions | 10,000 URLs per day (UTC) | site |
| Google URL Inspection | the property's daily budget, shared with the daily sync | Search Console property |
| `POST /v1/device/start` | 10 per hour | IP address |
| `POST /v1/device/token` | 60 per minute, and one poll per 5 seconds per device code | IP address |
| `GET /v1/health` | none | |

The per-endpoint limits apply in addition to the 60 per minute. All keys of one user count separately; the recheck limit is shared by everyone in the team.

Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`. A `429 rate_limited` answer also carries `Retry-After` (seconds). `recheck_rate_limited` and `budget_exhausted` carry `Retry-After` too.

## Insufficient credits

Audits and rechecks reserve credits before they start:

- An audit reserves its full page cap (`max_pages`). The unused part is refunded when it ends.
- A recheck reserves 1 credit per URL.

If the balance that pays for it is lower, the API answers `402 insufficient_credits` and creates nothing: no audit, no reservation. A recheck refused this way does not count against the hourly recheck limit.

Which balance pays: a plain-URL audit (`POST /v1/crawls` with `url`) uses the key holder's own balance; audits of a registered site, scheduled audits and rechecks use the team owner's balance. `GET /v1/account` shows both (`balance` and `team_credits`).

To proceed: lower `max_pages` to fit the balance, or add credits (Settings → Plan & billing in the app, or a checkout link from `POST /v1/billing/checkout` for the team owner). Do not retry the same request unchanged.

## Retry guidance

| Answer | Retry? |
| --- | --- |
| `429 rate_limited`, `recheck_rate_limited` | Yes, after `Retry-After` seconds |
| `429 budget_exhausted` | Yes, after `resets_at` |
| `429 indexnow_cap` | Yes, after midnight UTC |
| `404 report_not_ready` | Yes, every 5-10 seconds while the audit is `finalizing` or just `done` |
| `500 internal_error`, `502 indexnow_rejected`, `503` codes | Yes, with exponential backoff (for example 5 s, 15 s, 60 s), a few times |
| `503 dispatch_failed` | Yes, once after a minute; the failed audit's credits are refunded |
| `428 authorization_pending` | Yes, every 5 seconds until approved or expired |
| `400`, `401`, `402`, `403`, other `404`, `409`, `410`, `422` | No: fix the request, the key, the plan or the site first |

Never retry `POST /v1/crawls` or `POST /v1/sites/{id}/recheck` after a timeout without checking first: the audit or recheck may have started. List audits with `GET /v1/crawls?site_id=` before starting another. Over MCP, `verify_fix` never starts a second recheck on its own; continue with `get_recheck`.

## Related

- [API reference](https://seofix.ai/help/api-reference.md)
- [REST API quickstart](https://seofix.ai/help/api-quickstart.md)
- [Agent playbook](https://seofix.ai/help/agent-playbook.md)
- [Plans and credits](https://seofix.ai/help/plans-and-credits.md)
