Jobs
Checked against the API code 3 November 2026
Some actions take longer than one request should wait: restarts, reinstalls, backups, restores and bulk domain changes. They answer at once with 202 Accepted and a job. The job runs in the background. You poll it until it ends.
Before you start
Section titled “Before you start”The examples on this page restart a VPS with POST /api/v2/vps/{id}/actions/restart. Running them restarts that server.
- An API key with the scope
power:vm. It also coversread:vm, which Jobs on one VPS needs. The requests use the key as$HOSTUP_API_KEY(Quickstart). Authentication and scopes - The ID of one of your VPS,
vps_…. curlandjqfor the polling loop.
export VPS_ID="vps_…" # your VPS: List VPS instancesStart the action
Section titled “Start the action”curl -X POST "https://cloud.hostup.se/api/v2/vps/$VPS_ID/actions/restart" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ -H "Content-Type: application/json"{ "operation": { "status": "queued", "jobId": "job_01hxa3b4c5d6e7f8g9h0j1k2m3", "pollUrl": "/api/jobs/job_01hxa3b4c5d6e7f8g9h0j1k2m3" }}export JOB_ID="job_…" # operation.jobId abovejobIdnames the job. Most job IDs start withjob_. Bulk domain changes returndbj_IDs.pollUrlis the path to poll. Puthttps://cloud.hostup.sein front of it. For most actions it is the job endpoint below. Jobs polled at another endpoint lists the others.
When pollUrl is null, there is nothing to poll. Most often the action has already ended. A paid IP change through Change the network setup of a VPS waits for its invoice instead. pendingChange in that response names the invoice to pay and the new address.
Use pollUrl and jobId exactly as you got them. Never build a job ID from a VPS, domain or order ID.
Poll the job
Section titled “Poll the job”GET /api/jobs/{jobId} returns the job. It needs no particular scope. The key must belong to the account that started the job.
curl "https://cloud.hostup.se/api/jobs/$JOB_ID" \ -H "Authorization: Bearer $HOSTUP_API_KEY"{ "success": true, "timestamp": "2026-11-02T10:15:20.000Z", "requestId": "3f1c2a9e-8d4b-4c7a-9e21-5b6f0d2c7a10", "data": { "jobId": "job_01hxa3b4c5d6e7f8g9h0j1k2m3", "queueType": "vm", "type": "restart", "state": "active", "status": "running", "progress": 70, "progressText": "Restarting VM...", "statusKey": "instance.jobs.restarting_vm", "progressDetailText": null, "progressDetails": null, "result": null, "error": null, "isCompleted": false, "isFailed": false, "isRunning": true, "isWaiting": false, "createdAt": "2026-11-02T10:15:02.000Z", "finishedAt": null }}This endpoint is older than v2. The job sits inside data, next to success, timestamp and requestId. Its errors use a plain JSON shape, not Problem Details. See Errors from the job endpoint.
The fields you use:
| Field | What it tells you |
|---|---|
data.status | waiting, running, completed or failed. Branch on this one. |
data.progress | How far the job has come, from 0 to 100. |
data.progressText | A short English line about the current step. Show it to a person. Do not parse it. |
data.statusKey | A stable key for the same step, for clients that show their own translated text. |
data.progressDetails | Progress details when the job reports them. Bulk domain changes give current, total and currentDomain. Orders and bulk renewals give items, one row per line. null when the job reports none. |
data.result | What the job returned. null until it has finished. The shape depends on data.type. |
data.error | A short message when the job failed. A job that is retrying a step can also show Job reported an error. here. So can a job that completed after a retry. Decide on data.status, not on this field. |
data.createdAt, data.finishedAt | When the job was created and when it ended. finishedAt is null while it runs. |
data.state | A lower-level state, such as delayed or active. Branch on data.status instead. |
The 202 says pending, queued or in_progress. The job endpoint says waiting or running. Both end in completed or failed.
A job can go back from running to waiting. That happens when a step is retried. It also happens when a VPS start waits for free capacity. Keep polling until the status is completed or failed.
When the job ends
Section titled “When the job ends”completed and failed are the only final states. Stop polling when you see one of them.
Completed. data.result holds what the job returned. A job that finished with a warning is still completed. The warning is in data.result.warning. data.statusKey is then instance.jobs.completed_with_warnings.
Failed. data.result is null. data.error is a short message, most often Job failed. An ISO download says what was wrong with the link instead. data.progressText explains the failure when we know more, for example when a VPS could not start for lack of capacity. Show progressText to the person who started the action.
{ "status": "failed", "progressText": "Operation failed", "error": "Job failed.", "result": null, "finishedAt": "2026-11-02T10:16:01.000Z", "…": "…"}A job from an order can end as completed although the order was refused. Jobs from orders shows how to tell.
A polling loop in bash
Section titled “A polling loop in bash”Save the script below as wait-for-job.sh. It needs curl, jq and your key in $HOSTUP_API_KEY (Quickstart). Run it with the pollUrl from the 202:
chmod +x wait-for-job.sh./wait-for-job.sh "/api/jobs/$JOB_ID"It polls every 4 seconds and stops on completed or failed. It waits longer when the API is busy. When the job completes, it prints data.result. A refused order counts as a failure.
#!/usr/bin/env bash# Usage: ./wait-for-job.sh "/api/jobs/$JOB_ID"poll_url="$1"
case "$poll_url" in /api/jobs/*) ;; *) echo "Pass a pollUrl that starts with /api/jobs/."; exit 1 ;;esac
for attempt in $(seq 1 150); do # 150 polls about 4 s apart: 10 minutes code=$(curl -s -o job.json -w '%{http_code}' "https://cloud.hostup.se$poll_url" \ -H "Authorization: Bearer $HOSTUP_API_KEY")
case "$code" in 200) ;; 429|5??|000) sleep 15; continue ;; # busy, server error or no connection: retry *) echo "Polling stopped with HTTP $code:"; cat job.json; exit 1 ;; esac
status=$(jq -r '.data.status' job.json) progress=$(jq -r '.data.progress' job.json) text=$(jq -r '.data.progressText // ""' job.json) echo "$status $progress% $text"
case "$status" in completed) if [ "$(jq '.data.result.status | numbers >= 400' job.json)" = true ]; then echo "Refused:"; jq '.data.result' job.json; exit 1 fi jq '.data.result' job.json; exit 0 ;; failed) jq -r '.data.error, .data.progressText' job.json; exit 1 ;; waiting|running) ;; *) echo "No job status in the answer:"; cat job.json; exit 1 ;; esac sleep 4done
echo "The job has not ended after 10 minutes."exit 1Poll every 3 to 5 seconds. Every poll counts toward the key’s rate limit. A key may make 300 requests per five minutes. One job polled every 4 seconds uses 75 of them. Follow at most three jobs at that rate. To follow more, poll each one less often. Raise the 150 for jobs that run long, such as backups, reinstalls and new orders.
How long jobs are kept
Section titled “How long jobs are kept”A completed job is kept for at most 24 hours. It can be removed much sooner, when many newer jobs have finished since. A failed job is kept for up to 7 days. After that the job endpoint answers 404 for it. Read the outcome while you poll. Store what you need from result.
Jobs on one VPS
Section titled “Jobs on one VPS”GET /api/v2/vps/{id}/active-jobs lists the jobs that are queued or running on a server right now. Check it before you start a power, backup, reinstall or disk action. Then you do not start work that clashes. It also finds a running job whose ID you lost.
curl "https://cloud.hostup.se/api/v2/vps/$VPS_ID/active-jobs" \ -H "Authorization: Bearer $HOSTUP_API_KEY"{ "jobs": [ { "id": "job_01hxa3b4c5d6e7f8g9h0j1k2m3", "type": "backup", "status": "in_progress", "progress": 45, "…": "…" } ], "hasActiveBackup": true, "hasActiveOperation": true, "actions": { "canPerformActions": { "allowed": false, "reason": "A backup is currently running. Try again after it finishes." } }}Poll a listed job with its id at /api/jobs/{jobId}. An empty jobs list means nothing is running.
Jobs polled at another endpoint
Section titled “Jobs polled at another endpoint”Two kinds of jobs are not followed at the job endpoint. It answers 404 for their IDs.
Domain availability checks return a dcheck_… ID. Poll it with Get availability check results.
Web hosting restores return a job_… ID like other jobs. Restore a web hosting backup and Restore a database backup give a pollUrl for Get the status of a web hosting restore. That endpoint answers in another shape. status (queued, running, completed or failed) and progressPercent are at the top level, not inside data. The polling loop above does not work for these jobs. Poll their pollUrl and read status.
Jobs from orders
Section titled “Jobs from orders”POST /api/v2/orders answers at once with the order and no job. Only a body with "async": true answers 202 with a job. That job works like the others on this page. An order that restores a backup onto a new VPS is always answered at once.
A job from an order ends as completed also when the order was refused. Look at data.result:
- An order that went through has the order in
result, with itsord_ID inresult.id. - A refused order has a numeric
result.statusof 400 or higher. Itsresult.codenames the reason, for exampledomain_conflict. The rest ofresultis the same error document the order endpoint would have answered with.progressTextcan still say the order is complete.
The polling script above treats a refused order as a failure.
Errors from the job endpoint
Section titled “Errors from the job endpoint”The job endpoint answers errors as plain JSON with error, message, code, timestamp and requestId. The code values are upper case.
| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | The job ID has characters other than letters, digits, _, - and :. |
| 401 | UNAUTHORIZED | No valid API key was sent. |
| 403 | FORBIDDEN | The job belongs to another account or to a service the key cannot see. |
| 404 | NOT_FOUND | No job has this ID. A job is also gone some time after it ends. See How long jobs are kept. |
| 429 | API_KEY_RATE_LIMIT_EXCEEDED or none | Too many requests. When Retry-After is there, wait that many seconds. The key’s own limit sends no Retry-After. Wait up to five minutes. Then poll less often. |
| 500 | INTERNAL_ERROR | Something went wrong on our side. Poll again a little later. |
Guides that use jobs
Section titled “Guides that use jobs”- Restart a VPS
- Back up a VPS
- Restore a VPS from a backup
- Take a snapshot and roll back to it
- Reinstall a VPS
- Change a domain’s nameservers
- Change DNS records on many domains at once
Next: Errors and retries