Errors
SkyState uses standard HTTP status codes and a consistent JSON error body for all error responses.
Error Response Format
All error responses use this shape:
{
"error": "not_found",
"message": "Project not found"
}| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error code (snake_case) |
message | string | Human-readable explanation |
Note: The 402 (over limit) and 409 (version conflict) responses use different shapes. See the sections below.
HTTP Status Codes
| Code | Meaning | When it occurs |
|---|---|---|
200 | OK | Successful read or action |
201 | Created | Resource created (config versions, projects) |
204 | No Content | Successful update or delete with no body |
302 | Redirect | OAuth flow redirects |
400 | Bad Request | Validation error, missing required fields, invalid format |
401 | Unauthorized | Missing or invalid authentication credentials |
402 | Payment Required | Resource or storage limit exceeded for the account's tier |
403 | Forbidden | Authenticated but not permitted (e.g. dev key used outside development) |
404 | Not Found | Resource does not exist or does not belong to the account |
409 | Conflict | Version conflict on PATCH, or slug already taken |
429 | Too Many Requests | Monthly API request quota exceeded |
500 | Internal Server Error | Unexpected server-side error |
503 | Service Unavailable | Health check indicates database is unreachable |
Common Error Codes
400 Bad Request
error | message (example) | Endpoint |
|---|---|---|
invalid_slug | "Slugs must contain only lowercase alphanumeric characters and hyphens" | Account slug update, public config |
invalid_slug_format | "Slugs must contain only lowercase alphanumeric characters and hyphens" | Public config |
invalid_config | "..." | Config create (PUT) |
invalid_patch | "..." | Config PATCH |
if_match_required | "If-Match header with version string is required for PATCH requests" | Config PATCH |
if_match_required | "If-Match: * is not supported. Provide the explicit version from your last read." | Config PATCH |
validation_error | "..." | Project create, billing checkout, tier change |
checkout_error | "Account not found" | Billing checkout |
portal_error | "..." | Billing portal |
402 Payment Required — Over Limit
When a resource or storage limit is exceeded, the server returns a structured LimitResponse instead of the standard error shape:
{
"resource": "projects",
"current": 3,
"limit": 3,
"tier": "free",
"upgradeTier": "hobby",
"checkoutUrl": "/upgrade",
"code": "LIMIT_PROJECTS"
}| Field | Type | Description |
|---|---|---|
resource | string | The resource that is over limit (e.g. "projects") |
current | integer | Current count |
limit | integer | Maximum allowed on the current tier |
tier | string | Account's current tier |
upgradeTier | string or null | Next tier that would lift the limit; null if already on highest tier |
checkoutUrl | string or null | Frontend URL path for upgrading |
code | string | Machine-readable limit code (e.g. "LIMIT_PROJECTS") |
403 Forbidden
error | message | Endpoint |
|---|---|---|
forbidden | "Dev API key required." | Dev config PATCH |
forbidden | "Dev API keys can only write to the development environment." | Dev config PATCH |
404 Not Found
error | message | Endpoint |
|---|---|---|
not_found | "Project not found" | Project endpoints |
not_found | "Account not found" | Account endpoints |
not_found | "Dev API key not found" | Dev key revoke |
409 Conflict — Version Conflict (Config PATCH)
When a PATCH request's If-Match version does not match the current version, the server returns a ConflictResponse:
{
"code": "VERSION_CONFLICT",
"message": "The config has been modified since your last read.",
"currentVersion": "1.2.5"
}| Field | Type | Description |
|---|---|---|
code | string | "VERSION_CONFLICT" |
message | string | Human-readable explanation |
currentVersion | string or null | The actual current version string |
The response also includes an ETag header with the current version.
To resolve a conflict: fetch the latest config, re-apply your changes, and retry the PATCH with the updated If-Match value.
409 Conflict — Slug Already Taken
When setting an account slug that is already in use:
{
"error": "slug_conflict",
"message": "The slug 'jane-smith' is already taken."
}429 Too Many Requests — Monthly Quota
See Rate Limiting for the full 429 response format.
500 Internal Server Error
error | message |
|---|---|
internal_error | "An unexpected error occurred." |
503 Service Unavailable — Health Check
{
"status": "degraded",
"database": "unreachable"
}Authentication Errors
Authentication failures return 401 Unauthorized with no JSON body. This occurs when:
- The
Authorizationheader is missing on a protected endpoint - The Bearer token is invalid or rejected by GitHub
- The Dev API key does not exist or has been revoked
- The Dev API key hash does not match any stored key
Handling Errors in Client Code
A general-purpose error handler should:
- Check the HTTP status code first.
- For
400,403,404,500: parse the{ error, message }body. - For
402: parse theLimitResponsebody and surface the upgrade prompt. - For
409on PATCH: parseConflictResponse, usecurrentVersionto refetch and retry. - For
429: readRetry-Afterand wait before retrying. - For
401: prompt the user to re-authenticate.