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 with the new project's projectId and "sourceImportCode": "<code>" → Import Project into it → poll 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 takes it as projectId in its body.
#Export Project
/api/v1/vcaas/projects/:projectId/exportapi-key4 creditsCreate 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
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/exportconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/export", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"includeRecords": true
}),
});
const { errors, data } = await res.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
/api/v1/vcaas/projects/:projectId/importapi-key12 creditsImport 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 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 |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The target project |
data.status | string | "importing" |
data.message | string | How to follow it |
Example request
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/importconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings-copy/import", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"importCode": "barber-bookings-export-project-8f3b1c9a47e2d650b9114af0c7e3a2d1"
}),
});
const { errors, data } = await res.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 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
/api/v1/vcaas/projects/:projectId/migrate-to-v2api-key50 creditsMove 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.NAMEon the server). Their values never appear in the response or the agent's prompt; the response lists their names. The legacy Totalum API key andBETTER_AUTH_SECRETare 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 the new project. The databases hold only the app's own tables;legacy-database.jsonstays 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 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. 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
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-v2const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/migrate-to-v2", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"projectId": "barber-bookings-new"
}),
});
const { errors, data } = await res.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 |
