Errors and Rate Limits
Error format
All errors return a JSON body with a consistent shape:
{
"error": {
"code": "INVALID_PARAMETER",
"message": "Field 'cidr' is required",
"field": "cidr",
"requestId": "req_abc123def456"
}
}
code: stable machine-readable error code, safe to switch on.message: human-readable description, may change between versions.field: optional, present for validation errors.requestId: include this in any support ticket.
HTTP status codes
| Status | Meaning | What to do |
|---|---|---|
| 200 | Success | Process the response body. |
| 201 | Created | Resource created, ID is in the response. |
| 202 | Accepted | Async job started, poll for completion. |
| 400 | Bad request | Fix the request payload. Check field in the error. |
| 401 | Unauthorized | Token is missing, invalid, or expired. Re-authenticate. |
| 403 | Forbidden | Token is valid but the action is not allowed for this user. |
| 404 | Not found | Resource ID does not exist or is in another account. |
| 409 | Conflict | Resource state does not allow this action (e.g. delete on a running VM). |
| 422 | Validation failed | Request shape is valid but business rules rejected it. |
| 429 | Rate limit | Back off and retry. See below. |
| 500 | Server error | Retry with exponential backoff. Open a ticket if it persists. |
| 503 | Service unavailable | Platform in maintenance or overloaded. Retry. |
Common error codes
| Code | Meaning |
|---|---|
INVALID_PARAMETER | A required parameter is missing or malformed. |
INVALID_TOKEN | JWT is missing, expired, or signed incorrectly. |
MFA_REQUIRED | Account has MFA and the token is pre-MFA only. |
RESOURCE_NOT_FOUND | The ID in the URL does not exist in your account. |
RESOURCE_BUSY | The resource is locked by another operation. Retry shortly. |
QUOTA_EXCEEDED | Account quota would be exceeded by this operation. |
INSUFFICIENT_CAPACITY | The platform cannot fulfil the request right now. Retry or try a different zone. |
RATE_LIMITED | Too many requests. Slow down. |
OPERATION_NOT_ALLOWED | Resource state forbids this action (e.g. stop on a Stopped VM). |
Async jobs
Many operations are asynchronous. The API returns 202 immediately with a job ID, and you poll for the result:
{
"jobId": "job_abc123",
"status": "pending"
}
Poll the job:
api GET "/jobs/$JOB_ID"
States:
pending: queued, not yet started.running: in progress.succeeded: finished,resultfield contains the created resource.failed: finished,errorfield describes why.
A robust polling pattern:
wait_for_job() {
local job_id=$1
for i in $(seq 1 60); do
local job
job=$(api GET "/jobs/$job_id")
local status
status=$(echo "$job" | jq -r '.status')
case "$status" in
succeeded) echo "$job" | jq '.result'; return 0 ;;
failed) echo "$job" | jq '.error'; return 1 ;;
*) sleep 5 ;;
esac
done
echo "Timed out after 5 minutes" >&2
return 2
}
Operations that are always async:
- Instance create, start, stop, reboot, destroy.
- Volume create, attach, detach, resize.
- VPC and tier create.
- Kubernetes cluster create, scale, upgrade, delete.
- Backup create and restore.
Operations that are always sync:
- All
GETcalls. - Login and authentication.
- Listing resources.
Rate limits
The API enforces rate limits per account to protect the platform:
| Tier | Requests per minute | Burst |
|---|---|---|
| Standard | 300 | 50 |
| Higher tiers | On request | On request |
Limits apply across all endpoints and all users in your account combined.
When you exceed the limit, the API returns 429 with headers:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716624000
Retry-After: 12
Retry-After is the number of seconds to wait before retrying.
Avoiding rate limits
- Cache lookup data: zone, offering, and template IDs change rarely. Fetch them once at the start of your script and reuse.
- Use list endpoints:
GET /instancesreturns many instances in one call. Do not loopGET /instances/{id}for known IDs. - Batch where possible: bulk endpoints exist for tagging, security group rules, and ACL rules.
- Back off on 429: respect
Retry-After. A simple exponential backoff with jitter prevents thundering herd.
Example backoff in bash:
call_with_retry() {
local max=5
for attempt in $(seq 1 $max); do
response=$(api "$@")
status=$?
if [ $status -eq 0 ]; then
echo "$response"
return 0
fi
if echo "$response" | grep -q "RATE_LIMITED"; then
sleep $((2 ** attempt))
continue
fi
return $status
done
return 1
}
Timeouts
- Connect timeout: clients should use 10 seconds.
- Read timeout: 60 seconds for sync calls, 30 seconds for polling.
- Long-running async operations: do not hold the connection open, use job polling.
Idempotency
The API does not currently support idempotency keys. To make your scripts safe to re-run:
- Check whether the resource exists first by name or tag.
- If it exists, use the existing ID.
- If not, create it.
Pattern:
existing=$(api GET "/vpcs?name=my-prod-vpc" | jq -r '.vpcs[0].id // empty')
if [ -n "$existing" ]; then
VPC_ID="$existing"
else
VPC_ID=$(api POST "/vpcs" -d "..." | jq -r '.vpc.id')
fi
Idempotency key support is on our roadmap.
When to contact support
Open a ticket if you see:
- Persistent 500 errors after retries.
- Async jobs stuck in
pendingorrunningfor more than 30 minutes. - 401 responses with a token that should be valid.
- Rate limit errors when you believe you are well under the limit.
Always include:
- The
requestIdfrom the error response. - The full request (URL, method, body with secrets redacted).
- The full response including status code and body.
- The time of the request in UTC.