Migrate
Move a server from another provider onto a VPS with one command, or copy a restore point onto another of your VPS. Free.
Migrate brings files onto one of your VPS from somewhere else:
- Import ("Migrate to Helipod"): a Linux server outside Helipod runs a one-time command that uploads it, encrypted. Helipod checks the upload (operating system, size, file count), then you restore folders or the whole server onto the VPS.
- Copy: a restore point of one of your VPS (see Backups) is restored onto another of your VPS.
Migrations are free. An import is kept for 7 days, so it can be restored more than once, then it is deleted. The same features are in the dashboard's Migrate tab.
The target VPS must be running, a Linux server, and in your key's scope; for a copy, the source VPS must be too. One import per VPS at a time, at most 3 imports per account, and an upload may not be larger than the target's disk.
How an import works
- Start an import on the target VPS. The answer has a one-time command:
curl -fsSL https://api.helipod.io/migrate/… | sudo bash. - Run it as root on the old server. It works once and expires in 1 hour. With
sourceIpit only works from that IP. It uploads in the background, so the terminal can be closed. - Follow the status:
waiting→uploading→inspecting→uploaded. While it uploads you can pause, resume or stop it. - Check
fetchedIp(who ran the command), then restore it withconfirmSource: truethe first time.
The command is shown once, in the answer that creates it. Lost or expired: get a new command.
List migrations
/vps/:id/migrationscurl https://api.helipod.io/api/vps/1f757fd5-98d1-490a-81a0-58b5798a5815/migrations \
-H "Authorization: Bearer hel_your_key"{
"success": true,
"data": [
{
"id": "2746f3c3-ae6f-4944-9d46-a8fb5e34221a",
"kind": "import",
"status": "uploaded",
"targetVpsId": "1f757fd5-98d1-490a-81a0-58b5798a5815",
"sourcePointId": null,
"sourceOs": "ubuntu-24.04",
"sourceBytes": 3308296246,
"mode": null,
"paths": null,
"restoreTarget": null,
"commandExpiresAt": null,
"sourceIp": "203.0.113.10",
"fetchedIp": "203.0.113.10",
"fetchedAt": "2026-10-21T00:03:31.000Z",
"fetchedAgent": "curl/8.5.0",
"sourceConfirmedAt": null,
"result": { "hostname": "old-server", "files": 25238 },
"error": null,
"expiresAt": "2026-10-28T00:02:11.000Z",
"createdAt": "2026-10-21T00:02:11.000Z",
"updatedAt": "2026-10-21T00:09:02.000Z"
}
]
}| Field | Description |
|---|---|
kind | import (from a server outside Helipod) or copy (from another VPS's restore point). |
status | waiting (command not run yet), uploading, inspecting (checking the upload), uploaded (ready to restore), awaiting_command, restoring, restored, failed, expired, cancelled. |
sourceOs | The uploaded server's OS, e.g. ubuntu-24.04, debian-12 (import), or the source VPS's (copy). |
sourceBytes | Size of the upload (import). |
mode, paths, restoreTarget | The last restore asked for: files or full, the paths, folder or inplace. |
commandExpiresAt | Set while a one-time command can still be fetched. |
sourceIp | The IP the upload command is locked to, if you gave one. |
fetchedIp, fetchedAt, fetchedAgent | Who ran the command, as Helipod saw it. Check it before restoring. |
sourceConfirmedAt | When the upload was confirmed as yours (needed once before restoring an import). |
result | hostname and files of the uploaded server; after a restore target (where the files went), restored, bytes, restoredAt; while a job runs progress, stored and control (below); refusedFetch when someone tried the command from another IP. |
error | Why it failed, in plain words. |
expiresAt | When the import is deleted. |
While an upload or a command-run restore runs, result also has:
| Field | Description |
|---|---|
progress.percent | Percentage of the data read (an upload then finishes sending the last of it). |
progress.bytesDone, progress.bytesTotal | Bytes read (upload) or restored, out of the total. |
progress.sentBytes, progress.sendRate | Bytes the server sent since the upload began, and its current speed (bytes per second). |
progress.secondsLeft | Estimate, when there is a sensible one. |
stored.bytes | What has arrived in Helipod storage so far (checked every 2 minutes). |
control.want, control.state, control.at | Pause state: what is wanted (run, pause, stop), what the job last reported (running, paused, stopping) and when it last checked in (every ~5 s). |
Get a migration
/vps/:id/migrations/:migrationIdPoll it every 15 to 30 seconds while it runs.
Start an import
/vps/:id/migrations/importcurl -X POST https://api.helipod.io/api/vps/1f757fd5-98d1-490a-81a0-58b5798a5815/migrations/import \
-H "Authorization: Bearer hel_your_key" \
-H "Content-Type: application/json" \
-d '{ "sourceIp": "203.0.113.10" }'| Body field | Description |
|---|---|
sourceIp | Optional, recommended. The old server's public IP (on that server: curl -4 ifconfig.me). The command then only works from there; a fetch from anywhere else is refused, does not use it up, and shows as result.refusedFetch. |
{
"success": true,
"data": {
"migration": { "id": "2746f3c3-ae6f-4944-9d46-a8fb5e34221a", "kind": "import", "status": "waiting", "commandExpiresAt": "2026-10-21T01:02:11.000Z", "sourceIp": "203.0.113.10" },
"command": "curl -fsSL https://api.helipod.io/migrate/Xr3…kQ | sudo bash"
}
}Run command as root on the old server. It needs bash and curl (it installs curl and bzip2 itself when missing), uploads the main disk (/) except system, temporary and cache folders, and dumps running MySQL, MariaDB and PostgreSQL databases into /var/backups/helipod-db first. Nothing stays installed on the server afterwards.
New command
/vps/:id/migrations/:migrationId/commandThe previous command stops working, and a job still running with it is told to stop. The answer has the same shape as Start an import.
Pause, resume or stop
/vps/:id/migrations/:migrationId/controlcurl -X POST https://api.helipod.io/api/vps/1f757fd5-98d1-490a-81a0-58b5798a5815/migrations/2746f3c3-ae6f-4944-9d46-a8fb5e34221a/control \
-H "Authorization: Bearer hel_your_key" \
-H "Content-Type: application/json" \
-d '{ "action": "pause" }'action is pause, resume or stop. The job follows within a few seconds; result.control.state shows when it has. Stop is final: the migration becomes failed, and a new command starts the upload again, reusing what was already uploaded where possible. On the server itself the same works with sudo helipod-migrate pause | resume | stop (and helipod-restore for a restore), and both stay in sync.
Restore an import
/vps/:id/migrations/:migrationId/restorecurl -X POST https://api.helipod.io/api/vps/1f757fd5-98d1-490a-81a0-58b5798a5815/migrations/2746f3c3-ae6f-4944-9d46-a8fb5e34221a/restore \
-H "Authorization: Bearer hel_your_key" \
-H "Content-Type: application/json" \
-d '{ "mode": "files", "paths": ["/var/www", "/home", "/var/backups/helipod-db"], "target": "folder", "confirmSource": true }'| Body field | Description |
|---|---|
mode | files: the given paths. full: the whole VPS becomes a copy of the old server (everything on it is replaced). |
paths | files only: 1 to 50 absolute paths. |
target | files only: folder (default) restores into /root/restore-<date>; inplace overwrites files at the same paths. |
confirmSource | true confirms the upload came from your server. Needed for the first restore of an import: check fetchedIp first, and delete the import instead if you do not recognise it. |
full needs a VPS of the full-VM kind (providerCode 2) running the same OS and version as the upload (sourceOs); otherwise restore folders. The restore runs in the background; follow it with Get a migration. An import can be restored again until it expires.
Copy to another VPS
/vps/:id/backup/points/:pointId/copycurl -X POST https://api.helipod.io/api/vps/1f757fd5-98d1-490a-81a0-58b5798a5815/backup/points/5d0e…/copy \
-H "Authorization: Bearer hel_your_key" \
-H "Content-Type: application/json" \
-d '{ "targetVpsId": "9a8b7c6d-…", "mode": "files", "paths": ["/var/www"], "target": "folder" }'| Body field | Description |
|---|---|
targetVpsId | The VPS to copy onto. Not the same VPS: to bring a VPS back, use restore. |
mode, paths, target | As in Restore an import. full only between two VPS of the same kind running the same OS and version. |
The copy is listed on the target VPS (List migrations with the target's id). If the answer has a command, the target's management agent could not be reached: run that one-time command as root on the target instead.
Delete a migration
/vps/:id/migrations/:migrationIdA running upload is told to stop. Not possible while a restore or the upload check runs (BUSY).
Errors
Migrate answers like the rest of the VPS API (Errors): a VPS or migration that does not exist or is outside your key's scope is 404; a business rule is 400 with one of these codes:
| Code | Meaning |
|---|---|
IMPORT_EXISTS | This VPS already has an import: open it or delete it first. |
TOO_MANY_IMPORTS | The account already has 3 imports. |
BAD_IP | sourceIp is not an IP address. |
VPS_NOT_RUNNING, VPS_EXPIRED, NOT_ELIGIBLE | The target VPS cannot take a migration right now. |
NOT_READY | The upload has not finished yet. |
CONFIRM_SOURCE | Send confirmSource: true after checking fetchedIp. |
FULL_NOT_ALLOWED, OS_MISMATCH | A whole-server restore needs the same kind of VPS and the same OS; restore folders instead. |
SAME_VPS | A copy needs another VPS. |
NO_COMMAND | There is no command to run for this migration right now. |
NOT_CONTROLLABLE, STOPPING | Only a job started by a command, once it checked in, can be paused or stopped; or it is already stopping. |
VPS_BUSY, BUSY | Another restore onto the VPS is waiting or running, or a step is in progress. |