Error reference

Errors come back in the dialect of the route you called, never the provider’s. An Anthropic SDK that hits /v1/messages gets an Anthropic-shaped error even when the failure is “this provider is not an Anthropic provider” — because that is the only shape it can render. Given an OpenAI-shaped body, Claude Code would print a bare API Error: 403 with no detail, and every guardrail block would be unreadable.

The two envelopes

OpenAI shape

Returned on /v1/chat/completions, /v1/responses, and /v1/models for OpenAI-wire providers.

{
  "error": {
    "message": "human-readable explanation",
    "type": "policy_violation",
    "code": "request_blocked",
    "blocked_by": "no-secrets",
    "violations": [ ... ]
  }
}

blocked_by and violations appear only on guardrail blocks.

Anthropic shape

Returned on /v1/messages and /v1/messages/count_tokens.

{
  "type": "error",
  "error": {
    "type": "permission_error",
    "code": "request_blocked",
    "message": "human-readable explanation",
    "blocked_by": "no-secrets",
    "violations": [ ... ]
  }
}

The error.type is derived from the HTTP status, so it is always one of Anthropic’s own types. code, blocked_by, and violations are Vulnetix extensions — SDKs ignore fields they do not know, and they give you something machine-readable to branch on.

Status-to-type mapping for the Anthropic envelope:

Statuserror.type
400invalid_request_error
401authentication_error
403permission_error
404not_found_error
413request_too_large
5xxapi_error

401 — authentication

Every one of these means the credential is wrong, not the request.

codeMessageWhat to do
invalid_api_keymissing Vulnetix API key: send it as Authorization: Bearer or x-api-keyNothing reached us. Check the variable is exported in the shell your client actually runs in.
invalid_api_keyinvalid Vulnetix API keySomething arrived but it is not your org’s key. Nine times out of ten it is a provider key in the client’s api_key field. See your two keys.
invalid_api_keyunknown organisationThe org UUID in the base URL is wrong, malformed, or the organisation is inactive.
invalid_api_keymissing organisation: use https://guardrails.vulnetix.com/{providerSlug}/{orgUuid}/v1 as the base URLNo org UUID in the path at all.
Warning Authentication never fails open. If the authentication backend is unreachable you get a 503, never a pass-through. A backend outage cannot turn into an unauthenticated request reaching a provider.

403 — policy

You are authenticated. Your organisation’s policy refused the request.

codeMeaningFix
request_blockedA guardrail blocked it. blocked_by names the rule.The content matched a rule you configured. See guardrails.
provider_deniedYour org has denied this provider.Provider policy.
provider_disabledThe provider is disabled platform-wide.Not your policy — contact support.
model_deniedThis model is on your org’s deny list.Model policy.
model_not_allowedThe provider is in allowlist mode and this model is not on the list.Add it, or use a model that is. This is the one that surprises people — see the flip.
provider_key_missingNo provider key is stored for this provider.Not a refusal — there is simply nothing to forward with. Store one: BYOK.

A guardrail block carries the detail:

{
  "error": {
    "message": "request blocked by AI firewall policy: content matched pattern for policy \"no-private-keys\"",
    "type": "policy_violation",
    "code": "request_blocked",
    "blocked_by": "no-private-keys",
    "violations": [
      {
        "policy_name": "no-private-keys",
        "rule_type": "blocked_pattern",
        "action": "block",
        "detail": "content matched pattern for policy \"no-private-keys\""
      }
    ]
  }
}

violations names the rules that fired. It never contains the content that triggered them.

404 — routing

codeMeaningFix
unknown_providerThe first path segment is not a provider slug.Check the spelling against the provider catalog.
unsupported_apiThe provider does not serve the API you called.Your base URL is wrong. The message names the route to use instead.

unsupported_api is the one you will actually hit, and it is almost always the /v1 asymmetry:

{
  "type": "error",
  "error": {
    "type": "not_found_error",
    "code": "unsupported_api",
    "message": "provider groq does not support the Anthropic Messages API (POST /v1/messages). Use POST /v1/chat/completions for groq, or point your Anthropic client at a provider that serves the Messages API: https://guardrails.vulnetix.com/anthropic/{orgUuid} (note: no /v1 suffix — the Anthropic SDK appends it)"
  }
}

See base URLs & request surfaces.

400 — your request

MessageMeaning
could not read request bodyThe body could not be read from the connection.
invalid JSON body: …The body is not valid JSON for the surface you called.
model is requiredNo model in the request.

413 — too large

request_too_large — the request body exceeds 8 MiB.

This is a transport limit, not a policy. It is not configurable, and it is not a token limit. Coding agents that read large files can hit it.

5xx — us

StatusMessageMeaning
500internal errorA bug. Report it.
500failed to encode requestA bug. Report it.
502upstream request failedWe could not reach the provider. Usually the provider is down.
502provider key decryption failedYour stored key could not be decrypted. Re-store it: BYOK.
502provider credential configuration invalidA platform misconfiguration. Report it.
503authentication backend unavailableWe could not verify your key. Fail-closed by design — never a pass-through.
503policy backend unavailableWe could not load your guardrails, and FAIL_OPEN is off (the default). The request is refused rather than forwarded unscreened.
503provider backend unavailableWe could not load the provider catalog.
503model catalog unavailableWe could not load the model list.
Note Every 503 here is the firewall refusing to guess. A firewall that forwards requests it could not screen is not a firewall. If you would rather take the traffic than the outage, FAIL_OPEN exists — but it applies only to guardrail evaluation, and never to authentication or provider/model policy.

Errors you will not see

Because these features do not exist: no 429 rate limit, no quota error, no budget-exceeded error, no billing error. The firewall imposes none of these. A 429 relayed to you came from the provider, against your own account and your own quota. See limitations & roadmap.

Handling these in code

The pattern that matters: distinguish your organisation refused this from the provider refused this. Both are HTTP errors, and only the first is something you can fix by changing your prompt.

from openai import OpenAI, APIStatusError

try:
    resp = client.chat.completions.create(model="gpt-4o-mini", messages=[...])
except APIStatusError as e:
    body = e.response.json().get("error", {})
    code = body.get("code")

    if code == "request_blocked":
        print(f"Blocked by your organisation's rule: {body['blocked_by']}")
    elif code in ("model_denied", "model_not_allowed"):
        print("That model is not permitted by your organisation.")
    elif code == "provider_key_missing":
        print("No provider key is stored. Run: vulnetix ai-firewall key set <provider>")
    else:
        raise

Note there is no error for a redaction. A redacted request returns a normal 200. The client cannot tell. See redact PII.