# Totalum App Builder API — Projects

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


---

# 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](#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

```endpoint
method: POST
path: /api/v1/vcaas/projects/launch
cost: 2 credits + the first prompt
```

Create 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.

:::tip Control your costs
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](#create-project), and change them any time with [Update Credit Limits](https://www.totalum.app/docs/api/limits#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](https://www.totalum.app/docs/api/agent#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`) 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](https://www.totalum.app/docs/api/migration-v2) |
| `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](https://www.totalum.app/docs/api/project-groups) |
| `dataResidency` | string | No | `"global"` or `"eu"` (data kept in the EU only). Default: your account's setting ([Set Data Residency](https://www.totalum.app/docs/api/account#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](https://www.totalum.app/docs/api/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 |

:::warning A 200 means the project exists — read `agent.started` and `warnings`
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](https://www.totalum.app/docs/api/agent#run-ai-agent) with the same prompt.
:::

**Example request**

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

:::success Success · 200 OK
```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 |
| `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](https://www.totalum.app/docs/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

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

Create an **empty** project: no prompt, nothing built. Use it only when you will [import](https://www.totalum.app/docs/api/transfer#import-project) a project into it, [connect GitHub](https://www.totalum.app/docs/api/github#connect-github-repository) first, or [write the files](https://www.totalum.app/docs/api/project-files#write-file-content) yourself; otherwise use [Launch Project](#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](#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](https://www.totalum.app/docs/api/transfer#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](#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**

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

:::success Success · 200 OK
```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 |
| `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](https://www.totalum.app/docs/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

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

Your 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](#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](#update-project)); absent otherwise |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://builder-api.totalum.app/api/v1/vcaas/projects?limit=20&sortField=lastModified"
```

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

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

Everything 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](https://www.totalum.app/docs/api/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](https://www.totalum.app/docs/api/agent#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](#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](#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](#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**

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

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

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

The 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](https://www.totalum.app/docs/api/database-backups#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**

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

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

```endpoint
method: PATCH
path: /api/v1/vcaas/projects/:projectId
cost: Free
```

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

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

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

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

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

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

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

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

Take 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](#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**

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

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

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

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

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

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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/runaway/release
cost: Free
```

Bring 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](https://www.totalum.app/docs/api/webhooks). The pause lifts by itself after 24 hours (`runawayPause.resumesAt` on [Get Project](#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](#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](#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**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/runaway/release
```

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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/preview/rotate-url
cost: Free
```

Give 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](#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**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/preview/rotate-url
```

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