Errors & Status Codes
The oec.sh API uses standard HTTP status codes and returns consistent JSON error responses.
Error Response Format
All errors return a JSON object with at least two fields:
{
"error": "error_code",
"message": "A human-readable description of what went wrong."
}Validation errors include an additional detail array with field-level information:
{
"error": "validation_error",
"message": "Request body validation failed.",
"detail": [
{
"field": "name",
"message": "Field is required."
},
{
"field": "webhook_url",
"message": "Must be a valid HTTPS URL."
}
]
}HTTP Status Codes
| Status | Meaning | When you'll see it |
|---|---|---|
200 OK | Success | GET and PUT/PATCH requests that succeed |
201 Created | Resource created | Successful POST that created a new resource |
204 No Content | Success, no body | Successful DELETE |
400 Bad Request | Invalid request body | Malformed JSON, wrong field types, missing required fields |
401 Unauthorized | Authentication failed | Missing API key, invalid key, expired key |
403 Forbidden | Insufficient permission | Read-only key on a write endpoint, project-scoped key on another project |
404 Not Found | Resource not found | Resource doesn't exist, or exists but belongs to another org |
409 Conflict | State conflict | Action already running, duplicate idempotency key in-flight |
422 Unprocessable Entity | Invalid field values | Valid JSON but values fail business rules |
429 Too Many Requests | Rate limit exceeded | Exceeded req/min for your key type |
500 Internal Server Error | Server error | Unexpected error on our side |
404 is also returned for IDOR protection. If you request a resource that exists but belongs to another organization, you receive 404 Not Found — not 403 Forbidden. This prevents information leakage about resources in other organizations.
Common Error Codes
Authentication Errors
| Code | Status | Description |
|---|---|---|
missing_auth | 401 | No Authorization or X-API-Key header was provided |
invalid_key_format | 401 | The key doesn't match the expected format (oec_live_ro_* or oec_live_rw_*) |
invalid_key | 401 | The key is correctly formatted but does not exist or has been revoked |
key_expired | 401 | The key existed but its expiry date has passed |
insufficient_scope | 403 | The key is valid but not scoped to this project or organization |
read_only_key | 403 | A read-only key was used on a write endpoint |
Resource Errors
| Code | Status | Description |
|---|---|---|
not_found | 404 | The resource does not exist (or belongs to another org) |
validation_error | 400 | Request body failed schema validation |
unprocessable | 422 | Request body is valid but values fail business logic |
Action Errors
| Code | Status | Description |
|---|---|---|
action_already_running | 409 | An action (deploy, restart, etc.) is already in progress for this environment |
environment_not_running | 409 | Attempted an action that requires the environment to be running (e.g., restart) |
environment_already_stopped | 409 | Attempted to stop an already-stopped environment |
quota_exceeded | 422 | The action would exceed your plan's resource quotas |
plan_feature_unavailable | 403 | The action requires a higher plan |
Rate Limit Errors
| Code | Status | Description |
|---|---|---|
rate_limit_exceeded | 429 | Exceeded requests per minute for your key type |
Error Examples
401 — Missing API Key
{
"error": "missing_auth",
"message": "No API key was provided. Include an Authorization: Bearer <key> header."
}401 — Expired Key
{
"error": "key_expired",
"message": "This API key expired on 2026-01-01. Create a new key in Settings → API Keys."
}403 — Read-Only Key on Write Endpoint
{
"error": "read_only_key",
"message": "This endpoint requires a full-access key. Read-only keys can only be used with GET endpoints."
}400 — Validation Error
{
"error": "validation_error",
"message": "Request body validation failed.",
"detail": [
{
"field": "webhook_url",
"message": "Must be a valid HTTPS URL. HTTP URLs are not accepted."
}
]
}409 — Action Already Running
{
"error": "action_already_running",
"message": "A deployment is already in progress for this environment. Wait for it to complete before triggering another."
}429 — Rate Limit
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. You have made 20 requests in the last 60 seconds. Please wait before retrying.",
"retry_after": 14
}500 — Internal Server Error
{
"error": "internal_error",
"message": "An unexpected error occurred. If this persists, contact support at support@oec.sh."
}Handling Errors in Code
A robust API client handles the most common error cases explicitly:
import requests
def api_call(session, method, url, **kwargs):
response = session.request(method, url, **kwargs)
if response.status_code == 200 or response.status_code == 201:
return response.json()
error = response.json()
code = error.get("error", "unknown")
if response.status_code == 401:
raise ValueError(f"Authentication failed ({code}): {error['message']}")
if response.status_code == 403:
raise PermissionError(f"Forbidden ({code}): {error['message']}")
if response.status_code == 404:
return None # Resource not found
if response.status_code == 409:
# Action already running — inspect the response, maybe that's fine
raise RuntimeError(f"Conflict ({code}): {error['message']}")
if response.status_code == 429:
retry_after = error.get("retry_after", 60)
raise RateLimitError(retry_after=retry_after)
if response.status_code >= 500:
raise RuntimeError(f"Server error: {error['message']}")
raise RuntimeError(f"Unexpected status {response.status_code}: {error}")500 errors are worth reporting. If you receive a 500 and it's repeatable, contact support@oec.sh with the request details and the X-Request-Id response header — this lets us trace the exact request in our logs.