Skip to content

Errors and retries

Checked against the API code 3 November 2026

When a call fails, the response says what went wrong in a form your code can act on. This page shows that format and which errors are worth retrying. It ends with how to retry without doing the same thing twice.

Almost every error from a /api/v2/ endpoint is a Problem Details document (RFC 7807) with the content type application/problem+json. Errors in another shape lists the exceptions.

This list request asks for too many items per page:

GET /api/v2/vps
curl "https://cloud.hostup.se/api/v2/vps?limit=500" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 400
{
"type": "https://developer.hostup.se/errors/invalid_pagination_limit",
"title": "Validation failed",
"status": 400,
"detail": "Limit must be an integer between 1 and 100.",
"instance": "/api/v2/vps",
"code": "invalid_pagination_limit",
"requestId": "9b2e4f61-3c8a-4d17-a5e0-7f6b1c2d3e4f",
"timestamp": "2026-11-02T10:15:20.000Z",
"errors": [
{
"pointer": "/limit",
"detail": "Limit must be an integer between 1 and 100.",
"code": "invalid_pagination_limit"
}
]
}
FieldWhat it is
codeThe stable machine code. Branch on this.
statusThe HTTP status, repeated in the body.
titleA short summary.
detailWhat went wrong, for a person to read. It is in English, with one exception under Codes with 429. The wording can change. Do not parse it.
typeA link to the page for this code on this site.
instanceThe path you called.
requestIdThe ID of this request. Log it.
timestampWhen the error happened, in UTC.
errorsMostly on validation errors, sometimes on a 409. One entry per field that failed, with pointer, detail and code. Some endpoints leave it out when the whole body is wrong.

pointer shows which field failed. Most pointers look like /limit for a query parameter or /items/0/domainName for a field in the body. A few endpoints put /body/ or /query/ in front, as in /body/domain. Remove that part before you match the pointer to a field. When the whole body is wrong, pointer is / or empty.

  • requiredScopes, on forbidden: the scopes the endpoint checks. When a key is refused says what to do.
  • recovery: the next step, as a fixed action value. The list below says what to do for each one.
  • A block such as vps, domain, invoice or existingInvoice: the resource that is in the way.

What to do for each recovery.action:

  • retry_after: wait retryAfterSeconds. Then send the same request again.
  • await_cooldown: the same action ran a short while ago. Read the resource first. The earlier action may already have done what you want. To run it again, wait retryAfterSeconds or until retryAfterAt. When both are missing or null, the end of the wait is not known.
  • pay_outstanding_invoice: pay the invoice the error names. Then read the resource. Paying often completes the action, a renewal for example.
  • resend_with_cancel_existing_invoice: add cancelExistingInvoice: true to the body. Then send the request again. This cancels the unpaid invoice that is in the way. suggestedBody shows the fields to send.

Other recovery.action values belong to a single endpoint. The detail says what to do. When suggestedBody is there, it holds the fields to send in the next request.

Use status for the broad class and code for what to do. Two errors with the same status can need different handling. A 409 can be vps_not_ready or domain_conflict, for example. Error codes lists every code with the endpoints that return it.

Keep requestId with every error you log. Take it from the body. The X-Request-ID response header carries a different value today. When you contact support about a failed call, send the requestId and the time of the call. Support can then find your request.

A few errors come from the layer in front of the endpoints, not from the endpoint itself. They use a simpler JSON shape with an upper-case code:

Response 415
{
"error": "Unsupported Media Type",
"message": "Content-Type must be one of: application/json, multipart/form-data",
"code": "UNSUPPORTED_MEDIA_TYPE",
"timestamp": "2026-11-02T10:15:20.000Z",
"requestId": "5d0c7e2b-1f4a-4b8e-9c3d-2a6f8e1b7c90"
}
StatuscodeCause
400BAD_REQUESTThe body is not valid JSON or is larger than 10 MB.
415UNSUPPORTED_MEDIA_TYPEA POST, PUT or PATCH without Content-Type: application/json. Send the header also when there is no body.
504TIMEOUTWe stopped waiting for the endpoint, after 30 seconds for most endpoints. The endpoint may still be running. It can still finish the action. Read the resource before you send a change again.

Two errors have no JSON body at all:

StatusBodyCause
404An HTML pageThe path does not exist. Check it for typos. A path that exists with an unknown ID returns 404 not_found as Problem Details.
405Plain text, such as POST method not allowedThe endpoint does not take this method. The Allow header lists the methods it takes.

Check Content-Type before you parse an error body.

GET /api/jobs/{jobId} uses the upper-case shape for all its errors. Jobs lists them.

A read (GET) is always safe to send again. A change is safe to send again only when you know the first attempt changed nothing.

StatusRetry?What to do
429Depends on codeMost clear after a wait. Codes with 429 says what to do for each one.
500Reads, yes. Changes, check first.Something failed on our side. Retry a read a few times with a growing pause. If it keeps failing, contact support with the requestId. Today most endpoints also answer 500 internal_error when the key is over its rate limit. If 500s come in a row after a burst, wait five minutes.
502, 503Reads, yes. Changes, check first.A service the request depends on failed or is busy. Wait as Retry-After says when the header is there. Otherwise retry with a growing pause.
504Check firstWe stopped waiting. The action may still finish in the background.
409Only when recovery says soThe resource is in a state that blocks the action. Read its state before you decide. vps_not_ready comes with retry_after while a new VPS is being set up.
Other 4xxNoThe same request fails the same way. Fix it first.

“Check first” means: read the resource before you send the change again. Did the server restart? Is the DNS record there? For a VPS, GET /api/v2/vps/{id}/active-jobs shows whether the action is already running.

  • rate_limit_exceeded: you reached one of the rate limits. Wait the number of seconds in the Retry-After header. Then send the request again. On POST /api/v2/orders its detail is in Swedish today.
  • rate_limited: a limit inside one endpoint. Wait as Retry-After says. Some of these responses do not have the header. Without it, wait until recovery.retryAfterAt when it is set. Otherwise retry with a growing pause.
  • cooldown_active: the same action ran a short while ago, for example a reinstall of the VPS. Do not send it again blindly. A retry after the wait runs the action a second time. Read the resource first, as await_cooldown above says.
  • unblock_limit_reached: a pause on the same mail sender was lifted 3 times in 7 days. Waiting a few seconds does not clear it. Contact support to lift it again.

A growing pause means waiting longer after each failure, for example 1, 2, 4 and 8 seconds. Stop after a handful of tries. For reads, curl --retry does this for you and follows Retry-After:

GET /api/v2/vps
curl --retry 4 "https://cloud.hostup.se/api/v2/vps" \
-H "Authorization: Bearer $HOSTUP_API_KEY"

--retry also retries a 500. Today that includes the 500 a key gets over its rate limit. Pauses of a few seconds do not outlast that five-minute window. When the retries all fail, stop and wait as Rate limits says.

Do not use --retry on a POST, PUT, PATCH or DELETE.

Most changes have no retry key. Some actions hand back the job that is already running when you send them a second time. Others run again. After a timeout or a lost response, check what happened before you send the change again.

POST /api/v2/orders is the exception. It takes an attemptKey in the body:

  • Make a new random value for every new order, for example a UUID. Never use a counter or a fixed text such as order-1. Such a key can match an earlier order. Then no order is placed.
  • Send the same value again only when you retry that same order after a timeout or a lost response.
  • If the same attemptKey comes back within an hour of the order it placed, no second order is placed. The response is 200 instead of 201, with the order the first request created.
  • A repeat sent while the first request is still running waits for it. If that takes too long, the repeat ends in 504 TIMEOUT. Send it again a little later with the same attemptKey.

Error codes has one page per code: what it means, which endpoints return it and what to do about it.

Next: Rate limits. How many requests a key may send and what happens over the limit.