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
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.
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:
| Status | error.type |
|---|---|
| 400 | invalid_request_error |
| 401 | authentication_error |
| 403 | permission_error |
| 404 | not_found_error |
| 413 | request_too_large |
| 5xx | api_error |
401 — authentication
Every one of these means the credential is wrong, not the request.
code | Message | What to do |
|---|---|---|
invalid_api_key | missing Vulnetix API key: send it as Authorization: Bearer or x-api-key | Nothing reached us. Check the variable is exported in the shell your client actually runs in. |
invalid_api_key | invalid Vulnetix API key | Something 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_key | unknown organisation | The org UUID in the base URL is wrong, malformed, or the organisation is inactive. |
invalid_api_key | missing organisation: use https://guardrails.vulnetix.com/{providerSlug}/{orgUuid}/v1 as the base URL | No org UUID in the path at all. |
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.
code | Meaning | Fix |
|---|---|---|
request_blocked | A guardrail blocked it. blocked_by names the rule. | The content matched a rule you configured. See guardrails. |
provider_denied | Your org has denied this provider. | Provider policy. |
provider_disabled | The provider is disabled platform-wide. | Not your policy — contact support. |
model_denied | This model is on your org’s deny list. | Model policy. |
model_not_allowed | The 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_missing | No 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
code | Meaning | Fix |
|---|---|---|
unknown_provider | The first path segment is not a provider slug. | Check the spelling against the provider catalog. |
unsupported_api | The 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
| Message | Meaning |
|---|---|
could not read request body | The body could not be read from the connection. |
invalid JSON body: … | The body is not valid JSON for the surface you called. |
model is required | No 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
| Status | Message | Meaning |
|---|---|---|
| 500 | internal error | A bug. Report it. |
| 500 | failed to encode request | A bug. Report it. |
| 502 | upstream request failed | We could not reach the provider. Usually the provider is down. |
| 502 | provider key decryption failed | Your stored key could not be decrypted. Re-store it: BYOK. |
| 502 | provider credential configuration invalid | A platform misconfiguration. Report it. |
| 503 | authentication backend unavailable | We could not verify your key. Fail-closed by design — never a pass-through. |
| 503 | policy backend unavailable | We could not load your guardrails, and FAIL_OPEN is off (the default). The request is refused rather than forwarded unscreened. |
| 503 | provider backend unavailable | We could not load the provider catalog. |
| 503 | model catalog unavailable | We could not load the model list. |
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.
Related
- Troubleshooting — symptom-first.
- Your two keys — the cause of most 401s.
- Base URLs — the cause of every
unsupported_api.