# 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`, `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](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) |
| `ownerEmail` | string | No | The e-mail of the app's owner: the person who may use the app's admin and staff pages once they sign up with it and confirm it in the app. 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). **Builders on Totalum** (your users build apps through your product): send your user's e-mail. Change it later with [Set App Owner](#set-app-owner) |
| `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 |
| `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](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 |
| `ownerEmail` | string | No | The e-mail of the app's owner (its admin once confirmed in the app). 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); see [Set App Owner](#set-app-owner) |
| `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 |
| `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](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 |

## Set App Owner

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/owner-email
cost: Free
```

Set who owns the generated app. The owner's e-mail is the app's admin: after signing up **in the app** with that address, that person enters the 8-digit code e-mailed to them (the app asks for it right after sign-up) and then has the admin and staff pages. The default is none: in a project without an owner e-mail, 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). If your users build apps through your product, set each project's owner to your user's e-mail, best at [Launch Project](#launch-project) with `ownerEmail`. The running preview and published app use the new address at once, with no rebuild. `GET` on the same path reads it; it answers `ownerEmail: null` when none is set. Projects created earlier keep the owner they had. The project's owner can also set, change or remove it by asking the project's coding agent in a prompt ("set the owner e-mail to ana@example.com", "remove the owner e-mail"): the agent changes only its own project, with the same effect. Current engine only.

:::info Accounts in the preview and in the published app
The preview and the published app have separate databases. Accounts and sample data created in the preview stay in the preview; after publishing, users sign up again in the published app, and the owner enters the e-mailed code there too. To put the preview's sample data on the published app, ask for it in a prompt ("put the demo data on the live site").
:::

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `ownerEmail` | string | Yes | The owner's e-mail (`email` is accepted too). Stored in lower case |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.ownerEmail` | string | The owner's e-mail now in effect |

**Example request**

```bash
curl -X PUT -H "api-key: tlm_sk_your_key" -H "content-type: application/json" \
  -d '{"ownerEmail":"ana@example.com"}' \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/owner-email
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "ownerEmail": "ana@example.com" } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `INVALID_OWNER_EMAIL` | 400 | `ownerEmail` is not a valid e-mail address (also answered by Launch and Create Project) |
| `PROJECT_NOT_FOUND` | 404 | No project with this id in your account |
