Database Backups & Restore
Nothing in your database is ever lost for good.
Time Travel keeps every minute of the last 30 days of each database; backups are SQL dumps you can download; a restore point is a bookmark Totalum records before a change: at the start of every agent run (on the development database) and before every restore. A restore puts one database back in place — every write made since is lost — so it always takes a backup and a restore point first, never runs during an agent run and is never done automatically. Every long step answers 202 and runs in the background. A restore of the live database never goes back past the database changes the published site needs: a point before the publish that last changed the live database's structure answers 409 RESTORE_BEFORE_LIVE_MIGRATION with errorDetails.earliestPoint, the first point allowed. To go back further, unpublish the site first; the next publish applies those changes again.
Scheduled backups (paid plans). The live database of every project on a paid plan is backed up once a night, in the night of the region where the database lives. Nothing to set up. A night with no write since the last backup makes no new copy: schedule.lastStatus is "skipped_unchanged" and that backup stays current. Each backup is one gzip SQL dump with its SHA-256, kept by plan:
| Plan | Daily | Weekly | Monthly |
|---|---|---|---|
| Starter | 7 days | 4 weeks | – |
| Builder | 7 days | 4 weeks | 3 months |
| Professional | 7 days | 4 weeks | 6 months |
| Enterprise | 7 days | 4 weeks | 12 months |
The first backup of each week is kept as the weekly one, the first of each month as the monthly one. The backups count as storage of the project, like its files; there is no other fee. After a downgrade the older backups are trimmed to the new plan at the next run; on the free plan no new backup is made and the existing ones stay until they expire. A database of several gigabytes is backed up weekly instead of nightly (schedule.frequency is "weekly"), copied table by table while the site keeps running. The development database is not backed up on a schedule: Time Travel and the restore points cover it. A deleted project's last backup is deleted 30 days later.
#List Backups
/api/v1/vcaas/projects/:projectId/database/backupsapi-keyFreeThe project's backups, newest first, the schedule of the live database's backups and the Time-Travel window.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Response fields
| Field | Type | Description |
|---|---|---|
data.backups[].id | string | Backup ID |
data.backups[].env | string | "live" or "dev" |
data.backups[].kind | string | "daily", "weekly" or "monthly" (scheduled), "manual", or "pre_restore" (taken before a restore) |
data.backups[].status | string | "running", "complete" or "failed" (error says why) |
data.backups[].sizeBytes | number | null | Size of the stored file |
data.backups[].bookmark | string | null | The restore point taken with it |
data.backups[].createdAt | string | ISO time |
data.backups[].expiresAt | string | null | When a scheduled backup is deleted; null for the others |
data.schedule.enabled | boolean | true on a paid plan |
data.schedule.frequency | string | "daily", or "weekly" for a database of several gigabytes |
data.schedule.nextRunAt | string | null | The next run |
data.schedule.lastRunAt | string | null | The last run |
data.schedule.lastStatus | string | null | "complete", "skipped_unchanged" (no write since lastBackupAt) or "failed" (lastError says why; the next night tries again) |
data.schedule.lastBackupAt | string | null | The newest scheduled backup |
data.schedule.retention | object | null | { daily, weekly, monthly }: days, weeks and months each kind is kept; null on the free plan |
data.timeTravel | object | { availableFrom, availableTo } — the window of Restore to a Point in Time |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backupsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "backups": [{ "id": "bkp_01k6xg", "env": "live", "kind": "daily", "status": "complete", "sizeBytes": 18233, "bookmark": "00000085-00000001-4f1c", "createdAt": "2026-10-08T02:10:00.000Z", "expiresAt": "2026-10-15T02:10:00.000Z" }], "schedule": { "enabled": true, "frequency": "daily", "nextRunAt": "2026-10-09T02:10:00.000Z", "lastRunAt": "2026-10-08T02:10:00.000Z", "lastStatus": "complete", "lastBackupAt": "2026-10-08T02:10:00.000Z", "retention": { "daily": 7, "weekly": 4, "monthly": 3 } }, "timeTravel": { "availableFrom": "2026-09-08T12:00:00.000Z", "availableTo": "2026-10-08T12:00:00.000Z" } } }Error codes
| Code | HTTP | Meaning |
|---|
#Create Backup
/api/v1/vcaas/projects/:projectId/database/backupsapi-key2 creditsDump one database now. One manual backup per 10 minutes per project. Poll List Backups until status is "complete".
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | No | "live" (default) or "dev" |
Response fields
| Field | Type | Description |
|---|---|---|
data.backupId | string | The backup being written |
data.status | string | "running" |
data.creditsCharged | number | 2 (BACKUP_CREATE_MANUAL in Get Credit Costs) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backupsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "backupId": "bkp_01k6xj", "status": "running", "creditsCharged": 2 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
BACKUP_RATE_LIMITED | 429 | A manual backup was made less than 10 minutes ago |
DEV_DATABASE_NOT_FOUND | 404 | env is "dev" and the project has no development database |
#Download Backup
/api/v1/vcaas/projects/:projectId/database/backups/:backupId/downloadapi-keyFreeA 15-minute link to a complete backup as standard SQLite SQL (every row, including password hashes and sessions; uploaded files are not included). A scheduled backup is gzip-compressed (.sql.gz).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
backupId | string | From List Backups |
Response fields
| Field | Type | Description |
|---|---|---|
data.url | string | Signed download URL (15 minutes) |
data.fileName | string | Suggested file name |
data.sizeBytes | number | Size of the file |
data.sha256 | string | SHA-256 of the file |
data.importRecipe | string | sqlite3 my-project.sqlite < <fileName>, or gunzip -c <fileName> | sqlite3 my-project.sqlite for a .sql.gz |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj/downloadconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj/download", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "url": "https://files.totalum.app/backups/…", "fileName": "barber-bookings-live-2026-10-08.sql", "sizeBytes": 18233, "sha256": "3b1f…", "importRecipe": "sqlite3 my-project.sqlite < barber-bookings-live-2026-10-08.sql" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
BACKUP_NOT_FOUND | 404 | No such backup, or it has no complete dump |
#Restore Backup
/api/v1/vcaas/projects/:projectId/database/backups/:backupId/restoreapi-key4 creditsPut a database back to one of its backups (scheduled, manual or pre-restore). A backup of the current state is taken first; then the database is emptied, the backup is loaded and its tables, row counts and migration history are checked against the backup. If the load fails, the database goes back to the state before the restore. Every write made since the backup is lost. Poll GET /projects/:projectId/state until the operation ends.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
backupId | string | From List Backups, with status "complete" |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
confirm | string | Yes | The project ID, typed to confirm |
Response fields
| Field | Type | Description |
|---|---|---|
data.status | string | "restoring" |
data.env | string | The database restored (the backup's own) |
data.restoredTo | string | backup:<backupId> (on a database moved to Turso: turso:<ISO time of the backup>) |
data.previousBookmark | string | The point taken just before — restoring to it undoes this restore |
data.preRestoreBackupId | string | The backup taken first; the restore runs only once it is complete |
data.operation | object | { kind: "backupRestore", id } — progress in GET /projects/:projectId/state |
data.creditsCharged | number | 4 (BACKUP_RESTORE in Get Credit Costs) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"confirm":"barber-bookings"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj/restoreconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj/restore", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"confirm": "barber-bookings"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "env": "live", "status": "restoring", "restoredTo": "backup:bkp_01k6xj", "previousBookmark": "00000091-00000002-77aa", "preRestoreBackupId": "bkp_01k6xk", "operation": { "kind": "backupRestore", "id": "op_01k6xm" }, "creditsCharged": 4 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CONFIRMATION_REQUIRED | 400 | confirm is not the project ID |
VALIDATION_ERROR | 400 | On a database moved to Turso, the backup is from before the move (errorDetails.availableFrom): download it, or restore a point in time from availableFrom on. Nothing is charged |
BACKUP_NOT_FOUND | 404 | No such backup, or it has no complete dump |
RESTORE_BEFORE_LIVE_MIGRATION | 409 | The live backup is older than the database changes the published site needs; unpublish first |
BACKUP_TOO_LARGE_FOR_IMPORT | 409 | The backup is too large to load in place: download it, or use Restore to a Point in Time |
AGENT_RUNNING | 409 | The agent is running |
OPERATION_IN_PROGRESS | 409 | Another operation is in progress |
#Delete Backup
/api/v1/vcaas/projects/:projectId/database/backups/:backupIdapi-keyFreeDelete a manual or pre-restore backup. Scheduled backups are deleted by their retention.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
backupId | string | From List Backups |
Response fields
| Field | Type | Description |
|---|---|---|
data.deleted | boolean | true |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xjconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "deleted": true } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
BACKUP_NOT_DELETABLE | 409 | Only manual and pre-restore backups can be deleted |
BACKUP_NOT_FOUND | 404 | No such backup |
#List Restore Points
/api/v1/vcaas/projects/:projectId/database/bookmarksapi-keyFreeEvery restore point, newest first. Query runId to see one agent run's.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Response fields
| Field | Type | Description |
|---|---|---|
data.databaseCase | string | "dev+live": the agent works on the development database |
data.items[].env | string | The database |
data.items[].bookmark | string | Send it to Restore to a Restore Point |
data.items[].at | string | ISO time it was taken |
data.items[].kind | string | "run" or "pre_restore" |
data.items[].runId | string | undefined | The agent run that took it |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/bookmarksconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/bookmarks", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "databaseCase": "dev+live", "items": [{ "env": "dev", "bookmark": "00000085-00000001-4f1c", "at": "2026-10-08T11:10:02.000Z", "kind": "run", "runId": "run_01k6xc4n7d" }] } }Error codes
| Code | HTTP | Meaning |
|---|
#Restore to a Restore Point
/api/v1/vcaas/projects/:projectId/database/restoreapi-keyFreePut one database back to a restore point from List Restore Points — for example the moment before the agent changed it. Free: it undoes an agent step.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
bookmark | string | Yes | From List Restore Points |
env | string | No | "live" (default) or "dev" |
confirm | string | Yes | The project ID, typed to confirm |
Response fields
| Field | Type | Description |
|---|---|---|
data.status | string | "restoring" |
data.env | string | The environment restored |
data.restoredTo | string | The Time-Travel bookmark restored to |
data.previousBookmark | string | The point taken just before — restoring to it undoes this restore |
data.preRestoreBackupId | string | The backup taken first; the restore runs only once it is complete |
data.operation | object | { kind: "backupRestore", id } — progress in GET /projects/:projectId/state |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"bookmark":"00000085-00000001-4f1c","env":"dev","confirm":"barber-bookings"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/restoreconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/restore", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"bookmark": "00000085-00000001-4f1c",
"env": "dev",
"confirm": "barber-bookings"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "env": "dev", "status": "restoring", "restoredTo": "00000085-00000001-4f1c", "previousBookmark": "00000091-00000002-77aa", "preRestoreBackupId": "bkp_01k6xn", "operation": { "kind": "backupRestore", "id": "op_01k6xp" } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
BOOKMARK_NOT_FOUND | 404 | The bookmark is not a restore point of this database |
RESTORE_BEFORE_LIVE_MIGRATION | 409 | The live point lies before database changes the published site needs (errorDetails: deploymentId, migrations, earliestPoint); choose earliestPoint or later, or unpublish first |
CONFIRMATION_REQUIRED | 400 | confirm is not the project ID |
AGENT_RUNNING | 409 | An agent run is in progress |
OPERATION_IN_PROGRESS | 409 | Another operation (a restore, a publish…) is running |
DEV_DATABASE_NOT_FOUND | 404 | env is "dev" and the project has no development database |
#Restore to a Point in Time
/api/v1/vcaas/projects/:projectId/database/time-travel/restoreapi-key4 creditsPut one database back to any minute of the last 30 days (on a database moved to Turso: from the move on, timeTravel.availableFrom in List Backups).
A refused request costs nothing. A restore that is accepted and then fails gives its 4 credits back; the same holds for Restore a Backup.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
env | string | No | "live" (default) or "dev" |
timestamp | string | Yes | ISO time within the last 30 days |
confirm | string | Yes | The project ID, typed to confirm |
Response fields
| Field | Type | Description |
|---|---|---|
data.status | string | "restoring" |
data.env | string | The environment restored |
data.restoredTo | string | The Time-Travel bookmark restored to |
data.previousBookmark | string | The point taken just before — restoring to it undoes this restore |
data.preRestoreBackupId | string | The backup taken first; the restore runs only once it is complete |
data.operation | object | { kind: "backupRestore", id } — progress in GET /projects/:projectId/state |
data.creditsCharged | number | 4 (BACKUP_RESTORE in Get Credit Costs) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"env":"live","timestamp":"2026-10-08T09:00:00.000Z","confirm":"barber-bookings"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/time-travel/restoreconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/time-travel/restore", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"env": "live",
"timestamp": "2026-10-08T09:00:00.000Z",
"confirm": "barber-bookings"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "env": "live", "status": "restoring", "restoredTo": "00000080-00000003-1a2b", "previousBookmark": "00000091-00000002-77aa", "preRestoreBackupId": "bkp_01k6xq", "operation": { "kind": "backupRestore", "id": "op_01k6xr" }, "creditsCharged": 4 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | timestamp is outside the last 30 days or, on a database moved to Turso, before the move (its point-in-time history starts there; errorDetails.availableFrom, also in GET …/database/backups?env= as timeTravel.availableFrom) |
RESTORE_BEFORE_LIVE_MIGRATION | 409 | The live point lies before database changes the published site needs (errorDetails: deploymentId, migrations, earliestPoint); choose earliestPoint or later, or unpublish first |
RESTORE_VERIFY_FAILED | 409 | Time Travel resolved timestamp to a state older or newer than a restore point recorded around that time; nothing was restored (errorDetails: timestamp, resolvedBookmark, restorePoint). Restore to that recorded point instead: its bookmark with Restore to a Restore Point |
CONFIRMATION_REQUIRED | 400 | confirm is not the project ID |
AGENT_RUNNING | 409 | An agent run is in progress |
OPERATION_IN_PROGRESS | 409 | Another operation (a restore, a publish…) is running |
DEV_DATABASE_NOT_FOUND | 404 | env is "dev" and the project has no development database |
#Move to Turso
/api/v1/vcaas/projects/:projectId/database/migrate-providerapi-key50 creditsMove the project's database to Turso, (a managed Turso database, run by Totalum). One way: it cannot be moved back. The 50 credits are refunded in full when the move fails before the switch. Both databases, live and dev, move. Available on the Builder plan and above.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project ID |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | "turso" |
confirm | string | Yes | The project ID, typed to confirm |
Response fields
| Field | Type | Description |
|---|---|---|
data.operation | object | { kind: "dbProviderMigration", id, status, startedAt, to, envs, estimateSec, databaseBytes } — follow it in GET /projects/:projectId/state (below) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"to":"turso","confirm":"barber-bookings"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/migrate-providerconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/migrate-provider", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"to": "turso",
"confirm": "barber-bookings"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "operation": { "kind": "dbProviderMigration", "id": "op_01k6xs", "status": "running", "startedAt": "2026-10-08T12:00:00.000Z", "to": "turso", "envs": ["live", "dev"], "estimateSec": 240, "databaseBytes": 2457600 } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
CONFIRMATION_REQUIRED | 400 | confirm is not the project ID |
MIGRATION_NOT_REVERSIBLE | 400 | to is not "turso" |
PLAN_REQUIRED | 403 | The account's plan is below Builder |
ALREADY_ON_PROVIDER | 409 | The project is already on Turso |
INSUFFICIENT_CREDITS | 402 | Fewer than 50 credits |
AGENT_RUNNING | 409 | An agent run is in progress |
Following the move
It takes about 4 minutes per GB. While it runs, GET /api/v1/vcaas/projects/:projectId/state lists the running dbProviderMigration operation with its progress (the environments move one after the other, live first):
| Field | Type | Description |
|---|---|---|
operations[].progress.phase | string | "copying", then "verifying" (the copy is checked), then "switching" |
operations[].progress.env | string | The database moving now: "live" or "dev" |
operations[].progress.copiedBytes | number | Bytes copied so far, updated after each copied part (a few hundred MB) |
operations[].progress.totalBytes | number | The database's size |
operations[].progress.etaSec | number | About how many seconds are left for this database, from the speed measured so far |
Meanwhile the agent and code changes are refused with 409 OPERATION_IN_PROGRESS and the live site is read-only: its pages keep answering and every write gets a short maintenance answer (503). When the move ends, the project's chat says so and the owner gets an e-mail: moved, or rolled back with the 50 credits refunded (the operation then stays listed with status: "error" and errorMessage).
