Skip to content

API errors /v1

Every error response from /v1/* has the same shape (api.contracts.ErrorResponse, #46, ADR 0015):

{"error": {"code": "unknown_rule", "message": "Unknown rules: NOPE",
           "details": {"rules": ["NOPE"]}, "request_id": "6cb511c42fb241ec9e04334766829427"}}
  • code — stable machine identifier (below). Clients branch on code, never on message.
  • message — human-readable description, max 300 characters; may change without a version bump.
  • details — optional structured data (e.g. a list of validation errors), null if none.
  • request_id — same as the X-Request-ID header; quote it when reporting a problem.

Every /v1 response (including success and 500) has X-Request-ID and X-API-Version: v1 headers. A client may send its own X-Request-ID ([A-Za-z0-9._-]{1,128}) — it is used instead of a generated one.

Error codes

HTTP code When
400 unknown_rule rules contains ids outside the catalog; details.rules = unknown ids
400 unknown_profile unknown profile (superadmin override only; other callers' profile is ignored, #314)
400 rule_disabled rules contains ids disabled in the catalog; details.rules = disabled ids
400 invalid_webhook_url webhook_url not https, an IP literal, localhost/internal host, or has credentials (#51)
400 bad_request other request error
400 invalid_url page url rejected before analysis (#111): IP literal, localhost, .local/.internal, credentials, scheme other than http(s), port other than 80/443; details = {url}
401 unauthorized not logged in (JWT Authorization: Bearer, API key Bearer gsj_... or session cookie, #48/#50)
400 invalid_key_expiry expires_at in the past or more than 1 year ahead, or never_expires on a non-organization key / by a non-owner/admin (#125)
401 invalid_api_key gsj_... key unknown, wrong secret or checksum, revoked (also after the rotation grace period or a leak report), its project deleted, or a user key whose owner left the organization (#50, #125)
401 api_key_expired the key passed its expires_at; create or rotate a key (#125)
403 csrf_failed session-cookie request without a matching Origin/Referer
403 forbidden role too low (require_role), no permission in the project, or user-only operation (e.g. managing keys)
403 ip_not_allowed the key has an IP allowlist and the client address is outside it (#125)
403 insufficient_scope API key lacks the required scope (judge, reports:read, jobs, projects:read, #50)
403 org_suspended the current organization (from X-Org-ID, the only org of the account, or the API key's project) is suspended (#124)
404 report_not_found GET /v1/reports/{id} — no such report; also any id that is not a public id (rep_…), e.g. an old integer id (#142)
404 job_not_found GET /v1/jobs/{id} — no such job in the caller's projects (#51)
404 audit_not_found GET /v1/audits/{id} — no such audit in the caller's projects (#87)
404 intent_not_found GET /v1/intent/{id} and exports - no such intent report or batch in the caller's projects (#194)
422 tier_unavailable POST /v1/intent* - the requested Intent tier is not available yet (#194)
422 invalid_csv POST /v1/intent/batch/csv - the CSV has no phrases or a row has a bad market (#194)
403 private_project POST /v1/intent/{id}/shares - intent reports in a private project cannot be shared publicly (#194)
503 pdf_unavailable GET /v1/intent/{id}/export.pdf - PDF rendering is unavailable on this server (#194)
404 share_not_found DELETE /v1/share-links/{id} (and per report/audit) — no active share link with this id in the caller's organization (#126)
404 org_not_found X-Org-ID names an unknown organization, one without the caller's membership, or a different org than the API key's project (#124)
409 upgrade_unavailable POST /v1/reports/{id}/upgrade, /v1/audits/{id}/upgrade (#192): nothing to add; details.reason = nothing_to_add | busy | private_mode | no_pages, details.estimate
413 audit_too_large POST /v1/audits/site — pages over GSPAMJUDGE_AUDITS_MAX_PAGES (#87), or over GSPAMJUDGE_BETA_AUDIT_MAX_PAGES (200) for beta-plan orgs (#220; API and panel)
422 sitemap_not_found POST /v1/audits/site — no sitemap URLs for the site (#87)
422 invalid_scope /v1/audits/site* — invalid include/exclude rule (globs only) (#119)
422 empty_selection POST /v1/audits/site — no URLs left after include/exclude (#119)
422 preview_expired /v1/audits/site* — preview_id expired, unknown or from another user/project, and no source given (#119)
404 not_found path does not exist
405 method_not_allowed wrong HTTP method
409 key_not_active POST /v1/keys/{id}/rotate on a revoked, expired or already rotated key (#125)
409 conflict resource already exists, e.g. POST /v1/invites for an e-mail with an account or a pending invite (#89)
413 batch_too_large POST /v1/judge/batch over GSPAMJUDGE_BATCH_MAX pages — use /v1/jobs
413 job_too_large POST /v1/jobs over GSPAMJUDGE_JOBS_MAX items (#51)
413 item_too_large POST /v1/jobs, /v1/audits/site* (import items, #119): html/text over GSPAMJUDGE_JOBS_ITEM_MAX_CHARS per item or GSPAMJUDGE_JOBS_MAX_CHARS per job
413 payload_too_large other oversized request
422 validation_error body/parameters do not match the schema; details[] = {loc, msg, type}
400 registration_failed Registration refused (#387); no longer returned by POST /v1/auth/register since #400

POST /v1/auth/register (#400) always answers 202 {status: "pending", message} with the same body, whether or not the e-mail has an account, so the status does not reveal existing accounts. The account becomes active only after the link from the e-mail (POST /v1/auth/confirm {token} or the panel page /confirm/{token}); an existing account gets an informational e-mail instead. Only a password that breaks the rules returns 400 invalid_password (checked the same way for every e-mail). An unconfirmed account cannot sign in (invalid_credentials).

HTTP code When
400 email_unavailable PATCH /v1/users/me: the new e-mail cannot be used (#387)
400 invalid_password Password does not meet the rules: min. 8 characters, no name from the e-mail (#387)
400 invalid_credentials Sign-in failed: wrong e-mail or password (#387)
400 account_not_verified Sign-in requires a confirmed e-mail (#387)
400 invalid_token Password reset or e-mail confirmation link invalid or expired (#387, POST /v1/auth/confirm #400)
429 rate_limited API key rate limit exceeded (#50); headers Retry-After, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
429 rate_limited Too many sign-in, registration, e-mail confirmation or password reset attempts (#68, #400): per IP and per e-mail, 10/min, growing block; header Retry-After
429 too_many_active_jobs project already has GSPAMJUDGE_JOBS_MAX_ACTIVE queued/running jobs, or the organization has GSPAMJUDGE_JOBS_MAX_ACTIVE_ORG (2) queued/running jobs and site audits (#220); Retry-After
503 spend_cap_reached global daily spend cap reached (#94, GSPAMJUDGE_DAILY_SPEND_CAP_USD, UTC day); details = {day, spent_usd, cap_usd}, Retry-After: 3600; resets at 00:00 UTC
503 beta_budget_reached closed beta paused: global monthly beta budget used up (#220, GSPAMJUDGE_BETA_MONTHLY_BUDGET_USD, UTC month; 0 = off); message "Beta paused: the monthly beta limit has been reached…" (PL with Accept-Language: pl); details = {month, spent_usd, budget_usd}, Retry-After: 3600; checked before every judge, batch, job, site audit, upgrade and JS retry
429 quota_exceeded spending limit reached (#89, #117, #221): per user, project or organization (monthly allowance, then PAYG balance), per day or month (UTC); amounts in USD, message e.g. "Monthly limit of $20.00 reached for this organization: used $19.99, balance $0.00, requested $0.0450."; details = {subject, period, unit: "usd", limit_usd, used_usd, requested_usd, remaining_usd} (+ balance_usd for org); a batch must fit entirely. Usage: GET /v1/usage, see Billing
500 internal_error unexpected server error; generic message (see below)
502 fetch_failed the page at url could not be fetched (HTTP error, timeout, DNS; host resolving to a non-public address, redirect to one, body over 10 MB)
502 inference_failed no rule could be scored (the inference provider failed for every rule); the page is not charged
503 unavailable service temporarily unavailable
503 rate_limiter_unavailable Redis rate limiter unreachable (fail-closed; GSPAMJUDGE_APIKEY_RATE_FAIL_OPEN=true lets requests through)
503 queue_unavailable POST /v1/jobs: job queue (Redis) not configured or unreachable (#51)
503 webhooks_unavailable POST /v1/jobs with webhook_url, but the server has no webhook signing secret (#51)

message for 500/502 is generic by default; the exception text (type + message, max 300 characters) appears only with GSPAMJUDGE_EXPOSE_ERRORS=true (dev). Details are always in the server log under request_id.

A new code is a non-breaking change (changelog entry); changing the meaning of or removing a code happens only in a new version. Errors of single pages in a batch are not HTTP errors: they go to results[].error (response 200). Paths outside /v1 (panel, /health) keep FastAPI's default responses.