# Totalum App Builder API — Database Backups & Restore

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# 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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/database/backups
cost: Free
```

The 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](#restore-to-a-point-in-time) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/backups
cost: 2 credits
```

Dump one database now. One manual backup per 10 minutes per project. Poll [List Backups](#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](https://www.totalum.app/docs/api/account#get-credit-costs)) |

**Example request**

```bash
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/backups
```

:::success Success · 202 Accepted
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/database/backups/:backupId/download
cost: Free
```

A 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj/download
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/backups/:backupId/restore
cost: 4 credits
```

Put 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](https://www.totalum.app/docs/api/account#get-credit-costs)) |

**Example request**

```bash
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/restore
```

:::success Success · 202 Accepted
```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](#restore-to-a-point-in-time) |
| `AGENT_RUNNING` | 409 | The agent is running |
| `OPERATION_IN_PROGRESS` | 409 | Another operation is in progress |

## Delete Backup

```endpoint
method: DELETE
path: /api/v1/vcaas/projects/:projectId/database/backups/:backupId
cost: Free
```

Delete 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**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/backups/bkp_01k6xj
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/database/bookmarks
cost: Free
```

Every 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/database/bookmarks
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/restore
cost: Free
```

Put 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**

```bash
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/restore
```

:::success Success · 202 Accepted
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/time-travel/restore
cost: 4 credits
```

Put 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](#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](#restore-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](https://www.totalum.app/docs/api/account#get-credit-costs)) |

**Example request**

```bash
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/restore
```

:::success Success · 202 Accepted
```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](#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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/migrate-provider
cost: 50 credits
```

Move 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**

```bash
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-provider
```

:::success Success · 202 Accepted
```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`).
