Error Handling
The error format, status codes, and retry strategies for the Creed Space API.
Error Response Format
Error bodies take one of four shapes, depending on the error. Branch on the HTTP status code first, then read the body shape for that status.
Authentication and scope errors (RFC 7807)
API-key authentication (401 invalid_api_key) and scope (403 scope_required) errors follow the RFC 7807 problem+json format and are returned with Content-Type: application/problem+json:
{
"type": "https://api.creed.space/problems/invalid-api-key",
"title": "Invalid API Key",
"status": 401,
"detail": "API key is invalid or not found.",
"instance": "/api/v1/safety/evaluate",
"error_code": "INVALID_API_KEY",
"reason": "invalid",
"docs_url": "https://docs.creed.space/authentication"
}The problem type is a URI of the form https://api.creed.space/problems/<slug>. The slugs in the table below identify the problem type; in the type URI they appear kebab-cased, so invalid_api_key becomes .../problems/invalid-api-key. Match on the full type URI as returned, rather than reconstructing it from the snake_case slug.
Validation errors (422)
A request body that fails validation returns 422 with the failing fields:
{
"detail": [
{"loc": ["body", "input_text"], "msg": "Field required", "type": "missing"}
],
"error": "Validation error"
}Rate limit errors (429)
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"retry_after": 30
}The response also carries a Retry-After header (seconds) and the X-RateLimit-* headers described in Rate Limits.
That body comes from the per-key limit. The per-IP limit that runs before authentication returns a nested body instead:
{
"error": {
"type": "rate_limit_exceeded",
"message": "Rate limit exceeded. Too many requests. Please try again later.",
"retry_after": 30
},
"detail": "Rate limit exceeded"
}Both carry Retry-After, so branch on the 429 status and read that header rather than parsing the body.
Other HTTP errors
Other errors raised by an endpoint, such as 400, 404 or 503, use a flat shape:
{
"error": "Evaluation timed out",
"status_code": 503
}An unexpected server failure returns 500 with {"error": "Internal server error", "message": "..."}.
Error Codes
| Status | Code | Body shape | Description |
|---|---|---|---|
| 401 | invalid_api_key | problem+json | Key is invalid, missing, badly formatted or revoked (see reason) |
| 403 | scope_required | problem+json | Key lacks the required permission scope |
| 404 | — | flat | Requested resource does not exist |
| 422 | — | validation | Request body validation failed |
| 429 | rate_limit_exceeded | rate limit | Request rate limit exceeded |
| 500 | — | flat | Unexpected server error |
| 503 | — | flat | Service or evaluation temporarily unavailable; fail closed |
Handling Errors
Python (REST)
import logging
import time
import httpx
logger = logging.getLogger(__name__)
http = httpx.Client(
base_url="https://api.creed.space",
headers={"X-API-Key": "cs_live_..."},
timeout=30.0,
)
def evaluate(text: str) -> dict:
response = http.post("/api/v1/safety/evaluate", json={"input_text": text})
if response.status_code == 401:
# Invalid or missing API key (problem+json)
raise RuntimeError(f"Auth failed: {response.json().get('detail')}")
if response.status_code == 403:
raise RuntimeError(f"Missing scope: {response.json().get('detail')}")
if response.status_code == 422:
# Invalid request body
raise ValueError(f"Validation failed: {response.json().get('detail')}")
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "1"))
logger.warning("Rate limited, retry after %ss", retry_after)
time.sleep(retry_after)
return evaluate(text)
response.raise_for_status()
return response.json()["evaluation"]Python SDK
The Python SDK raises typed errors from creed_sdk.errors:
from creed_sdk.errors import AuthenticationError, CreedError, RateLimitError, TimeoutError
try:
result = await client.decide(tool_name="send_email", arguments={"to": "user@example.com"})
except AuthenticationError:
logger.error("API key error")
raise
except RateLimitError as e:
logger.warning("Rate limited, retry after %ss", e.retry_after)
raise
except TimeoutError:
logger.error("Request timed out")
raise
except CreedError as e:
logger.error("API error: %s (status %s)", e.code, e.status_code)
raiseJavaScript (REST)
async function evaluate(text: string): Promise<unknown> {
const response = await fetch('https://api.creed.space/api/v1/safety/evaluate', {
method: 'POST',
headers: { 'X-API-Key': 'cs_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({ input_text: text }),
});
if (response.status === 401 || response.status === 403) {
const problem = await response.json();
throw new Error(`Auth failed: ${problem.detail}`);
}
if (response.status === 422) {
const body = await response.json();
throw new Error(`Validation failed: ${JSON.stringify(body.detail)}`);
}
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') ?? '1');
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
return evaluate(text);
}
if (!response.ok) {
throw new Error(`Safety check failed with status ${response.status}`);
}
return (await response.json()).evaluation;
}The JavaScript SDK throws CreedError, AuthenticationError, RateLimitError (with retryAfter) and ServerError.
Retry Strategies
Neither SDK retries automatically. Retry only on 429 and 5xx, never on 4xx validation or auth errors. A governance decide() call records a new run and action each time, so retry it with care.
Simple Retry
Using the evaluate helper from above:
def evaluate_with_retry(text: str, max_retries: int = 3) -> dict:
for attempt in range(max_retries):
try:
return evaluate(text)
except httpx.HTTPStatusError as e:
if e.response.status_code < 500 or attempt == max_retries - 1:
raise
time.sleep(1 * (attempt + 1))Circuit Breaker
For production systems, use a circuit breaker pattern:
from circuitbreaker import circuit
@circuit(failure_threshold=5, recovery_timeout=30)
def evaluate_safe(text: str) -> dict:
return evaluate(text)Debugging
Enable Logging
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("httpx").setLevel(logging.DEBUG)Check Response Headers
Every response carries an X-Request-ID header. Log it with the status code:
response = http.post("/api/v1/safety/evaluate", json={"input_text": text})
if response.is_error:
print(f"Request ID: {response.headers.get('X-Request-ID')}")
print(f"Status: {response.status_code}")Getting Help
When errors persist:
- Check that the API is up:
GET https://api.creed.space/healthreturns200when the service is healthy - Include the request ID when contacting support
- Review your Dashboard for usage issues