WellMarked Docs
Guides

Errors

One envelope for every 4xx and 5xx, three fields built for autonomous callers, and the full table of codes the API can emit.

Every error response on this API has the same shape:

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Monthly request quota exhausted.",
    "retry": true,
    "docs_url": "https://wellmarked.io/docs/guides/rate-limits",
    "upgrade_url": "https://wellmarked.io/billing",
    "retry_after": 1843
  }
}

retry_after (seconds) is present on 429.

The three agent fields

Most callers here are autonomous. They need to decide, without a human, whether to back off, change the request, or escalate. Three fields make that decision without parsing prose:

FieldMeaning
retryCan retrying this exact request, unchanged, succeed later?
docs_urlThe documentation section describing this code.
upgrade_urlPresent only where a plan upgrade is the actual remedy.

upgrade_url is the escalation signal

It is never a generic billing link. Its presence means a human with a card has to act — that is the whole point, and a generic link would destroy the signal. Branch on retry to decide whether to back off; treat upgrade_url as "escalate to my operator".

An error where retry is false will not become true by waiting. no_content, for example, is a fact about the page — an agent that retries it burns quota forever.

Every code

StatusCodeRetry?Meaning
401invalid_api_keynoThe key format is wrong, or the key does not exist.
401missing_api_keynoNo `Authorization: Bearer …` header was sent.
402payment_invalidnoThe payment was replayed, tampered with, or mispriced. Request a fresh challenge — do not retry this one.
402payment_requiredyesThe x402 challenge itself. Pay the quoted amount and retry the same request.
402payment_settlement_failedyesThe payment has not settled on-chain yet. Retry the same request shortly.
403account_inactivenoThe account behind this key has been deactivated.
403domain_deniednoThe URL matches a `deny_patterns` rule on the key.
403domain_not_allowednoThe URL's domain is not in the key's `allow_domains` list.
403forbiddennoThe bulk or crawl job belongs to a different account.
403insufficient_scopenoThe key is valid but not scoped for this route. Mint a broader key — this is not a plan limit.
403plan_not_supportednoCrawl, or `render_js=true`, requires Pro, Growth, or Enterprise.
403robots_disallowedno`robots.txt` disallows this URL and the key is set to `respect_robots=strict`.
404job_not_foundnoUnknown `job_id`, or the 6-hour result TTL has expired.
404key_not_foundnoNo such API key on this account (`DELETE /keys/{id}`).
409email_takenno`POST /register`: that email already has an account.
409idempotency_in_progressyesThe same key and body arrived while the first submission was still enqueuing. Retry in a moment to get the job id.
422bulk_cap_exceedednoThe URL count exceeds the plan limit (50 on Pro, 200 on Growth).
422crawl_depth_exceedednoThe requested depth exceeds the plan limit (5 on Pro, 10 on Growth).
422fetch_failedyesThe target could not be reached (DNS, TLS, or connection failure).
422idempotency_key_reusenoThe same `Idempotency-Key` arrived with a *different* body. Surfaced rather than silently starting a second job.
422invalid_requestnoThe request body failed validation. The message names the offending field.
422js_rendering_unavailablenoJS rendering is temporarily unavailable server-side. Escalate to us rather than upgrading.
422no_contentnoNo main content could be identified on the page. A fact about the page, not a transient failure.
422search_cap_exceededno`num_results` exceeds the plan limit (5 Free, 10 Pro, 50 Growth).
422target_http_erroryesThe target returned a non-2xx status.
422target_timeoutyesThe target URL did not respond in time.
422unsafe_urlnoThe URL resolves to a private, loopback, or link-local address.
422webhook_url_invalidno`webhook_url` is not `https://`, or resolves to a private host.
429challenge_rate_limitedyesToo many unpaid 402 challenges from this IP. Every challenge mints a deposit address, so the path is capped.
429rate_limit_exceededyesThe monthly quota is exhausted, or this batch would exceed it. `retry_after` counts down to the reset.
429rate_limit_too_fastyesPer-second limit hit — Free 5/s, Pro 20/s, Growth 100/s, Enterprise unlimited. `Retry-After-Ms` carries the precise window.
429register_rate_limitedyes`POST /register`: too many registrations from this IP. Carries `Retry-After`.
503service_unavailableyesThe registration limiter backend is down and the endpoint fails closed. Retry shortly.

Rows marked ↑ in the retry column also carry an upgrade_url.

Handling them

Both official SDKs map these onto typed exceptions, so you can catch the class you care about rather than switching on strings. The exception carries code, status_code / statusCode, retry_after / retryAfter, retry_after_ms / retryAfterMs, and request_id / requestId.

upgrade_url is HTTP-only

The SDK exceptions do not surface upgrade_url or docs_url. If your escalation path needs the billing link, read it from the raw response body, or key off the codes marked ↑ above.

import time
from wellmarked import WellMarked, RateLimitError, UnprocessableEntityError

with WellMarked(api_key="wm_...") as wm:
    try:
        result = wm.extract("https://example.com")
    except RateLimitError as e:
        # Branch on `code`, not the status — the two 429s need
        # completely different handling.
        if e.code == "rate_limit_too_fast":
            time.sleep(e.retry_after_ms / 1000)   # milliseconds
        else:
            escalate()                            # the month is spent
    except UnprocessableEntityError as e:
        # The page could not be extracted; e.code names why.
        ...