Docs
BlogHomeStart building

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

POST/api/v1/vcaas/projects/:projectId/exportapi-key4 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

ParameterTypeDescription
projectIdstringThe project to export

Body parameters

FieldTypeRequiredDescription
includeRecordsbooleanNoAlso export the live data (default false: schema only)
includeFilesbooleanNoAlso export the uploaded files (default true)

Response fields

FieldTypeDescription
data.importCodestringSecret: anyone with it can import the project
data.includeRecordsbooleanData was included
data.includeFilesbooleanFiles were included
data.filesobject | null{ count, bytes } of the files exported
data.messagestringConfirmation

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/export
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

CodeHTTPMeaning
PROJECT_EXPORT_LIMIT_REACHED4005 exports in the last hour
INSUFFICIENT_CREDITS402Not enough credits

#Import Project

POST/api/v1/vcaas/projects/:projectId/importapi-key12 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 until importInProgress is null. Prompts are refused (IMPORT_IN_PROGRESS) meanwhile.

Path parameters

ParameterTypeDescription
projectIdstringThe target project; the imported project keeps this id

Body parameters

FieldTypeRequiredDescription
importCodestringYesThe code from Export Project

Response fields

FieldTypeDescription
data.projectIdstringThe target project
data.statusstring"importing"
data.messagestringHow 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/import
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

CodeHTTPMeaning
MISSING_IMPORT_CODE400importCode is missing
INVALID_IMPORT_CODE400Not an import code, or the export no longer exists
PROJECT_NOT_IMPORTABLE400The target already has content; use a new project
ENGINE_MISMATCH409The code comes from a project on the legacy engine and can only be imported into a legacy project
IMPORT_IN_PROGRESS409An import is already running
AGENT_RUNNING409A prompt is running
INSUFFICIENT_CREDITS402Not enough credits

#Migrate to the New Engine

POST/api/v1/vcaas/projects/:projectId/migrate-to-v2api-key15 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). There is no limit on the number of uploaded files: the first 2,000 are copied during this call, and the rest in the background shortly after (filesPending counts them); until each one is copied, its record keeps pointing at the old file, which keeps working.
  • Many-to-many relations: the legacy engine kept these links apart from the records; every link is copied too (manyToManyLinks counts them), and the agent turns each many-to-many relation into a join table of the new database.
  • Private files stay private: the legacy engine served uploaded files only through signed, expiring links, so each one is copied as a private file of the new project (an ID card scan never gets a public URL). Its field holds { name, key, url: null, private: true }; the rebuilt app shows or downloads it through one of its own routes that checks the signed-in user and redirects to a short-lived signed URL, await totalum.files.getSignedUrl(key) (or totalum.files.presign(keys) for several; see Files). A file that was publicly reachable without a signature stays public.
  • 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 the new project. The databases hold only the app's own tables; legacy-database.json stays in the project's files.

What does not carry over, and the new engine's own rules:

  • One field value can be at most 2 MB in the new database. Images or documents stored inline as base64 text in a field are larger than that: turn them into uploaded files in the legacy app before migrating.
  • BETTER_AUTH_SECRET is not reused (the new project has its own), so anything encrypted with the old one does not move: users who had two-factor authentication must set it up again. Passwords keep working.
  • Sessions do not carry over: every user signs in again once, on the new app.
  • The new app's sign-in rules: a session lasts 7 days (renewed once a day while it is used), and on the published site 10 wrong passwords for one e-mail, or 30 from one IP address, lock that e-mail or address out for 15 minutes.

The legacy app keeps running, so its data keeps changing: until the new project's first publish, Refresh Migrated Data reads it again into legacy-database.json, for free.

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 migration fee is 15 credits, charged once. The AI rebuild that follows always runs on the top tier and is billed like a prompt: typically 60–80 credits more, depending on the app's size. If the migration cannot start, nothing is charged and no project is kept. Limits: up to 20,000 records and 20 MB of data, and up to 20 MB of code in total (a code file over 1 MB is skipped and counted in codeFilesSkipped). 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

ParameterTypeDescription
projectIdstringThe legacy project to migrate

Body parameters

FieldTypeRequiredDescription
projectIdstringNoThe new project's id, with the rules of Create Project. A taken id is refused (PROJECT_ALREADY_EXISTS), never changed. Default <projectId>v2
ownerEmailstringNoThe new app's owner e-mail (its admin once confirmed in the app), as in Launch Project. Default: none — then the first person who signs up in the app becomes its admin after entering the 8-digit code e-mailed to them (the preview and the published app each have their own first sign-up); set one later with Set App Owner. An invalid address is refused (INVALID_OWNER_EMAIL) before anything is read

Response fields

FieldTypeDescription
data.projectIdstringThe new project
data.sourceProjectIdstringThe legacy project (unchanged)
data.engineVersionstring"v2"
data.creditsChargednumberCredits charged for the migration
data.migratedobject{ tables, records, files, filesNotCopied, filesPending, manyToManyLinks, codeFiles, codeFilesSkipped, secrets, secretsNotCopied }; files = uploaded files copied during the call, filesPending = the ones past the first 2,000, copied in the background shortly after; manyToManyLinks = many-to-many links copied; secrets and secretsNotCopied are secret names
data.agentobjectThe 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-v2
Success · 200 OK
json
{ "errors": null, "data": { "projectId": "barber-bookings-new", "sourceProjectId": "barber-bookings", "engineVersion": "v2", "creditsCharged": 15, "migrated": { "tables": 6, "records": 412, "files": 38, "filesNotCopied": 0, "filesPending": 0, "manyToManyLinks": 54, "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

CodeHTTPMeaning
PLAN_REQUIRED403The account is below the Starter plan (errorDetails.upgradePlan)
INVALID_PROJECT_NAME400The body's projectId breaks the id rules (also INVALID_PROJECT_NAME_LENGTH, RESERVED_PROJECT_NAME)
PROJECT_ALREADY_EXISTS409The body's projectId is taken
INVALID_OWNER_EMAIL400The body's ownerEmail is not a valid e-mail address
INSUFFICIENT_CREDITS402Fewer than 15 credits
ENGINE_MISMATCH409The project is already on the new engine
PROJECT_NOT_FOUND404No legacy project with this id on your account
MAX_PROJECTS_REACHED403Your plan's project limit is reached (the migration makes a new project)
MIGRATION_TOO_LARGE413Over 20,000 records, 20 MB of data or 20 MB of code: contact support
RATE_LIMITED429This project is already being migrated
MIGRATION_START_FAILED502The migration could not start; nothing was charged, try again
End of Migrate to the New EngineNext endpointPOSTRefresh Migrated Data

#Refresh Migrated Data

POST/api/v1/vcaas/projects/:projectId/migrate-to-v2/refresh-dataapi-keyFree

The legacy app keeps working while its new version is being built, so its data keeps changing. Call this on the new project (the one Migrate to the New Engine made) to read the legacy project again, records, many-to-many links and files, into a fresh legacy-database.json (same file, same key). Files already copied are not copied again; new ones are, private as above (past 2,000 new files, in the background). The legacy project is only read. It costs nothing beyond the storage of the new files.

It works only before the new project's first publish: the live database is loaded from legacy-database.json after that publish, and from then on the live database is the app's data, so the call is refused (MIGRATION_ALREADY_PUBLISHED). To bring the development database up to date after a refresh, ask the agent: "Run scripts/legacy-data.ts again on dev with the refreshed legacy-database.json". The usual order: refresh, publish, then load the live data (the agent's final message tells you the exact prompt).

Path parameters

ParameterTypeDescription
projectIdstringThe new project made by the migration

Response fields

FieldTypeDescription
data.projectIdstringThe new project
data.sourceProjectIdstringThe legacy project that was read again
data.snapshotKeystringThe key of legacy-database.json (unchanged)
data.refreshedobject{ tables, records, manyToManyLinks, files, filesAlreadyCopied, filesNotCopied, filesPending }; files = new files copied now
data.messagestringWhat to send the agent next

Example request

curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings-new/migrate-to-v2/refresh-data
Success · 200 OK
json
{ "errors": null, "data": { "projectId": "barber-bookings-new", "sourceProjectId": "barber-bookings", "snapshotKey": "pv/barber-bookings-new/…/legacy-database.json", "refreshed": { "tables": 6, "records": 431, "manyToManyLinks": 57, "files": 3, "filesAlreadyCopied": 38, "filesNotCopied": 0, "filesPending": 0 }, "message": "legacy-database.json now holds the legacy project as it is now. …" } }

Error codes

CodeHTTPMeaning
MIGRATION_ALREADY_PUBLISHED409The new project has been published: its live database holds the app's data now
OPERATION_IN_PROGRESS409Files of the migration (or of the last refresh) are still being copied: try again in a few minutes
PROJECT_NOT_FOUND404Not a project made by Migrate to the New Engine, or its legacy project is no longer on your account
MIGRATION_TOO_LARGE413The legacy project has grown past what one migration copies
RATE_LIMITED429A refresh of this legacy project is already running (one every 2 minutes)
End of Refresh Migrated Data Back to top