Projects
Create, inspect and manage your apps.
A project is one app: its source code, its preview, its published site, its own databases and files. Start with Launch Project — it creates the project and starts building it in one call. Every endpoint needs the api-key header and answers the { "errors", "data" } envelope.
#Launch Project
/api/v1/vcaas/projects/launchapi-key2 credits + the first promptCreate a project and start building it with its first prompt, in one call. Secrets, attachments, credit limits and a Figma token sent here are in place before the first build starts, which is the order the build needs.
Every project has two monthly caps: development (what the AI agent and project actions spend) and infrastructure (what the running app spends on hosting, database, AI, e-mail…). Projects created through the API start at 5,000 and 300 credits a month. Set your own in creditLimits here or in Create Project, and change them any time with Update Credit Limits.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Your preferred id: 4–35 characters, lowercase letters, digits and single hyphens, starting with a letter, never containing -dev-. It becomes the address <projectId>.totalum-project.com. If it is taken, a short random suffix is added and requestedProjectId is returned |
prompt | string | Yes | What to build (the first prompt, up to 40,000 characters) |
tier | string | No | "auto" (default), "cheap", "normal" or "expensive" — see agent tiers. Also accepted as model or modelTier |
engineVersion | string | No | "v2" to require the current engine. A request that cannot get it is refused (ENGINE_V2_DISABLED, ENGINE_REQUEST_NOT_ELIGIBLE, ENGINE_V2_REQUIRES_PLATFORM_ACCOUNT) and nothing is created. New accounts create every project on the current engine, with or without it ("legacy" is refused: ENGINE_LEGACY_UNAVAILABLE); on an older account a create without it can be a legacy project |
label | string | No | Display name, up to 80 characters. Can be changed later; the id cannot |
description | string | No | Up to 500 characters |
groupId | string | No | An existing project group |
dataResidency | string | No | "global" or "eu" (data kept in the EU only). Default: your account's setting (Set Data Residency), else "global". Fixed for the project's life |
audienceCountry | string | No | Two-letter country code of the app's users ("US"); the database is placed nearest them. Default: your account's country. Never makes a project EU-only |
files | object[] | No | Up to 10 attachments for the first prompt. Each has exactly one of url or content |
files[].name | string | Yes | File name, e.g. logo.png |
files[].description | string | No | What the file is for ("use as the logo"). imageDescription is accepted too |
files[].url | string | No | Public http(s) URL of a hosted file |
files[].content | string | No | The file as base64 (a data:…;base64, prefix is accepted). Uploaded for you, free |
secrets | object[] | No | Up to 50 environment variables, stored before the build starts |
secrets[].secretName | string | Yes | Uppercase letters, digits and _, starting with a letter (STRIPE_KEY) |
secrets[].secretValue | string | Yes | 1–10,000 characters. Never returned again |
secrets[].environment | string | No | "development", "production" or "both" (default) |
creditLimits | object | No | Monthly caps applied from the first prompt. Set them to control your costs — see Spending & Rate Limits |
creditLimits.maxDevelopmentCreditsPerMonth | number | null | No | Cap on what the AI agent and project actions spend (prompts, builds, publishing). Default 5000 for projects created through the API; null = no cap |
creditLimits.maxInfrastructureCreditsPerMonth | number | null | No | Cap on what the running app spends (hosting, database, AI, e-mail…). Default 300 for projects created through the API; null = no cap |
figma | object | No | Connect Figma so the agent can read designs linked in the prompt |
figma.token | string | Yes | A Figma personal access token, checked before anything is created |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The id that was created. Use this one from now on |
data.requestedProjectId | string | undefined | The id you asked for, only when the created one differs |
data.description | string | Project description |
data.plan | string | Always "api" |
data.createdAt | string | ISO 8601 creation time |
data.label | string | undefined | The label, when one was given |
data.groupId | string | undefined | The group, when one was given |
data.dataResidency | string | "global" or "eu" |
data.engineVersion | string | "v2" |
data.engineState | string | undefined | Present only when the project was still being set up when the call answered ("provisioning"); the build starts by itself |
data.creditLimits | object | The caps stored on the project, defaults included |
data.files | object[] | The attachments the agent received: { name, imageDescription, url } |
data.secrets | object[] | The secrets stored: { _id, secretName, environment } (never the value) |
data.figma | object | undefined | { connected: true, account } when Figma was connected |
data.agent.started | boolean | false = the project exists but the first prompt did not start (see warnings) |
data.agent.status | string | undefined | "init" when the run started |
data.agent.runId | string | undefined | The run's id |
data.agent.message | string | What to do next |
data.agent.expectedMinutes | number | undefined | Estimated minutes for the first prompt — an estimate, never a deadline |
data.agent.expectedFinishAt | string | undefined | Start time + expectedMinutes (ISO 8601) |
data.agent.warnings | string[] | undefined | Run options that were ignored |
data.warnings | object[] | Steps that did not happen after the project was created: { step, errorCode, errorMessage }; step is "secrets", "figma", "files" or "agent", and the message names the endpoint that retries it |
Everything checked before creation (body, balance, Figma token, project limit, name) answers 4xx and creates nothing. After creation a failed step is a warning, never an error: if agent.started is false, call Run AI Agent with the same prompt.
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"projectId":"barber-bookings","prompt":"Build a booking site for a barbershop with online payments","label":"Barber bookings","creditLimits":{"maxDevelopmentCreditsPerMonth":500}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/launchconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/launch", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"projectId": "barber-bookings",
"prompt": "Build a booking site for a barbershop with online payments",
"label": "Barber bookings",
"creditLimits": {
"maxDevelopmentCreditsPerMonth": 500
}
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"description": "",
"plan": "api",
"createdAt": "2026-10-08T10:30:00.000Z",
"label": "Barber bookings",
"dataResidency": "global",
"creditLimits": { "maxDevelopmentCreditsPerMonth": 500, "maxInfrastructureCreditsPerMonth": 300 },
"files": [],
"secrets": [],
"agent": {
"started": true,
"status": "init",
"runId": "run_01k6x8m2c9",
"message": "Process started. Estimated time: about 9 minutes. This is only an approximation…",
"expectedMinutes": 9,
"expectedFinishAt": "2026-10-08T10:39:00.000Z"
},
"warnings": [],
"engineVersion": "v2"
}
}Error codes
Nothing is created or charged for any of these.
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROMPT | 400 | prompt is missing or empty |
MISSING_PROJECT_ID | 400 | projectId is missing |
INVALID_PROJECT_NAME | 400 | Not lowercase letters, digits and single hyphens starting with a letter |
INVALID_PROJECT_NAME_LENGTH | 400 | Not 4–35 characters |
RESERVED_PROJECT_NAME | 400 | The id contains -dev- |
INVALID_FILES / TOO_MANY_FILES | 400 | files is not an array of objects, or has more than 10 items |
INVALID_FILE_NAME / INVALID_FILE_SOURCE / INVALID_FILE_URL / INVALID_FILE_CONTENT | 400 | An attachment has no name, has both or neither of url/content, a non-http(s) URL, or invalid base64 |
INVALID_SECRETS / TOO_MANY_SECRETS / MISSING_SECRET_FIELDS / DUPLICATE_SECRET_NAME / INVALID_SECRET_ENVIRONMENT | 400 | A malformed secrets list |
INVALID_CREDIT_LIMITS / INVALID_LIMIT | 400 | creditLimits is not an object, or a cap is not a positive number or null |
INVALID_FIGMA / MISSING_FIGMA_TOKEN / FIGMA_TOKEN_MALFORMED / FIGMA_TOKEN_INVALID / FIGMA_TOKEN_FORBIDDEN | 400 | The Figma token is missing or was rejected |
INVALID_PROJECT_GROUP | 400 | The group does not exist, is not yours, or is full |
INVALID_DATA_RESIDENCY | 400 | dataResidency is not "global" or "eu" |
INVALID_AUDIENCE_COUNTRY | 400 | audienceCountry is not a two-letter code |
INVALID_ENGINE_VERSION | 400 | engineVersion has an unknown value |
INSUFFICIENT_CREDITS | 402 | Your balance cannot pay the project (or is below 1 credit, needed to start the agent) |
MAX_PROJECTS_REACHED | 403 | Your plan's project limit is in use. errorDetails: maxProjects, projectsUsed, plan, upgradePlan |
ENGINE_REQUEST_NOT_ELIGIBLE | 403 | engineVersion: "v2" was asked but this account cannot create projects on it yet |
ENGINE_V2_REQUIRES_PLATFORM_ACCOUNT | 403 | engineVersion: "v2" (or a current-engine export code) from an account with no Totalum platform account: sign in once at platform.totalum.app, then retry |
VCAAS_INSUFFICIENT_CREDITS | 403 | Your balance is below 1 credit |
PROJECT_ALREADY_EXISTS | 409 | The name and every suffixed candidate were taken |
ENGINE_V2_DISABLED | 409 | New projects cannot be created on the current engine right now |
ENGINE_LEGACY_UNAVAILABLE | 409 | engineVersion: "legacy", or a legacy (.zip) export code, on a new account: it creates only on the current engine |
RATE_LIMIT_EXCEEDED | 429 | You are creating projects faster than your plan allows (new projects and prompts share one per-minute allowance, see Limits) — retry after Retry-After seconds |
RATE_LIMITED | 429 | Your account has the most prompts running at once its plan allows, or too many Figma token checks (10 a minute) — retry after Retry-After seconds |
LAUNCH_PROJECT_ERROR | 503 | A temporary failure; nothing was created. Retry after Retry-After seconds |
#Create Project
/api/v1/vcaas/projectsapi-key2 creditsCreate an empty project: no prompt, nothing built. Use it only when you will import a project into it, connect GitHub first, or write the files yourself; otherwise use Launch Project.
The project is set up in the background (about 15 seconds); a write sent before it is ready waits for it.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Same rules as Launch Project; a taken id gets a suffix |
label | string | No | Display name, up to 80 characters |
description | string | No | Up to 500 characters |
groupId | string | No | An existing project group |
engineVersion | string | No | "v2" to require the current engine (new accounts always get it), as in Launch Project |
dataResidency | string | No | "global" or "eu", as in Launch Project |
audienceCountry | string | No | Two-letter country code of the app's users |
sourceImportCode | string | No | An importCode from Export Project: the new project is created to receive it (checked before anything is created) |
creditLimits | object | No | The project's monthly spending caps, as in Launch Project |
creditLimits.maxDevelopmentCreditsPerMonth | number | null | No | Cap on what the AI agent and project actions spend. Default 5000; null = no cap |
creditLimits.maxInfrastructureCreditsPerMonth | number | null | No | Cap on what the running app spends. Default 300; null = no cap |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The id that was created. Use this one |
data.requestedProjectId | string | undefined | The id you asked for, when it differs |
data.description | string | Project description |
data.plan | string | Always "api" |
data.createdAt | string | ISO 8601 creation time |
data.label | string | undefined | The label, when given |
data.groupId | string | undefined | The group, when given |
data.dataResidency | string | "global" or "eu" |
data.engineVersion | string | "v2" |
data.engineState | string | "provisioning" — the project is being set up |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"projectId":"crm-import","label":"CRM (imported)","creditLimits":{"maxDevelopmentCreditsPerMonth":1000,"maxInfrastructureCreditsPerMonth":500}}' \
https://builder-api.totalum.app/api/v1/vcaas/projectsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"projectId": "crm-import",
"label": "CRM (imported)",
"creditLimits": {
"maxDevelopmentCreditsPerMonth": 1000,
"maxInfrastructureCreditsPerMonth": 500
}
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "crm-import",
"description": "",
"plan": "api",
"createdAt": "2026-10-08T10:30:00.000Z",
"label": "CRM (imported)",
"dataResidency": "global",
"engineVersion": "v2",
"engineState": "provisioning"
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID / INVALID_PROJECT_NAME / INVALID_PROJECT_NAME_LENGTH / RESERVED_PROJECT_NAME | 400 | The id is missing or malformed |
INVALID_PROJECT_GROUP | 400 | Unknown or full group |
INVALID_DATA_RESIDENCY / INVALID_AUDIENCE_COUNTRY / INVALID_ENGINE_VERSION | 400 | An invalid option |
INVALID_CREDIT_LIMITS / INVALID_LIMIT | 400 | creditLimits is not an object, or a cap is not a positive number or null |
INSUFFICIENT_CREDITS | 402 | Not enough credits for the project |
MAX_PROJECTS_REACHED | 403 | Your plan's project limit is in use |
ENGINE_V2_REQUIRES_PLATFORM_ACCOUNT | 403 | engineVersion: "v2" (or a current-engine export code) from an account with no Totalum platform account: sign in once at platform.totalum.app, then retry |
PROJECT_ALREADY_EXISTS | 409 | The name and every suffixed candidate were taken |
ENGINE_V2_DISABLED | 409 | New projects cannot be created on the current engine right now |
ENGINE_LEGACY_UNAVAILABLE | 409 | engineVersion: "legacy", or a legacy (.zip) export code, on a new account: it creates only on the current engine |
RATE_LIMIT_EXCEEDED | 429 | Too many creates for your plan (new projects and prompts share one per-minute allowance, see Limits); retry after Retry-After seconds |
CREATE_PROJECT_ERROR | 503 | A temporary failure; nothing was created. Retry after Retry-After seconds |
Project limits per plan:
| Plan | Projects at once | New projects |
|---|---|---|
| Free | 2 | 1 a minute |
| Starter | 10 | 5 a minute |
| Builder | 50 | 10 a minute |
| Professional | 300 | 30 a minute |
| Enterprise | Unlimited | 100 a minute |
#List Projects
/api/v1/vcaas/projectsapi-keyFreeYour projects, newest first, up to 100 per page. The total is in the X-Total-Count header and X-Has-More says whether another page exists.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
limit | number | No | 1–100 (default 100) |
skip | number | No | Projects to skip, for paging (default 0) |
search | string | No | Case-insensitive match on id, label and description |
sortField | string | No | date (default, creation time) or lastModified |
sortDirection | string | No | desc (default) or asc |
groupId | string | No | Only the projects of one group; none = only ungrouped projects |
createdFrom | string | No | Created at or after this date (ISO 8601; a bare day is UTC) |
createdTo | string | No | Created at or before this date (a bare day includes the whole day) |
Response fields
| Field | Type | Description |
|---|---|---|
data[].projectId | string | Project id |
data[].description | string | Description |
data[].plan | string | "api" |
data[].createdAt | string | ISO 8601 creation time |
data[].label | string | undefined | Display name, when set |
data[].groupId | string | undefined | Group, when filed in one |
data[].previewImageUrl | string | undefined | Screenshot of the home page, once a prompt has finished |
data[].lastModifiedAt | string | Last change (prompt, publish, edit); never null |
data[].productionProjectUrl | string | The published host, as on Get Project: the custom domain once it is active, else <projectId>.totalum-project.com (or <projectId>.webapp-project.com when chosen) |
data[].engineVersion | string | "v2" |
data[].liveHostSuffix | string | undefined | "webapp-project.com" when chosen (Update Project); absent otherwise |
Example request
curl -H "api-key: tlm_sk_your_key" \
"https://builder-api.totalum.app/api/v1/vcaas/projects?limit=20&sortField=lastModified"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects?limit=20&sortField=lastModified", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": [
{
"projectId": "barber-bookings",
"description": "",
"plan": "api",
"createdAt": "2026-10-08T10:30:00.000Z",
"label": "Barber bookings",
"previewImageUrl": "https://files.totalum.app/previews/barber-bookings.png",
"lastModifiedAt": "2026-10-08T10:41:00.000Z",
"productionProjectUrl": "barber-bookings.totalum-project.com",
"engineVersion": "v2"
}
]
}#Get Project
/api/v1/vcaas/projects/:projectIdapi-keyFreeEverything about one project: its URLs, the agent's state, the last publish, its domain, secrets and spending. Read it again after every prompt: the preview address can change.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.label | string | undefined | Display name, when set |
data.groupId | string | undefined | Group, when filed in one |
data.description | string | Description |
data.plan | string | "api" |
data.createdAt | string | ISO 8601 creation time |
data.agentProcessStatus | string | "init" (a prompt is running), "done" (the last one finished) or "idle" (never prompted) |
data.agentServerStatus | string | "Creating" while the project is set up, "Archived" while it sleeps after a long inactivity (the next prompt wakes it), else "Active" |
data.temporalDevelopmentProjectUrl | string | The development preview URL |
data.developmentUrlFieldToUse | string | Always "temporalDevelopmentProjectUrl" |
data.productionProjectUrl | string | The published host: the custom domain once it is active, else <projectId>.totalum-project.com (it answers only after the first publish) |
data.deployment | object | null | The last publish: { createdAt, status, versionId }, status = "deploying", "success" or "error", versionId = the version it put online (or tried to); null if never published |
data.customDomain | object | null | { _id, projectId, deploymentId, hostname, status, sslStatus, dnsRecordsToAdd, createdAt, updatedAt, source? } — see Custom Domains |
data.secrets | object[] | { _id, secretName, environment } (values are never returned) |
data.versionRecovery | object | null | While a version restore runs or after it failed: { status: "recovering" | "error", versionId, startedAt, errorMessage? } |
data.importInProgress | object | null | { startedAt } while an import runs |
data.previewImageUrl | string | undefined | Screenshot of the home page |
data.totalCreditsSpent | number | Credits this project has spent in total |
data.creditLimits | object | { maxDevelopmentCreditsPerMonth, maxInfrastructureCreditsPerMonth }, null = no cap |
data.multiPrompt | object | null | The multi-prompt batch, as on Get Agent Status |
data.engineVersion | string | "v2" |
data.engineState | string | "queued", "provisioning", "ready", "error", "suspended" or "deleting" |
data.liveHostSuffix | string | "totalum-project.com" or "webapp-project.com": the domain of the main published address and preview (Update Project) |
data.dataResidency | string | "global" or "eu" |
data.sandbox | object | The builder: { state, lastPromptAt } |
data.lifecycle | object | { state, nextStep, lastActivityAt, pausedAt, resumable } — see Pause Project |
data.preview | object | { url, token8, status, deployState }: the preview address and whether its latest build is live (status: "live", "deploying", "failed", "parked", "none") |
data.lastDeployError | object | null | Why the last preview or publish build failed, when it did |
data.currentRun | object | null | The running prompt: { id, state, startedAt, batchId?, promptPreview } |
data.state | string | "active" or "paused-by-user" |
data.pausedAt | string | null | When it was paused |
data.resumable | boolean | Whether a paused project can be resumed |
data.runawayPause | object | null | Set while the emergency stop has paused the site: { pausedAt, resumesAt, metric, value?, threshold? } — see Release Runaway Pause |
data.contextClearPending | boolean | The next prompt starts with a fresh context |
data.operations | object[] | Long operations running now (publish, restore, import…): { kind, id, status, startedAt, errorMessage?, versionId?, progress? } |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookingsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"label": "Barber bookings",
"description": "",
"plan": "api",
"agentProcessStatus": "done",
"createdAt": "2026-10-08T10:30:00.000Z",
"agentServerStatus": "Active",
"deployment": { "createdAt": "2026-10-08T11:02:00.000Z", "status": "success", "versionId": "ver_01k6xd2m4n6p8q0r2s4t6v8w0y" },
"versionRecovery": null,
"importInProgress": null,
"secrets": [{ "_id": "sec_01k6x9", "secretName": "STRIPE_KEY", "environment": "production" }],
"customDomain": null,
"developmentUrlFieldToUse": "temporalDevelopmentProjectUrl",
"temporalDevelopmentProjectUrl": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com",
"productionProjectUrl": "barber-bookings.totalum-project.com",
"totalCreditsSpent": 24.6,
"creditLimits": { "maxDevelopmentCreditsPerMonth": 500, "maxInfrastructureCreditsPerMonth": 300 },
"multiPrompt": null,
"engineVersion": "v2",
"engineState": "ready",
"dataResidency": "global",
"sandbox": { "state": "active", "lastPromptAt": "2026-10-08T10:30:05.000Z" },
"preview": { "url": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com", "token8": "k3y9x2ab", "status": "live", "deployState": "idle" },
"lastDeployError": null,
"currentRun": null,
"state": "active",
"pausedAt": null,
"resumable": false,
"contextClearPending": false,
"operations": []
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
PROJECT_NOT_FOUND | 404 | No project with this id in your account |
API_ACCESS_DISABLED | 403 | API access is switched off for this project in your account settings |
PROJECT_NOT_ALLOWED | 403 | Your API key is restricted to other projects |
#Get Project State
/api/v1/vcaas/projects/:projectId/stateapi-keyFreeThe project's live state in one read: what is running, the preview, the lifecycle and any long operation with its progress (for example the copy of a database move to Turso).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.engineVersion | string | "v2" |
data.state | string | "queued", "provisioning", "ready", "error", "suspended" or "deleting" |
data.lifecycle | object | { state, nextStep: { kind, at } | null, lastActivityAt, pausedAt, resumable } |
data.sandbox | object | { state, lastPromptAt } |
data.preview | object | { temporalLinkUrl, token8, versionId?, status, deploy: { state, fixAttempt?, error? } } |
data.lastDeployError | object | null | The last failed preview or publish build |
data.run | object | null | The running prompt: { id, state, startedAt, batchId?, promptPreview } |
data.batch | object | null | The newest multi-prompt batch |
data.contextClearPending | boolean | The next prompt starts fresh |
data.operations | object[] | { kind, id, status, startedAt, errorMessage?, versionId?, progress? } |
data.liveHostSuffix | string | Domain of the published host |
data.liveUrl | string | Published URL |
data.dataResidency | string | "global" or "eu" |
data.updatedAt | string | When this state last changed |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/stateconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/state", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"engineVersion": "v2",
"state": "ready",
"lifecycle": { "state": "active", "nextStep": null, "lastActivityAt": "2026-10-08T11:02:00.000Z", "pausedAt": null, "resumable": false },
"sandbox": { "state": "active", "lastPromptAt": "2026-10-08T10:30:05.000Z" },
"preview": { "temporalLinkUrl": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com", "token8": "k3y9x2ab", "status": "live", "deploy": { "state": "idle" } },
"lastDeployError": null,
"run": null,
"batch": null,
"contextClearPending": false,
"operations": [],
"liveHostSuffix": "totalum-project.com",
"liveUrl": "https://barber-bookings.totalum-project.com",
"dataResidency": "global",
"updatedAt": "2026-10-08T11:02:00.000Z"
}
}#Update Project
/api/v1/vcaas/projects/:projectIdapi-keyFreeChange the label, the description, the group or the live address's domain. The project id never changes. A field you leave out is untouched; null clears it.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
label | string | null | No | Display name, up to 80 characters; null clears it |
description | string | null | No | Up to 500 characters; null clears it |
groupId | string | null | No | An existing group; null takes the project out of its group |
liveHostSuffix | string | No | "totalum-project.com" (default) or "webapp-project.com" — the main domain of the published site and its preview, e.g. <projectId>.webapp-project.com. webapp-project.com needs the Professional or Enterprise plan. On those plans both domains serve the site and its preview; this setting only picks the main one (productionProjectUrl, canonical and sitemap links). Links inside your app follow at your next publish. If the account moves to a lower plan, the project goes back to totalum-project.com and its webapp-project.com address redirects for 90 days. |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.label | string | undefined | The label after the change |
data.description | string | The description after the change |
data.groupId | string | undefined | The group after the change |
data.liveHostSuffix | string | undefined | The domain chosen, when liveHostSuffix was sent |
Example request
curl -X PATCH \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"label":"Barber shop","groupId":null}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookingsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings", {
method: "PATCH",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "Barber shop",
"groupId": null
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "projectId": "barber-bookings", "label": "Barber shop", "description": "" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
NOTHING_TO_UPDATE | 400 | None of label, description, groupId, liveHostSuffix was sent |
INVALID_LIVE_HOST_SUFFIX | 400 | liveHostSuffix is not totalum-project.com or webapp-project.com |
PLAN_REQUIRED | 403 | webapp-project.com needs the Professional or Enterprise plan (errorDetails.upgradePlan) |
AGENT_RUNNING / OPERATION_IN_PROGRESS | 409 | A prompt, an operation or a preview deploy is in progress; try again when it ends |
INVALID_PROJECT_GROUP | 400 | The group does not exist or is not yours |
PROJECT_NOT_FOUND | 404 | No such project in your account |
#Delete Project
/api/v1/vcaas/projects/:projectIdapi-keyFreePermanently delete a project: its published site, preview, custom domain, databases, files and versions. It cannot be undone, and its id can never be used again. Deleting is free and always allowed, whatever your balance.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.success | boolean | true |
Example request
curl -X DELETE -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookingsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings", {
method: "DELETE",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "success": true } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
PROJECT_NOT_FOUND | 404 | No such project in your account |
PLAN_NOT_API | 400 | The project was not created through the API or the Totalum app's API plan and cannot be deleted here |
#Pause Project
/api/v1/vcaas/projects/:projectId/pauseapi-key2 creditsTake the whole project offline so it consumes nothing: the published site and the preview show a paused page and the databases are saved first. Answers 202 while the pause runs; follow it on Get Project State (lifecycle.state becomes paused-by-user).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.accepted | boolean | true |
data.state | string | The lifecycle state now |
data.operation | object | undefined | The operation started: { kind, id, startedAt, estimateSec }. Absent (and 200) when the project was already paused |
data.creditsCharged | number | Credits charged (0 when nothing started) |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/pauseconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/pause", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "accepted": true, "state": "active", "operation": { "kind": "pause", "id": "op_01k6xa", "startedAt": "2026-10-08T12:00:00.000Z", "estimateSec": 60 }, "creditsCharged": 2 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_CREDITS | 402 | Not enough credits |
AGENT_RUNNING | 409 | A prompt is running; wait for it |
DEPLOYMENT_RUNNING | 409 | A publish is running |
OPERATION_IN_PROGRESS | 409 | Another long operation is running |
LIFECYCLE_NOT_APPLICABLE | 409 | The project is asleep after a long inactivity: prompt it (or restart its server) to wake it first |
#Resume Project
/api/v1/vcaas/projects/:projectId/resumeapi-key2 creditsBring a paused project back: databases restored, preview and published site online again. Answers 202 while it runs.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.accepted | boolean | true |
data.state | string | The lifecycle state now |
data.operation | object | { kind, id, startedAt, estimateSec } |
data.creditsCharged | number | Credits charged |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/resumeconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/resume", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "accepted": true, "state": "paused-by-user", "operation": { "kind": "resume", "id": "op_01k6xb", "startedAt": "2026-10-09T08:00:00.000Z", "estimateSec": 90 }, "creditsCharged": 2 } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_CREDITS | 402 | Not enough credits |
PROJECT_NOT_PAUSED | 409 | The project is not paused |
OPERATION_IN_PROGRESS | 409 | Another long operation is running |
PAUSE_DUMP_EXPIRED | 410 | The saved databases of this pause are gone; the project cannot be resumed |
#Release Runaway Pause
/api/v1/vcaas/projects/:projectId/runaway/releaseapi-keyFreeBring a site back online after the emergency stop paused it. The emergency stop pauses a project when its app uses far more than normal (for example a loop that sends thousands of requests or spends credits on built-in features every minute): the site shows a paused page, the app's calls to Totalum answer 429 PROJECT_PAUSED_RUNAWAY, and you get an e-mail and the project.kill_switch.fired webhook. The pause lifts by itself after 24 hours (runawayPause.resumesAt on Get Project); this call lifts it now. Fix the cause first, or the same usage pauses it again. Free at any balance; the site answers again within about a minute. A project that is not paused by the emergency stop answers 200 with released: false and nothing changes. This is not Resume Project, which brings back a project you paused yourself.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.released | boolean | true when the pause was lifted, false when the project was not paused by the emergency stop |
data.message | string | What happened, in plain words |
runawayPause.metric (on Get Project) says which limit was crossed: credits_hour (credits spent on built-in features in one hour), usd_hour (unusually heavy use of built-in features in one hour; no value or threshold), requests_5min, rows_written_5min or rows_read_5min (in 5 minutes). value is what was measured and threshold the limit.
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/runaway/releaseconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/runaway/release", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "released": true, "message": "The site is back online; it can take up to a minute to show everywhere." } }#Rotate Preview URL
/api/v1/vcaas/projects/:projectId/preview/rotate-urlapi-keyFreeGive the development preview a new address; the old one stops working at once. The preview address also changes by itself every month, so never store it: read temporalDevelopmentProjectUrl from Get Project. The published site and custom domains never change. Owner (or a member allowed to manage API keys) only; at most once a minute.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.temporalLinkUrl | string | The new preview address |
data.rotatedAt | string | ISO 8601 time of the change |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/preview/rotate-urlconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/preview/rotate-url", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "temporalLinkUrl": "https://barber-bookings-dev-temporal-link-preview-p7m2q9rt.totalum-project.com", "rotatedAt": "2026-10-08T12:00:00.000Z" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
OWNER_ONLY | 403 | Only the owner or a member allowed to manage API keys |
AGENT_RUNNING | 409 | A prompt is running |
OPERATION_IN_PROGRESS | 409 | A publish or other operation is running |
RATE_LIMIT_EXCEEDED | 429 | Rotated less than a minute ago |
