API Reference
Errors & Status Codes

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

StatusMeaningWhen you'll see it
200 OKSuccessGET and PUT/PATCH requests that succeed
201 CreatedResource createdSuccessful POST that created a new resource
204 No ContentSuccess, no bodySuccessful DELETE
400 Bad RequestInvalid request bodyMalformed JSON, wrong field types, missing required fields
401 UnauthorizedAuthentication failedMissing API key, invalid key, expired key
403 ForbiddenInsufficient permissionRead-only key on a write endpoint, project-scoped key on another project
404 Not FoundResource not foundResource doesn't exist, or exists but belongs to another org
409 ConflictState conflictAction already running, duplicate idempotency key in-flight
422 Unprocessable EntityInvalid field valuesValid JSON but values fail business rules
429 Too Many RequestsRate limit exceededExceeded req/min for your key type
500 Internal Server ErrorServer errorUnexpected 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

CodeStatusDescription
missing_auth401No Authorization or X-API-Key header was provided
invalid_key_format401The key doesn't match the expected format (oec_live_ro_* or oec_live_rw_*)
invalid_key401The key is correctly formatted but does not exist or has been revoked
key_expired401The key existed but its expiry date has passed
insufficient_scope403The key is valid but not scoped to this project or organization
read_only_key403A read-only key was used on a write endpoint

Resource Errors

CodeStatusDescription
not_found404The resource does not exist (or belongs to another org)
validation_error400Request body failed schema validation
unprocessable422Request body is valid but values fail business logic

Action Errors

CodeStatusDescription
action_already_running409An action (deploy, restart, etc.) is already in progress for this environment
environment_not_running409Attempted an action that requires the environment to be running (e.g., restart)
environment_already_stopped409Attempted to stop an already-stopped environment
quota_exceeded422The action would exceed your plan's resource quotas
plan_feature_unavailable403The action requires a higher plan

Rate Limit Errors

CodeStatusDescription
rate_limit_exceeded429Exceeded 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.