Error Handling
Complete reference for OrbitKit API error codes, HTTP status codes, and validation error format.
All API errors return a consistent JSON body:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"docUrl": "https://orbitkit.io/api/errors/#error-code",
"details": []
}
}
Every error includes a docUrl field linking to the relevant section of this page or the rate limits page.
Error codes
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED |
401 | Missing, invalid, or expired authentication token |
FORBIDDEN |
403 | Authenticated but not allowed to perform this action |
NOT_FOUND |
404 | Resource not found (user, app, site, policy, etc.) |
VALIDATION_FAILED |
400 | Request body failed Zod schema validation |
RATE_LIMITED |
429 | Too many requests — slow down |
SLUG_TAKEN |
409 | The requested URL slug is already in use |
SLUG_RESERVED |
409 | The slug is a reserved name (e.g., admin, api) |
DOMAIN_IN_USE |
409 | The domain is already mapped to another app |
APP_LIMIT_REACHED |
400 | Maximum 10 apps per account |
API_KEY_LIMIT_REACHED |
400 | Maximum 10 active API keys per account |
SUBSCRIPTION_REQUIRED |
403 | An active subscription is required to deploy |
SUBSCRIPTION_EXISTS |
400 | This app already has an active subscription |
NO_PAYMENT_METHOD |
400 | No payment method on file |
NO_ACTIVE_SUBSCRIPTION |
400 | No active subscription to cancel or verify |
NOT_CANCELING |
400 | Subscription is not scheduled for cancellation (can’t reactivate) |
ALREADY_CANCELING |
400 | Subscription is already scheduled for cancellation |
SUBSCRIPTION_CANCELING |
400 | Cannot change plan while cancellation is pending (reactivate first) |
SAME_PLAN |
400 | Already on the requested plan |
CARD_ERROR |
400 | Stripe card declined or invalid |
PAYMENT_ERROR |
400 | Generic Stripe payment failure |
NO_STRIPE_CUSTOMER |
400 | User has no Stripe customer record |
INVALID_IDEMPOTENCY_KEY |
400 | Idempotency-Key header exceeds 255 characters |
IDEMPOTENCY_KEY_REUSE |
422 | Same idempotency key used with different request parameters |
CERT_CREATION_FAILED |
500 | SSL certificate provisioning failed |
DEPLOY_FAILED |
500 | Site deployment failed |
PREVIEW_FAILED |
500 | Site preview generation failed |
INTERNAL_ERROR |
500 | Unexpected server error |
HTTP status codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Resource created |
| 204 | Success, no content (delete operations) |
| 400 | Bad request (validation error or invalid state) |
| 401 | Unauthorized (missing or invalid token) |
| 403 | Forbidden (valid token but insufficient permissions) |
| 404 | Not found |
| 409 | Conflict (slug taken, domain in use) |
| 429 | Rate limited |
| 500 | Internal server error |
Validation errors
When a request fails Zod schema validation, the details array contains one entry per field:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"details": [
{
"field": "appName",
"message": "Required"
},
{
"field": "slug",
"message": "String must contain at least 1 character(s)"
}
]
}
}
Each detail has:
field— The field path that failed validationmessage— What’s wrong with the value
Rate limit headers
When you’re approaching or have exceeded a rate limit:
| Header | Description |
|---|---|
Retry-After |
Seconds to wait before retrying (on 429 responses) |
Handling errors in code
Swift
struct ApiErrorResponse: Decodable {
struct ApiError: Decodable {
let code: String
let message: String
let details: [ValidationDetail]?
}
struct ValidationDetail: Decodable {
let field: String
let message: String
}
let error: ApiError
}
func handleResponse(_ data: Data, _ response: HTTPURLResponse) throws {
guard (200...299).contains(response.statusCode) else {
let apiError = try JSONDecoder().decode(ApiErrorResponse.self, from: data)
switch apiError.error.code {
case "UNAUTHORIZED":
// Refresh token and retry
break
case "RATE_LIMITED":
// Back off and retry
break
default:
throw OrbitKitError.api(apiError.error.code, apiError.error.message)
}
return
}
// Handle success...
}
JavaScript
async function orbitKitFetch(path, options = {}) {
const token = await getOrbitKitToken();
const res = await fetch(`https://api.orbitkit.io/api${path}`, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!res.ok) {
const { error } = await res.json();
if (error.code === "UNAUTHORIZED") {
// Refresh token and retry
}
throw new Error(`${error.code}: ${error.message}`);
}
return res.status === 204 ? null : res.json();
}