# Totalum App Builder API — Export & Import

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": … }`.


---

# Export & Import

> Clone a project, or hand it to another account.

Export returns a secret `importCode`; whoever holds it can import the project into a new one (on any account). Use it to duplicate a project, to start projects from a template, or to give a project to a client.

**What travels:** the current code, the database schema and migrations, the live data with `includeRecords`, the uploaded files with `includeFiles`, and the scheduled jobs. **What never travels:** secrets, and the app's users and sign-in data (users, passwords, sessions) — their tables arrive empty, so everyone signs up again on the copy; with `includeRecords`, rows owned by a user follow that relation's delete rule.

**Clone:** export the source → [Create Project](https://www.totalum.app/docs/api/projects#create-project) with the new project's `projectId` and `"sourceImportCode": "<code>"` → [Import Project](#import-project) into it → poll [Get Project](https://www.totalum.app/docs/api/projects#get-project) until `importInProgress` is `null`.

**Choosing the new project's id:** an import keeps the id of the project it goes into, so the `projectId` you create it with is the imported project's id (legacy or new-engine code alike). A [migration](#migrate-to-the-new-engine) takes it as `projectId` in its body.

## Export Project

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/export
cost: 4 credits
```

Create an export and get its code. At most 5 exports an hour per project. The records exported are the **live** database's (a project never published exports none).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project to export |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `includeRecords` | boolean | No | Also export the live data (default `false`: schema only) |
| `includeFiles` | boolean | No | Also export the uploaded files (default `true`) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.importCode` | string | Secret: anyone with it can import the project |
| `data.includeRecords` | boolean | Data was included |
| `data.includeFiles` | boolean | Files were included |
| `data.files` | object \| null | `{ count, bytes }` of the files exported |
| `data.message` | string | Confirmation |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"includeRecords":true}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/export
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "importCode": "barber-bookings-export-project-8f3b1c9a47e2d650b9114af0c7e3a2d1", "includeRecords": true, "message": "Export ready. Save the importCode (keep it secret) and pass it to import-project as importCode.", "includeFiles": true, "files": { "count": 12, "bytes": 3145728 } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `PROJECT_EXPORT_LIMIT_REACHED` | 400 | 5 exports in the last hour |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits |

## Import Project

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/import
cost: 12 credits
```

Import an export into this project, which must be new and empty. The imported project keeps this project's id: to give it an id of your choice, create the project with that id first (with `sourceImportCode`, so it is made on the code's engine). The data fills both the development and the live database; then the code is built. Runs in the background (a few minutes): poll [Get Project](https://www.totalum.app/docs/api/projects#get-project) until `importInProgress` is `null`. Prompts are refused (`IMPORT_IN_PROGRESS`) meanwhile.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The target project; the imported project keeps this id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `importCode` | string | Yes | The code from [Export Project](#export-project) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The target project |
| `data.status` | string | `"importing"` |
| `data.message` | string | How to follow it |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"importCode":"barber-bookings-export-project-8f3b1c9a47e2d650b9114af0c7e3a2d1"}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings-copy/import
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "projectId": "barber-bookings-copy", "status": "importing", "message": "Import started. Poll GET /api/v1/vcaas/projects/barber-bookings-copy every 10-15s until importInProgress is null, then until agentServerStatus='Active'. Prompting is blocked until the import finishes." } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_IMPORT_CODE` | 400 | `importCode` is missing |
| `INVALID_IMPORT_CODE` | 400 | Not an import code, or the export no longer exists |
| `PROJECT_NOT_IMPORTABLE` | 400 | The target already has content; use a new project |
| `ENGINE_MISMATCH` | 409 | The code comes from a project on the [legacy engine](https://www.totalum.app/docs/api/migration-v2) and can only be imported into a legacy project |
| `IMPORT_IN_PROGRESS` | 409 | An import is already running |
| `AGENT_RUNNING` | 409 | A prompt is running |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits |

## Migrate to the New Engine

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

Move a project built on the legacy engine to the new engine. It makes a **new** project, `<projectId>v2` (with a few random letters added when that name is taken) or the id you pass as `projectId` in the body, and leaves the legacy project exactly as it is: it keeps working and serving its users until you switch. Needs the **Starter** plan or above.

What the new project gets:

- **The data**: the legacy database (its tables, fields and every record) is kept as a private file of the new project, `legacy-database.json`, and every uploaded file the records point at (a file field, or a storage link kept as text) is copied into the new project's storage (the records point at the copies).
- **The secrets**: the project's own secrets are copied with the same development and live values, stored encrypted on the new project before its first prompt, so the app reads them as before (`process.env.NAME` on the server). Their values never appear in the response or the agent's prompt; the response lists their names. The legacy Totalum API key and `BETTER_AUTH_SECRET` are not copied: the new project has its own.
- **The code**: the AI agent receives the legacy code, the legacy database structure and the legacy SDK reference, and rebuilds the same app on the new engine (pages, flows, design, sign-in; its users keep their accounts). It designs the SQL tables and moves every record into them with a data migration generated by code from `legacy-database.json`: on the development database during the run, on the live database when you first [publish](https://www.totalum.app/docs/api/deployments#deploy-to-production) the new project. The databases hold only the app's own tables; `legacy-database.json` stays in the project's files.

It answers once the new project exists and its first prompt has started (this call can take a minute). Follow the agent with [Get Agent Status](https://www.totalum.app/docs/api/agent#get-agent-status) on the **new** `projectId`. The 50 credits are charged once; the agent's work is then billed like any prompt. If the migration cannot start, nothing is charged and no project is kept. Custom domains, GitHub and Figma connections do not move: set them again on the new project. A secret the new engine cannot take (a name it reserves, such as one starting with `TOTALUM_`, or an empty value) is listed in `secretsNotCopied`: set it again under another name.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The legacy project to migrate |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `projectId` | string | No | The new project's id, with the rules of [Create Project](https://www.totalum.app/docs/api/projects#create-project). A taken id is refused (`PROJECT_ALREADY_EXISTS`), never changed. Default `<projectId>v2` |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The new project |
| `data.sourceProjectId` | string | The legacy project (unchanged) |
| `data.engineVersion` | string | `"v2"` |
| `data.creditsCharged` | number | Credits charged for the migration |
| `data.migrated` | object | `{ tables, records, files, filesNotCopied, codeFiles, codeFilesSkipped, secrets, secretsNotCopied }`; `secrets` and `secretsNotCopied` are secret names |
| `data.agent` | object | The first prompt: `{ started, status, runId, expectedMinutes, message }` |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"barber-bookings-new"}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/migrate-to-v2
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "projectId": "barber-bookings-new", "sourceProjectId": "barber-bookings", "engineVersion": "v2", "creditsCharged": 50, "migrated": { "tables": 6, "records": 412, "files": 38, "filesNotCopied": 0, "codeFiles": 126, "codeFilesSkipped": 0, "secrets": ["STRIPE_SECRET_KEY", "RESEND_API_KEY"], "secretsNotCopied": [] }, "agent": { "started": true, "status": "init", "runId": "run_01m4b8w0d9", "expectedMinutes": 18, "message": "Process started. …" } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `PLAN_REQUIRED` | 403 | The account is below the Starter plan (`errorDetails.upgradePlan`) |
| `INVALID_PROJECT_NAME` | 400 | The body's `projectId` breaks the id rules (also `INVALID_PROJECT_NAME_LENGTH`, `RESERVED_PROJECT_NAME`) |
| `PROJECT_ALREADY_EXISTS` | 409 | The body's `projectId` is taken |
| `INSUFFICIENT_CREDITS` | 402 | Fewer than 50 credits |
| `ENGINE_MISMATCH` | 409 | The project is already on the new engine |
| `PROJECT_NOT_FOUND` | 404 | No legacy project with this id on your account |
| `MAX_PROJECTS_REACHED` | 403 | Your plan's project limit is reached (the migration makes a new project) |
| `MIGRATION_TOO_LARGE` | 413 | Over 20,000 records, 20 MB of data, 20 MB of code or 500 files: contact support |
| `RATE_LIMITED` | 429 | This project is already being migrated |
| `MIGRATION_START_FAILED` | 502 | The migration could not start; nothing was charged, try again |
