Errors
All errors use RFC 7807
with Content-Type: application/problem+json:
{
"type": "https://developers.vps-server.host/errors/permission-denied",
"title": "Permission denied",
"status": 403,
"code": "permission_denied",
"detail": "PAT is missing scope 'servers:power'.",
"instance": "/v1/servers/srv_abc123:reboot",
"request_id": "req_01HRZX5QY7N9T6V4M2J8K3C5L9"
}
The code field is stable across versions — branch on it programmatically.
The request_id is invaluable in support tickets; always include it.
Error catalog
| Code | Status | When | What to do |
|---|---|---|---|
invalid_request | 400 | Body isn't JSON, top-level field missing, query string malformed | Fix the request shape and retry. |
authentication_failed | 401 | No Authorization header, malformed token, revoked / expired / unknown PAT | Re-mint a token. If the issue persists, check IP allow-list on the PAT. |
payment_required | 402 | No funds, no default payment method, declined card on a billable action | Add a card or top up credit in the client area, then retry. |
permission_denied | 403 | PAT lacks the scope this endpoint needs, caller IP isn't in the PAT's allow-list, or response was filtered by tenant defense | Mint a new PAT with the right scopes. Check the PAT's IP allow-list. |
two_factor_required | 403 | This action requires fresh step-up; the call didn't carry a valid X-Confirm-Token | Call POST /v1/auth/step-up with the user's password, then retry with the returned token in X-Confirm-Token. |
kyc_required | 403 | Action gated on completed identity verification (regulated billing actions, fund transfers, etc.) | Complete KYC in the client area, then retry. |
not_found | 404 | Resource doesn't exist, or it exists but doesn't belong to this PAT's tenant | Confirm the id. We deliberately don't differentiate "doesn't exist" from "not yours" to avoid enumeration. |
method_not_allowed | 405 | Wrong HTTP verb for the route (POST to a GET-only path, etc.) | Check the API Reference for the correct method. Response includes Allow header. |
conflict | 409 | Resource state forbids the change (deleting a server with pending snapshots; attaching an IP already in use elsewhere) | Resolve the conflicting state, then retry. |
idempotency_conflict | 409 | Same Idempotency-Key reused within 24h with a different body | Mint a fresh Idempotency-Key for the new request — or replay the exact original body to receive the cached response. |
operation_in_progress | 409 | A conflicting async op is already running on this target (rebooting a server while reinstall is in flight). Response carries the in-flight operation_id | Poll the existing op or subscribe to its webhook. Don't retry. |
payload_too_large | 413 | Request body exceeded the per-route limit | Slim the payload. Reference docs note the limit per endpoint. |
unsupported_media_type | 415 | Content-Type isn't supported (we expect application/json for write requests) | Send Content-Type: application/json. |
validation_failed | 422 | Field-level validation failed; errors[] lists each problem | Fix the listed fields and retry. See Field-level errors. |
rate_limited | 429 | Per-PAT or per-IP request bucket empty. Response includes a Retry-After header (seconds) | Sleep at least Retry-After seconds. Consider request-coalescing — see Rate limits. |
internal_error | 500 | Bug on our side. We've already logged it with your request_id | File a ticket with the request_id. Retry with exponential backoff — these are not idempotent-safe by default. |
upstream_unavailable | 503 | A dependency (server provider, DNS registrar, payment processor, or billing platform itself) returned an error or timed out | Retry with exponential backoff. We expose detail in detail where safe. |
Why "not found" vs "permission denied"?
When a request references a resource (e.g. GET /v1/servers/srv_abc123),
we return 404 not_found whether the id is unknown OR the id exists but
belongs to a different tenant. Returning 403 for "exists but not yours"
would leak existence information. The only exception is scope-based
rejection (PAT doesn't carry the scope this endpoint needs at all), which
returns 403 permission_denied.
Field-level errors (422)
validation_failed carries an errors array, one entry per offending field:
{
"type": "https://developers.vps-server.host/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"code": "validation_failed",
"detail": "One or more fields are invalid.",
"instance": "/v1/contacts",
"request_id": "req_…",
"errors": [
{ "field": "email", "code": "invalid_format",
"message": "Must be a valid RFC 5322 email address." },
{ "field": "phone", "code": "required",
"message": "Phone is required when company is set." }
]
}
Common errors[].code values:
| Field code | When |
|---|---|
required | Field is missing |
invalid_format | Wrong shape (email, CIDR, ISO date, etc.) |
invalid_or_missing | Generic — surfaced when the upstream is non-specific |
too_short / too_long | Length bounds |
out_of_range | Number outside accepted bounds |
unknown_field | Field was provided but isn't defined for this route |
Headers worth checking
X-Request-Id— always present on errors; pair it withrequest_idin the body when filing tickets.Retry-After— present on429and some503responses. Seconds to wait before retrying.Allow— present on405. Comma-separated list of methods the route actually accepts.
Retry strategy
Default for production callers:
status retry? backoff
─────────────────────────────────────────────────────
408 yes exponential, cap 30s
429 yes honor Retry-After, fall back to 1s × 2^n
500 yes exponential, cap 30s, max 3 attempts
502 yes exponential, cap 30s
503 yes exponential, cap 30s, max 5 attempts
504 yes exponential, cap 30s
others no surface to caller
Reads (GET, HEAD) are always safe to retry. Writes carry an
Idempotency-Key — see Errors, retries, idempotency
(this page) and the Quickstart for the wire format.