Skip to main content

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​

CodeStatusWhenWhat to do
invalid_request400Body isn't JSON, top-level field missing, query string malformedFix the request shape and retry.
authentication_failed401No Authorization header, malformed token, revoked / expired / unknown PATRe-mint a token. If the issue persists, check IP allow-list on the PAT.
payment_required402No funds, no default payment method, declined card on a billable actionAdd a card or top up credit in the client area, then retry.
permission_denied403PAT lacks the scope this endpoint needs, caller IP isn't in the PAT's allow-list, or response was filtered by tenant defenseMint a new PAT with the right scopes. Check the PAT's IP allow-list.
two_factor_required403This action requires fresh step-up; the call didn't carry a valid X-Confirm-TokenCall POST /v1/auth/step-up with the user's password, then retry with the returned token in X-Confirm-Token.
kyc_required403Action gated on completed identity verification (regulated billing actions, fund transfers, etc.)Complete KYC in the client area, then retry.
not_found404Resource doesn't exist, or it exists but doesn't belong to this PAT's tenantConfirm the id. We deliberately don't differentiate "doesn't exist" from "not yours" to avoid enumeration.
method_not_allowed405Wrong HTTP verb for the route (POST to a GET-only path, etc.)Check the API Reference for the correct method. Response includes Allow header.
conflict409Resource state forbids the change (deleting a server with pending snapshots; attaching an IP already in use elsewhere)Resolve the conflicting state, then retry.
idempotency_conflict409Same Idempotency-Key reused within 24h with a different bodyMint a fresh Idempotency-Key for the new request — or replay the exact original body to receive the cached response.
operation_in_progress409A conflicting async op is already running on this target (rebooting a server while reinstall is in flight). Response carries the in-flight operation_idPoll the existing op or subscribe to its webhook. Don't retry.
payload_too_large413Request body exceeded the per-route limitSlim the payload. Reference docs note the limit per endpoint.
unsupported_media_type415Content-Type isn't supported (we expect application/json for write requests)Send Content-Type: application/json.
validation_failed422Field-level validation failed; errors[] lists each problemFix the listed fields and retry. See Field-level errors.
rate_limited429Per-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_error500Bug on our side. We've already logged it with your request_idFile a ticket with the request_id. Retry with exponential backoff — these are not idempotent-safe by default.
upstream_unavailable503A dependency (server provider, DNS registrar, payment processor, or billing platform itself) returned an error or timed outRetry 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 codeWhen
requiredField is missing
invalid_formatWrong shape (email, CIDR, ISO date, etc.)
invalid_or_missingGeneric — surfaced when the upstream is non-specific
too_short / too_longLength bounds
out_of_rangeNumber outside accepted bounds
unknown_fieldField was provided but isn't defined for this route

Headers worth checking​

  • X-Request-Id — always present on errors; pair it with request_id in the body when filing tickets.
  • Retry-After — present on 429 and some 503 responses. Seconds to wait before retrying.
  • Allow — present on 405. 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.