Skip to content

Restore a VPS from a backup

4 steps · Checked against the API code 3 November 2026

A guide does one task from start to finish. Every endpoint, with all its fields and cases, is in the API reference.

A restore writes a backup back onto the VPS it came from. What is on the disk now is replaced. To get old files back without touching the server, restore the backup onto a new Cloud VPS instead (Restore to a new Cloud VPS instead).

  • An API key with the scope backup:vm. It covers every read on this page. A restore to a new Cloud VPS needs write:billing as well. The requests use the key as $HOSTUP_API_KEY (Quickstart). Keys and scopes
  • The ID of the VPS, vps_…. List VPS instances if you don’t have it.
  • A backup of that VPS. Back up a VPS takes one.
export VPS_ID="vps_…" # your VPS: List VPS instances

1. Pick the backup and check it can be restored

Section titled “1. Pick the backup and check it can be restored”

GET /api/v2/vps/{id}/backups lists the backups of the VPS and says whether a restore is allowed now.

GET /api/v2/vps/{id}/backups
curl "https://cloud.hostup.se/api/v2/vps/$VPS_ID/backups" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"backups": [
{
"id": "bkp_01hxa3b4c5d6e7f8g9h0j1k2m3",
"createdAt": "2026-11-02T10:24:00.000Z",
"backupType": "manual",
"note": "Before the PHP 8.4 upgrade",
"actions": {
"canRestore": { "allowed": true, "reason": null }
},
"…": "…"
}
],
"actions": {
"canRestore": { "allowed": true, "reason": null },
"canRestoreToNewCloudVm": { "allowed": true, "reason": null },
"…": "…"
},
"…": "…"
}

Two gates must both say allowed: true:

  • actions.canRestore for the VPS. It is false while the VPS isn’t active or another restore is running. reason says which.
  • backups[].actions.canRestore for the backup itself. A backup that holds only the server’s settings and no disk has allowed: false and the reason “This backup contains configuration only, with no server disk data. Choose another backup.”

When listing.status is unavailable, the list is empty because the backups could not be read right then. They are still there. Try again in a moment.

Keep the id of the backup you restore. The next requests use it.

export BACKUP_ID="bkp_…" # backups[].id of the backup to restore

POST /api/v2/vps/{id}/backups/{backupId}/actions/restore takes no body. The path picks the backup.

POST /api/v2/vps/{id}/backups/{backupId}/actions/restore
curl -X POST \
"https://cloud.hostup.se/api/v2/vps/$VPS_ID/backups/$BACKUP_ID/actions/restore" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json"
Response 202
{
"operation": {
"status": "queued",
"jobId": "job_01hxc4d5e6f7g8h9j0k1m2n3p4",
"pollUrl": "/api/jobs/job_01hxc4d5e6f7g8h9j0k1m2n3p4"
}
}
export JOB_ID="job_…" # operation.jobId above

Poll the job until data.status is completed or failed. While it runs, it is waiting or running.

GET /api/jobs/{jobId}
curl "https://cloud.hostup.se/api/jobs/$JOB_ID" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"success": true,
"data": {
"jobId": "job_01hxc4d5e6f7g8h9j0k1m2n3p4",
"type": "restore",
"status": "completed",
"progress": 100,
"…": "…"
},
"…": "…"
}

To poll until it ends, run the script from Jobs:

./wait-for-job.sh "/api/jobs/$JOB_ID"

If it ends failed, data.progressText says why when it is known. Check the server in step 4 either way.

A completed job can carry a warning in data.result.warning (Jobs). Log it. Read only message and warning from data.result. Its other fields are technical and can change.

GET /api/v2/vps/{id}/status shows what the server is doing now.

GET /api/v2/vps/{id}/status
curl "https://cloud.hostup.se/api/v2/vps/$VPS_ID/status" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"id": "vps_01hxa3b4c5d6e7f8g9h0j1k2m3",
"powerState": "running",
"uptimeSeconds": 35,
"…": "…"
}

A small uptimeSeconds confirms the fresh start. Your own services may need a little longer to come up than the server itself.

On a Cloud VPS that is on hold for an overdue invoice, the job fails before it stops the VPS. Pay the invoice first. Then restore again. Any other failure can leave the VPS stopped.

The restore did not start the server again. The job stops a running VPS before it writes the disk. It does not start it again after a failure. A completed restore whose start did not succeed also leaves it stopped. Its data.result.warning then says so. Start the VPS with Start a VPS and wait for that job the same way. If the server does not hold the data you expect, run the restore again.

Instead of overwriting the VPS, the API orders a new Cloud VPS on your account and writes the backup onto it. The new server gets its own IP addresses. The VPS you restore from stays as it is. Use this to get old files back or to test a backup.

List the backups as in step 1. actions.canRestoreToNewCloudVm.allowed must be true. It is false while a restore is already running. It is also false when the key lacks backup:vm or write:billing and when the account can’t use Cloud VPS yet. reason says which. Typical account blockers are a missing card and an account that isn’t verified.

backups[].actions.canRestore must be true for the backup you pick, as in step 1. Keep its id.

export BACKUP_ID="bkp_…" # backups[].id of the backup to restore

POST /api/v2/vps/{id}/backups/{backupId}/actions/restore-to-payg takes an optional body. Every field has a default:

  • name: the hostname of the new server, 3 to 63 characters. Use letters, digits and hyphens only. Start and end with a letter or digit. A name with a dot passes this request. The job then fails before it orders anything. Without name the API builds one from the VPS name and the end of the backup ID, such as restore-web-01-j1k2m3.
  • cpuCores (1 to 32) and memoryGb (1 to 256): the same as the VPS when left out. A higher number is lowered to the maximum.
  • storageGb: at least 50 and at least what the backup needs, up to 2,000. Without it the API picks the size the backup needs.

Any other field answers 400 invalid_request.

This call orders a new Cloud VPS. It is billed by usage like any Cloud VPS. Get Cloud VPS account limits has the hourly rates in billing. It also shows how much of your Cloud VPS limits is left.

POST /api/v2/vps/{id}/backups/{backupId}/actions/restore-to-payg
curl -X POST \
"https://cloud.hostup.se/api/v2/vps/$VPS_ID/backups/$BACKUP_ID/actions/restore-to-payg" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "web-01-restored"}'
Response 202
{
"operation": {
"status": "queued",
"jobId": "job_01hxd5e6f7g8h9j0k1m2n3p4q5",
"pollUrl": "/api/jobs/job_01hxd5e6f7g8h9j0k1m2n3p4q5"
}
}
export JOB_ID="job_…" # operation.jobId above

While that job runs, sending the same request again returns the same job. It does not order a second server. A job that failed after it had ordered the server is retried on that server.

The job places the order, waits until the new server is built and then writes the backup onto it. Wait for it as in step 3:

./wait-for-job.sh "/api/jobs/$JOB_ID"

When it is completed, data.result names the new server. The script prints data.result. The job endpoint shows it as well:

GET /api/jobs/{jobId}
curl "https://cloud.hostup.se/api/jobs/$JOB_ID" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"success": true,
"data": {
"jobId": "job_01hxd5e6f7g8h9j0k1m2n3p4q5",
"type": "restore_to_new_payg",
"status": "completed",
"result": {
"targetVpsId": "vps_01hxe6f7g8h9j0k1m2n3p4q5r6",
"orderId": "ord_01hxa3b4c5d6e7f8g9h0j1k2m3",
"invoiceId": "inv_01hxa3b4c5d6e7f8g9h0j1k2m3",
"…": "…"
},
"…": "…"
},
"…": "…"
}
export TARGET_VPS_ID="vps_…" # data.result.targetVpsId above

Use $TARGET_VPS_ID like any VPS ID. The status call in step 4 works with it. orderId and invoiceId point to the order behind the new server.

The new server is billed until you end it. Its disk is billed even while it is stopped. Copy what you need off it first. Then end it with Cancel a VPS on $TARGET_VPS_ID, with "cancelType": "immediate". That call needs write:billing as well.

  • A restore onto the same VPS keeps its IP addresses. It also keeps its current CPU and memory.
  • A restore onto the same VPS creates no invoice. Hostup sends the account holder no e-mail about it.
  • A restore to a new Cloud VPS places an order on your account. The new server runs until you end it.
  • 403 forbidden with requiredScopes: the key lacks the scope named there (When a key is refused).
  • 403 forbidden without requiredScopes: detail does not say what is wrong. Two things cause it:
    • The VPS has used up its bandwidth allowance. Restores stay blocked until the usage resets or the limit is raised.
    • The backup was deleted or belongs to another VPS. List the backups again and pick an id from there.
  • 404 backup_not_found: no backup has that ID. Schedules delete old backups. Read the list right before you restore.
  • 409 upstream_failed: the backup holds only the server’s settings and no disk. The answer does not say why. backups[].actions.canRestore in step 1 shows it. Pick another backup.
  • 403 with a code such as no_card_on_file or not_verified: the account can’t use Cloud VPS yet. detail says what to do.
  • 403 payg_payment_overdue: a Cloud VPS invoice is overdue. canRestoreToNewCloudVm does not show this. Pay the invoice first (Pay an invoice).
  • 404 backup_not_found: no backup has that ID. This call also answers it for a backup that was deleted or belongs to another VPS.
  • 409 target_storage_too_small: storageGb is below what the backup needs. requiredStorageGb in the answer has the size to send. Leave storageGb out to get it.
  • 409 target_storage_too_large: storageGb is above 2,000. A backup that needs more than that gets this answer too. maxStorageGb in the answer has the limit.
  • 409 backup_has_no_disk_images: the backup holds no disk data. Pick another backup.
  • 409 payg_limit_exceeded: the new server would go past your Cloud VPS limits. The answer shows requested, current and max. Ask for more with Request a higher Cloud VPS limit.

Next: Take a snapshot and roll back to it