API

Errors

The RFC 9457 problem format used by the VitalSentinel API, the full error catalog, and what a client should actually do about each response.

View as Markdown

Every error is application/problem+json, following RFC 9457. Here is a 403 for a missing scope, byte for byte:

{
  "type": "https://docs.vitalsentinel.com/api/errors/insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This credential does not have the 'read:overview' scope, which this endpoint requires. Create a new credential with that scope, or ask a workspace admin to grant it.",
  "instance": "/api/public/v1/domains/9c2f4d18-5e6a-4b70-8c19-2d3f5a7b9e04/overview",
  "request_id": "3f9a1c22-7b48-4e0d-96a1-5c8b2d7e4f10",
  "required_scope": "read:overview",
  "granted_scopes": ["read:domains"]
}

Branch on type, never on the prose in detail. The type URI is stable and is covered by the compatibility promise. detail is written to be acted on and can be reworded at any time.

Several problems carry structured extras alongside the prose, so a client rarely has to parse a sentence:

ProblemExtra fields
insufficient-scoperequired_scope, granted_scopes
rate-limitedbucket, retry_after_seconds
quota-exhaustedused, limit, resets_on

The OAuth endpoints are the one exception to all of this: POST /oauth/token returns RFC 6749 errors instead. See Authentication.

The catalog

Every type is https://docs.vitalsentinel.com/api/errors/ followed by a suffix. These are the base families:

type suffixStatusWhat it means and what to do
invalid-credential401Missing, malformed, revoked, expired, or wrong-audience credential. Stop and fix the credential. Retrying will not help.
insufficient-scope403Authenticated, but this endpoint needs a scope the credential lacks. Mint a new credential with that scope.
insufficient-role403The scope was granted, but the creator's workspace role does not allow the operation. A human has to change a membership. A new key will not help.
not-found404No such resource, or it exists and this credential cannot see it. Also returned for a path that matches no operation at all.
method-not-allowed405The path exists, but not on this HTTP method.
invalid-request400Bad parameters, a malformed cursor, mutually exclusive parameters together (a named period sent alongside start_date or end_date, for example), or a missing Idempotency-Key on a run: call.
conflict409An Idempotency-Key was reused with a different request body. This is a client bug.
rate-limited429A bucket or a daily ceiling is exhausted. Wait for Retry-After.
quota-exhausted402A plan quota or the credit allowance is spent. Retrying later today does not help; retrying next billing period might.
workspace-not-writable402The workspace subscription is past due, canceled, or expired. Writes are refused exactly as they would be in the dashboard.
api-disabled503The public API is temporarily off. Retry after the interval in Retry-After.
read-only-mode503Reads are still being served; writes and runs are temporarily disabled.
credit-balance-unavailable503The credit balance could not be read, so the request was refused rather than served unmetered. Retry.

Many operations return a more specific suffix

The table above is not the complete set of strings you will see in type. Individual operations narrow the suffix while keeping the same status, so an exact-match check against that table alone will fall through on real responses.

A 404 from a CrUX endpoint is crux-url-not-found, not not-found. A 401 caused by an IP allowlist is ip-not-allowed, not invalid-credential. There are roughly forty of these.

The specialization is systematic, so you do not need the full list:

StatusBase suffixSpecific suffixes you may see
401invalid-credentialmissing-credential, invalid-audience, ip-not-allowed, token-revoked, grant-revoked
404not-founddomain-not-found, workspace-not-found, crux-not-found, crux-url-not-found, gsc-not-found, gsc-property-not-found, gsc-property-not-linked, synthetic-not-found, synthetic-url-not-found, alert-rule-not-found, alert-history-not-found, notification-not-found, notification-trend-not-found, report-not-found, report-run-not-found, report-template-not-found, report-not-registered, check-data-not-found, and others in the same shape
400invalid-requestconsent-already-used, csrf-required, workspace-not-accessible
403insufficient-scope, insufficient-roleforbidden, plan-restriction, plan-feature-unavailable
409conflictidempotency-key-reuse, report-template-name-conflict
410nonereport-result-expired, when a stored report result has aged out
500noneclickhouse-error, when a RUM query against the analytics store fails
502noneupstream-error, upstream-unavailable, storage-unavailable, when a dependency fails

Until 6 August 2026, a rate-limited RUM read arrived as clickhouse-error instead. The RUM endpoints delegate to handlers that cap reads well below the public ceiling, and their rejection was published as a database failure, with no Retry-After header and no interval to wait for. It is now an ordinary 429 rate-limited on the rum-read bucket, carrying bucket, retry_after_seconds, and the header. An integration written before that date reads it as a transient server fault and retries immediately, straight back into the same window. Move that path onto your 429 handling.

The rule for a client: switch on the HTTP status first, then on the suffix only where you handle a case specifically. An unrecognized suffix should fall back to its status family rather than to an error path.

kind = problem["type"].rsplit("/", 1)[-1]

if response.status_code == 404:
    # Every *-not-found narrows this. Treat them all as "not there, or not visible to me".
    return None
if response.status_code == 401:
    # ip-not-allowed is the one worth naming: the credential is fine, the network is not.
    raise BadCredential(hint="check the IP allowlist" if kind == "ip-not-allowed" else None)

New suffixes are additive and can appear at any time. See Versioning.

Two 403s that are not the same problem

insufficient-scope and insufficient-role both return 403, and telling them apart is the difference between a client that recovers and one that retries forever.

insufficient-scopeinsufficient-role
CauseThe credential was never granted that scopeThe scope is granted, but the creator's role forbids the operation
FixCreate a new credential with the scopeA workspace owner or admin has to change someone's membership
Can your code fix it?Yes, by asking for a wider grantNo. Surface it to a human and stop

An agent that cannot distinguish them will keep minting credentials that fail the same way. See Scopes for the underlying rule.

Why 404 and not 403

When a credential's workspace access does not cover a resource that exists, the API returns 404, not 403. The two cases are deliberately indistinguishable.

Confirming that a resource exists but is not yours turns the endpoint into an identifier-guessing oracle. If you are getting an unexpected 404 on an identifier you are sure is right, check GET /me for the workspaces your credential actually reaches.

Handling errors well

def call(method: str, path: str, **kwargs) -> dict:
    response = http.request(method, path, **kwargs)

    if response.is_success:
        return response.json()

    problem = response.json()
    status = response.status_code
    kind = problem.get("type", "").rsplit("/", 1)[-1]

    # Transient. Pace off the header, not a fixed sleep.
    if status == 429:
        time.sleep(problem.get("retry_after_seconds")
                   or int(response.headers.get("Retry-After", "5")))
        return call(method, path, **kwargs)

    # Operational, not a contract change. Back off and try again later.
    # 502 is a failed dependency and is also worth retrying.
    if status == 502 or kind in ("api-disabled", "read-only-mode", "credit-balance-unavailable"):
        raise TemporarilyUnavailable(problem, retry_after=response.headers.get("Retry-After"))

    # Permanent until a human acts. Never retry these.
    # Status first: every *-not-found narrows 404, and 401 has five specific suffixes.
    if status in (401, 402) or kind in ("insufficient-scope", "insufficient-role"):
        raise NeedsHumanAttention(problem)

    # Your bug. Fix the request.
    raise ClientError(problem)

The three categories that matter:

  1. Retry with backoff: rate-limited, api-disabled, read-only-mode.
  2. Never retry, escalate to a human: invalid-credential, insufficient-scope, insufficient-role, quota-exhausted, workspace-not-writable.
  3. Fix your request: invalid-request, conflict, method-not-allowed, not-found.

Getting help

Every response carries X-Request-Id, and every problem body repeats it as request_id. Quote it in any support conversation. It is what lets us find the exact request in the access log, and without it a report of "a call failed yesterday" is not actionable.

On this page

VitalSentinel

Catch issues before they cost you

Track SEO, performance, and uptime in one place and get alerted the moment something breaks – hours before it hits your traffic.

  • Free plan for 1 domain
  • Set up in minutes
  • No credit card required