# Totalum API — Full Reference

The complete Totalum REST API reference in one file, for offline reading and AI agents.

- Base URL: `https://api-accounts.totalum.app`
- Authentication: every request includes the header `api-key: <your-api-key>` (keys are prefixed `tlm_sk_`).
- Response envelope: `{ "errors": null | { "errorCode": "...", "errorMessage": "..." }, "data": ... }`.
- Keep your API key on your backend only — never call the API from browser code.
- Source: https://www.totalum.app/docs/api/overview · MCP setup: https://www.totalum.app/docs/mcp


---

# Account

> Check your current credit balance and account information.

The Totalum API is a REST API for programmatically building, deploying, and managing projects.

- **Base URL:** `https://api-accounts.totalum.app`
- **Authentication:** every request must include the header `api-key: <your-api-key>` (keys are prefixed `tlm_sk_`).
- **Response envelope:** every response is shaped as `{ "errors": null | { "errorCode": "...", "errorMessage": "..." }, "data": ... }`.

:::tip
Keep your API key secret — it must never leave your backend server. Never call the Totalum API directly from frontend code.
:::

## Get Account Info

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

Retrieve your current credit balance and account details.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.credits` | number | Current credit balance |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/account
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "credits": 425
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "GET_ACCOUNT_ERROR",
    "errorMessage": "Internal error fetching account info"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `GET_ACCOUNT_ERROR` | 400 | Internal error fetching account info |


---

# Projects

> Manage the lifecycle of your vibe coding projects.

Projects are the top-level unit of the VCaaS API. Each project has its own database, source code, dev server, and (optionally) a deployment and custom domain. All endpoints require the `api-key` header and return the standard `{ "errors": ..., "data": ... }` envelope.

## Create Project

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

Create a new vibe coding project.

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `projectId` | string | Yes | 4-35 chars, lowercase letters + numbers + hyphens, must start with letter |
| `description` | string | No | Project description, max 500 characters |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The project ID |
| `data.description` | string | Project description |
| `data.plan` | string | Always `"api"` for VCaaS projects |
| `data.createdAt` | string | ISO 8601 creation date |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"my-app","description":"A SaaS landing page"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "description": "A SaaS landing page",
    "plan": "api",
    "createdAt": "2026-03-11T10:30:00.000Z"
  }
}
```
:::

:::danger Error · 409
```json
{
  "errors": {
    "errorCode": "PROJECT_ALREADY_EXISTS",
    "errorMessage": "A project with this name already exists"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `INVALID_PROJECT_NAME` | 400 | Invalid format. Use lowercase letters, numbers, and hyphens. Must start with a letter |
| `INVALID_PROJECT_NAME_LENGTH` | 400 | Project name must be between 4 and 35 characters |
| `PROJECT_ALREADY_EXISTS` | 409 | A project with this name already exists |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for this operation |
| `RATE_LIMIT_EXCEEDED` | 429 | Maximum 10 project creations per 60 seconds |

## List Projects

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

Get all your projects.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | array | Array of project objects |
| `data[].projectId` | string | The project ID |
| `data[].description` | string | Project description |
| `data[].plan` | string | Always `"api"` for VCaaS projects |
| `data[].createdAt` | string | ISO 8601 creation date |

**Example request**

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

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": [
    {
      "projectId": "my-app",
      "description": "A SaaS landing page",
      "plan": "api",
      "createdAt": "2026-03-11T10:30:00.000Z"
    }
  ]
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "LIST_PROJECTS_ERROR",
    "errorMessage": "Internal error listing projects"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `LIST_PROJECTS_ERROR` | 400 | Internal error listing projects |

## Get Project Details

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

Retrieve full project info including status, deployment, secrets, and URLs. This is the main polling endpoint for project state.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `:projectId` | string | The project ID |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The project ID |
| `data.description` | string | Project description |
| `data.plan` | string | Always `"api"` for VCaaS projects |
| `data.agentProcessStatus` | string \| undefined | `"init"` (running) \| `"done"` (finished) \| `"idle"` (not started) |
| `data.agentServerStatus` | string \| undefined | `"Active"` \| `"Creating"` \| `"Starting"` \| `"Archived"` \| `"Unarchiving"` \| `"Archiving"` |
| `data.createdAt` | string | ISO 8601 creation date |
| `data.deployment` | object \| null | Latest deployment info, null if never deployed |
| `data.deployment.status` | string | `"deploying"` \| `"success"` \| `"error"` |
| `data.deployment.createdAt` | string | ISO 8601 deployment date |
| `data.deployment.versionId` | string \| undefined | Version ID that was deployed |
| `data.versionRecovery` | object \| null | Set while a recoverVersion call is running, otherwise null. **This is the canonical signal for "is a version recovery in progress"** — do NOT poll agentProcessStatus for recovery (the agent is not involved). |
| `data.versionRecovery.status` | string | `"recovering"` (in progress) \| `"error"` (last recovery failed) |
| `data.versionRecovery.versionId` | string | The version ID currently being / last attempted being recovered |
| `data.versionRecovery.startedAt` | string | ISO 8601 start time |
| `data.versionRecovery.errorMessage` | string \| undefined | Present when status=`"error"` — surface this text to the user |
| `data.secrets` | array | List of secret names (values never returned) |
| `data.secrets[]._id` | string | Secret ID (use for deletion) |
| `data.secrets[].secretName` | string | Environment variable name |
| `data.secrets[].environment` | string | `"development"` \| `"production"` \| `"both"` |
| `data.customDomain` | object \| null | Custom domain info, null if none configured |
| `data.customDomain.hostname` | string | The custom domain hostname |
| `data.customDomain.status` | string | `"pending_validation"` \| `"pending_deployment"` \| `"active"` \| `"blocked"` |
| `data.customDomain.sslStatus` | string | SSL certificate status |
| `data.customDomain.dnsRecordsToAdd` | array \| undefined | DNS records to configure: `[{ type: "CNAME"\|"TXT", name, value }]` |
| `data.temporalDevelopmentProjectUrl` | string \| null \| undefined | Live development preview URL (from the running dev server). May be null/undefined if no server has started yet. |
| `data.cachedDevelopmentUrl` | string \| null \| undefined | Cached development preview URL (static snapshot, available when server is not active). May be null/undefined if the project has never been archived. |
| `data.developmentUrlFieldToUse` | string \| null \| undefined | Which field to use for the development preview right now: `"temporalDevelopmentProjectUrl"` or `"cachedDevelopmentUrl"`. **If this field is null or undefined, default to `temporalDevelopmentProjectUrl`.** |
| `data.productionProjectUrl` | string \| undefined | Production URL — custom domain if connected, otherwise {projectId}.totalum-project.com |
| `data.totalCreditsSpent` | number | Total credits spent on this project |
| `data.creditLimits` | object | Currently configured monthly credit limits for this project |
| `data.creditLimits.maxDevelopmentCreditsPerMonth` | number \| null | Max development credits/month (null = no limit) |
| `data.creditLimits.maxInfrastructureCreditsPerMonth` | number \| null | Max infrastructure credits/month (null = no limit) |
| `data.multiPrompt` | object \| null | Present only when a multi-prompt batch was started via POST /agent/start with `multiPrompt`. Same shape as on GET /agent/status. |

:::info Preview URL logic
Use `data.developmentUrlFieldToUse` to decide which development URL to display. It returns the name of the response field containing the best URL for the current state. If it returns `"cachedDevelopmentUrl"`, use `data.cachedDevelopmentUrl`; if it returns `"temporalDevelopmentProjectUrl"`, use `data.temporalDevelopmentProjectUrl`. **If `developmentUrlFieldToUse` is null or undefined, always fall back to `data.temporalDevelopmentProjectUrl`.** The cached URL is a static snapshot available when the dev server is down (e.g. archived). Once the server is active and a prompt completes, it switches back to the live URL.
:::

:::warning When to refresh the preview
You MUST call `GET /projects/:projectId` and re-read the preview URL fields on these events: (1) when the user navigates to the project page, (2) when the user manually refreshes the page, and (3) every time a prompt finishes (agent status becomes `"done"`). The preview URL can change between these events. Always re-read `developmentUrlFieldToUse` after fetching and use it to pick the correct URL. Never cache the preview URL permanently. If you embed a dev preview URL or the production URL in an iframe, always provide an "Open in new tab" button next to it.
:::

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "description": "A SaaS landing page",
    "plan": "api",
    "agentProcessStatus": "done",
    "agentServerStatus": "Active",
    "createdAt": "2026-03-11T10:30:00.000Z",
    "deployment": {
      "status": "success",
      "createdAt": "2026-03-11T11:00:00.000Z",
      "versionId": "v_abc123"
    },
    "versionRecovery": null,
    "secrets": [
      {
        "_id": "s1",
        "secretName": "STRIPE_KEY",
        "environment": "both"
      }
    ],
    "customDomain": {
      "hostname": "app.mysite.com",
      "status": "active",
      "sslStatus": "active",
      "dnsRecordsToAdd": [
        { "type": "CNAME", "name": "app", "value": "my-app.totalum-project.com" },
        { "type": "TXT", "name": "_cf-custom-hostname.app", "value": "verification-token" }
      ]
    },
    "temporalDevelopmentProjectUrl": "https://dev-my-app.totalum.app",
    "cachedDevelopmentUrl": "https://my-app-dev-a1b2c3d4.totalum-project.com",
    "developmentUrlFieldToUse": "temporalDevelopmentProjectUrl",
    "productionProjectUrl": "app.mysite.com",
    "totalCreditsSpent": 12.4,
    "creditLimits": {
      "maxDevelopmentCreditsPerMonth": 100,
      "maxInfrastructureCreditsPerMonth": null
    },
    "multiPrompt": null
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |

## Delete Project

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

Permanently delete a project and all its data.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `:projectId` | string | The project ID to delete |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.success` | boolean | true on successful deletion |

**Example request**

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

:::success Success · 200 OK
```json
{ "errors": null, "data": { "success": true } }
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `PLAN_NOT_API` | 400 | Only API plan projects can be deleted from this API |

## Update Credit Limits

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

Set monthly spending caps per project for development and/or infrastructure credits. By default, projects have no credit limits. Set a field to null to remove that limit.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `:projectId` | string | The project ID |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `maxDevelopmentCreditsPerMonth` | number or null | No | Max development credits per month (null to remove) |
| `maxInfrastructureCreditsPerMonth` | number or null | No | Max infrastructure credits per month (null to remove) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.creditLimits.maxDevelopmentCreditsPerMonth` | number or null | Current development limit |
| `data.creditLimits.maxInfrastructureCreditsPerMonth` | number or null | Current infrastructure limit |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"maxDevelopmentCreditsPerMonth":500,"maxInfrastructureCreditsPerMonth":100}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/credit-limits
```

To remove a limit, set it to null:

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"maxDevelopmentCreditsPerMonth":null}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/credit-limits
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "creditLimits": {
      "maxDevelopmentCreditsPerMonth": 500,
      "maxInfrastructureCreditsPerMonth": 100
    }
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project doesn't exist or you don't own it"
  },
  "data": null
}
```
:::

:::note
By default, projects have no credit limits — all operations are allowed as long as you have credits in your account. When a project reaches its monthly limit, operations in that category return a `403 PROJECT_CREDIT_LIMIT_REACHED` error. Limits reset automatically on the 1st of each month.
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_LIMIT_FIELDS` | 400 | At least one of the two limit fields is required |
| `INVALID_LIMIT` | 400 | Amount must be a positive number |
| `PROJECT_NOT_FOUND` | 404 | Project doesn't exist or you don't own it |


---

# Project Transfer

> Export a project and import it into another, on the same or a different account.

Project transfer lets you **clone** a project — its database and source code — into another project, on the same or a different account. Export returns a secret `importCode`; whoever holds it can import the project. This is **not** part of the normal build workflow.

:::note Clone sequence
1. `POST /projects/{sourceProjectId}/export` → save `data.importCode`.
2. `POST /projects` → create a NEW empty target project.
3. `POST /projects/{targetProjectId}/import` with `{ "importCode": "<the code>" }`.
4. Poll `GET /projects/{targetProjectId}` every 10-15s until `agentServerStatus: "Active"` — the target is now a clone of the source.
:::

## Export Project

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/export
cost: Uses credits
```

Export this project's database (excluding secrets/sandbox/auth/users/tokens/org) plus a reference to its source code, and get a secret import code. Rate limit: 1 per minute, 5 per hour.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `:projectId` | string | The project ID to export |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `includeRecords` | boolean | No | Default false. true also exports table data records; false exports only schema/pages/config |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.importCode` | string | Secret code to import this project. SECRET — anyone with it can import the data + source |
| `data.includeRecords` | boolean | Whether data records were included |
| `data.message` | string | Human-readable confirmation |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"includeRecords":false}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/export
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "importCode": "my-app-export-project-8f3b1c9a47e2d650b9114af0c7e3a2d1.zip",
    "includeRecords": false,
    "message": "Export ready. Save the importCode and keep it secret..."
  }
}
```
:::

:::danger Error · 402
```json
{
  "errors": {
    "errorCode": "INSUFFICIENT_CREDITS",
    "errorMessage": "Not enough credits"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `PROJECT_EXPORT_LIMIT_REACHED` | 400 | Rate limit reached (1/min, 5/hour) |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits |
| `PROJECT_NOT_FOUND` | 404 | Project not found or not owned by you |

## Import Project

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/import
cost: Uses credits
```

Import a project from an importCode into THIS (almost empty) project: restores the database, sets up the source code, then builds and runs it. Returns immediately; the restore + rebuild run in the background (a few minutes). Existing data is always dropped before restoring. Rate limit: 1 per minute, 5 per hour.

:::note Preconditions
The target project must be (almost) empty — at most 5 database tables and at most 1 version. While importing, prompting the agent returns `IMPORT_IN_PROGRESS`. After calling import, poll `GET /projects/{projectId}` every 10-15s until `agentServerStatus` is `"Active"` and a preview URL is present.
:::

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `:projectId` | string | The target project ID to import into |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `importCode` | string | Yes | The importCode returned by the export endpoint (any project) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The target project ID |
| `data.status` | string | Always `"importing"` |
| `data.message` | string | Instructions to poll for completion |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"importCode":"my-app-export-project-8f3b1c9a....zip"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-new-app/import
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-new-app",
    "status": "importing",
    "message": "Project import started. Poll GET /projects/:projectId until agentServerStatus='Active'."
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_IMPORTABLE",
    "errorMessage": "Target already has content (>5 tables or >1 version). Use a fresh project"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_IMPORT_CODE` | 400 | importCode is required |
| `PROJECT_NOT_IMPORTABLE` | 400 | Target already has content (>5 tables or >1 version). Use a fresh project |
| `IMPORT_IN_PROGRESS` | 400 | An import is already running for this project |
| `AGENT_RUNNING` | 409 | Wait for the agent to finish before importing |
| `PROJECT_IMPORT_LIMIT_REACHED` | 400 | Rate limit reached (1/min, 5/hour) |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits |


---

# AI Agent

> Drive the AI agent that builds and edits your project, in single- or multi-prompt mode.

The AI agent builds and modifies your project from natural-language prompts. Agent runs are **asynchronous** — you start a run, then poll for status. All endpoints require the `api-key` header.

## Run AI Agent

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/start
cost: Uses credits
```

Start the AI agent with a prompt to build or modify your project. Supports two modes:

- **Single-prompt** (default, recommended): omit `multiPrompt`. One prompt, 10 to 30 minutes, 10 to 40 credits.
- **Multi-prompt** (rare, opt-in): include `multiPrompt`. A sequential batch that runs unsupervised. Each prompt still takes 10 to 30 minutes and 10 to 40 credits, so a 10-prompt batch can run for several hours and burn hundreds of credits.

:::warning When to use multi-prompt
Only use multi-prompt when a single job genuinely requires several sequential AI runs AND the user has explicitly accepted the cost and time. For everything else, send one prompt at a time — it is faster, cheaper, and lets the user steer between steps. Multi-prompt is unsupervised: no human approval step, no pause between prompts. If unsure, do NOT pass `multiPrompt`.
:::

:::warning Asynchronous
Returns immediately with status `"init"`. Poll `GET /agent/status` every 10–15s and show `realtimeConversation` messages live. Single-prompt: complete when status is `"done"`. Multi-prompt: poll `multiPrompt.status` until `"done"`/`"cancelled"` — the per-prompt status flips between `"init"` and `"done"` repeatedly. After every prompt completes you MUST refresh the preview URL via `GET /projects/:projectId` → `developmentUrlFieldToUse` (default `temporalDevelopmentProjectUrl` if `null`/`undefined`).
:::

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project to run the agent on |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `prompt` | string | Yes¹ | Single-prompt: the user instruction. Multi-prompt + `letTotalumDecide`: the high-level goal Totalum breaks down. Multi-prompt + `prompts[]`: ignored. |
| `inputFiles` | array | No | Reference images/files for the agent |
| `inputFiles[].name` | string | Yes | File name |
| `inputFiles[].imageDescription` | string | Yes | Description of the image content |
| `inputFiles[].url` | string | Yes | Public URL or uploaded file URL |
| `multiPrompt` | object | No | Opt into the unsupervised multi-prompt batch mode. Omit for normal use. |
| `multiPrompt.prompts` | string[] | No² | Up to 50 prompts to run sequentially. Each item must be a non-empty string. |
| `multiPrompt.letTotalumDecide` | boolean | No² | When true, Totalum plans the prompt list from the top-level `prompt`. |

¹ `prompt` is required unless `multiPrompt.prompts` is provided (then it is ignored).
² When `multiPrompt` is present, exactly one of `prompts` (non-empty array, ≤ 50 items) or `letTotalumDecide=true` must be set.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The project ID |
| `data.status` | string | Always `"init"` on success — for both single and multi-prompt mode |
| `data.message` | string | Instructions on how to poll for status (multi-prompt includes the cost/time warning) |
| `data.multiPrompt` | object \| undefined | Present only when the request had `multiPrompt`. Detailed batch status is on `GET /agent/status` under `multiPrompt`. |
| `data.multiPrompt.totalPrompts` | number \| undefined | Known when the caller provided `prompts`; absent while Totalum is still planning a `letTotalumDecide` batch. |

**Example request**

```bash
# Single-prompt (recommended)
curl -X POST -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Create a landing page with a contact form","inputFiles":[]}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start

# Multi-prompt with explicit list
curl -X POST -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"multiPrompt":{"prompts":["Set up auth","Add a Stripe checkout","Wire up the dashboard"]}}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start

# Multi-prompt with Totalum planning
curl -X POST -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Build a full SaaS billing system with auth, Stripe, and a customer portal","multiPrompt":{"letTotalumDecide":true}}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "status": "init",
    "message": "Process started, can take from 10 to 30 minutes (10 to 40 credits). Fetch GET .../agent/status every 10-15 seconds to track progress."
  }
}
```

Multi-prompt response also includes `multiPrompt`:
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "status": "init",
    "message": "Multi-prompt run started with 3 prompts. ⚠️ Multi-prompt mode is expensive and slow — only use it when a job genuinely requires multiple sequential AI runs. Each prompt typically takes 10 to 30 minutes and costs 10 to 40 credits...",
    "multiPrompt": { "totalPrompts": 3 }
  }
}
```
:::

:::danger Error · 409
```json
{
  "errors": { "errorCode": "AGENT_RUNNING", "errorMessage": "An agent is already running on this project" },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_PROMPT` | 400 | `prompt` is required (single-prompt mode, or `letTotalumDecide=true` — it is the planner goal) |
| `INVALID_MULTI_PROMPT` | 400 | `multiPrompt` was set but neither (or both) of `prompts[]` and `letTotalumDecide=true` were provided |
| `TOO_MANY_PROMPTS` | 400 | `multiPrompt.prompts` exceeds the 50-item limit |
| `INVALID_PROMPT_ITEM` | 400 | A `multiPrompt.prompts` entry is not a non-empty string |
| `AGENT_RUNNING` | 409 | An agent is already running on this project |
| `AUTO_EXECUTION_ALREADY_ACTIVE` | 409 | A previous multi-prompt batch is still active (executing/paused/awaiting approval). Cancel it first. |
| `DEPLOYMENT_RUNNING` | 409 | Cannot start agent while a deployment is in progress |
| `RECOVERY_RUNNING` | 409 | Cannot start agent while a version recovery is in progress — poll `versionRecovery` until null, then retry |
| `INSUFFICIENT_CREDITS` | 402 | Minimum 50 credits required to start agent (multi-prompt typically needs many more) |
| `PROMPT_SECURITY_VIOLATION` | 400 | Prompt failed security validation |

## Get Agent Status

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

Poll to track agent progress and get real-time messages. Poll every 10–15 seconds.

:::warning Refresh preview URL when done
When status becomes `"done"`, you MUST refresh the preview URL by calling `GET /projects/:projectId` and reading `developmentUrlFieldToUse` (`"temporalDevelopmentProjectUrl"` for the live server or `"cachedDevelopmentUrl"` for a cached snapshot). If `developmentUrlFieldToUse` is `null` or `undefined`, default to `temporalDevelopmentProjectUrl`. The URL can change after each agent run.
:::

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project to poll |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The project ID |
| `data.status` | string | `"init"` (a prompt is currently running) \| `"done"` (last prompt finished) \| `"idle"` (never run). For multi-prompt runs this flips between `"init"` and `"done"` repeatedly as the batch advances — poll `multiPrompt.status` to track the whole batch. |
| `data.startedAt` | string \| null | ISO 8601 start time of the **current** prompt (not the whole batch) |
| `data.realtimeConversation` | array | Single-prompt: messages from the current run. Multi-prompt: messages from the whole batch (scoped to `multiPrompt.startedAt`). |
| `data.realtimeConversation[].author` | string | `"user"` \| `"agent"` |
| `data.realtimeConversation[].message` | string | The message text |
| `data.realtimeConversation[].messageType` | string | `"regular"` \| `"starting"` \| `"building"` \| `"finished"` \| `"error"` \| `"limit-reached"` |
| `data.realtimeConversation[].createdAt` | string | ISO 8601 message date |
| `data.realtimeConversation[].versionId` | string \| undefined | Version created at this step |
| `data.realtimeConversation[].secretKeysNeeded` | object \| undefined | Secrets the agent needs (`key: { isProvided, description }`) |
| `data.realtimeConversation[].gitDiffUrl` | string \| undefined | Diff URL for this step |
| `data.creditsSpent` | number \| undefined | Credits spent on the current prompt (present when `status` is `"done"`) |
| `data.multiPrompt` | object \| null | Present only when a multi-prompt batch exists. The **canonical** signal for "is a multi-prompt run in flight" — poll its `status` until `"done"`/`"cancelled"`. |
| `data.multiPrompt.status` | string | `"planning"` \| `"executing"` \| `"paused"` \| `"done"` \| `"cancelled"` |
| `data.multiPrompt.totalPrompts` | number | Number of prompts in the batch (0 during planning) |
| `data.multiPrompt.currentPromptIndex` | number | Index of the in-flight prompt; -1 before the first one starts |
| `data.multiPrompt.prompts` | array | Ordered list of prompts in the batch |
| `data.multiPrompt.prompts[].order` | number | Position in the batch |
| `data.multiPrompt.prompts[].prompt` | string | The prompt text |
| `data.multiPrompt.prompts[].status` | string | `"pending"` \| `"executing"` \| `"done"` \| `"failed"` |
| `data.multiPrompt.startedAt` | string | ISO 8601 start time of the whole batch |
| `data.multiPrompt.updatedAt` | string | ISO 8601 last change to the batch state |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/status
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "status": "init",
    "startedAt": "2026-03-11T10:35:00.000Z",
    "realtimeConversation": [
      {
        "author": "agent",
        "message": "Creating the project structure...",
        "messageType": "building",
        "createdAt": "2026-03-11T10:36:00.000Z"
      },
      {
        "author": "agent",
        "message": "Project built successfully!",
        "messageType": "finished",
        "versionId": "v_abc123",
        "gitDiffUrl": "https://...",
        "secretKeysNeeded": {
          "STRIPE_SECRET_KEY": { "isProvided": false, "description": "Required for processing payments" }
        },
        "createdAt": "2026-03-11T10:50:00.000Z"
      }
    ],
    "creditsSpent": 3.2,
    "multiPrompt": null
  }
}
```

Multi-prompt example (running prompt 2 of 3):
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "status": "init",
    "startedAt": "2026-03-11T11:05:00.000Z",
    "realtimeConversation": [ /* messages from the whole batch */ ],
    "multiPrompt": {
      "status": "executing",
      "totalPrompts": 3,
      "currentPromptIndex": 1,
      "prompts": [
        { "order": 0, "prompt": "Set up auth",        "status": "done" },
        { "order": 1, "prompt": "Add Stripe checkout", "status": "executing" },
        { "order": 2, "prompt": "Wire up dashboard",   "status": "pending" }
      ],
      "startedAt": "2026-03-11T10:35:00.000Z",
      "updatedAt": "2026-03-11T11:05:00.000Z"
    }
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": { "errorCode": "PROJECT_NOT_FOUND", "errorMessage": "Project does not exist or you don't own it" },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |

## Get Full Conversation

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/agent/full-conversation
cost: Free
```

Retrieve the complete conversation history across all agent runs (not just the current one).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project whose conversation to fetch |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The project ID |
| `data.conversation` | array | All messages from all agent runs |
| `data.conversation[].author` | string | `"user"` \| `"agent"` |
| `data.conversation[].message` | string | The message text |
| `data.conversation[].messageType` | string | `"regular"` \| `"starting"` \| `"building"` \| `"finished"` \| `"error"` \| `"limit-reached"` |
| `data.conversation[].createdAt` | string | ISO 8601 message date |
| `data.conversation[].versionId` | string \| undefined | Version created at this step |
| `data.conversation[].secretKeysNeeded` | object \| undefined | API keys the agent needs (`key: { isProvided, description }`). Add missing keys as project secrets. |
| `data.conversation[].gitDiffUrl` | string \| undefined | Diff URL for this step |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/full-conversation
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "conversation": [
      { "author": "user", "message": "Create a landing page", "messageType": "regular", "createdAt": "..." },
      { "author": "agent", "message": "Building project structure...", "messageType": "building", "createdAt": "..." },
      {
        "author": "agent",
        "message": "Project built successfully!",
        "messageType": "finished",
        "versionId": "v_abc123",
        "gitDiffUrl": "https://...",
        "secretKeysNeeded": {
          "STRIPE_SECRET_KEY": { "isProvided": false, "description": "Required for processing payments" }
        },
        "createdAt": "..."
      }
    ]
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": { "errorCode": "PROJECT_NOT_FOUND", "errorMessage": "Project does not exist or you don't own it" },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |

## Stop Agent

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/stop
cost: Free
```

Send a stop signal to a running agent.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project whose agent to stop |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.message` | string | Confirmation message |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/stop
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "message": "Agent stop signal sent" } }
```
:::

:::danger Error · 400
```json
{
  "errors": { "errorCode": "NO_PROCESS_RUNNING", "errorMessage": "No agent process is currently running" },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `NO_PROCESS_RUNNING` | 400 | No agent process is currently running |

## Concurrency rules

- Only one heavy operation at a time per project: agent, deployment, version recovery, or server start.
- If deploy or recover is called and the server is not active, it auto-starts (charges `START_SERVER` credits) and returns `SERVER_NOT_READY`. Poll `GET /projects/:projectId` until `agentServerStatus` is `"Active"`, then retry.
- Agent start IS allowed during server operations (the backend waits internally).

## Complete integration flow

1. **Ensure project exists** — `POST /projects`, body `{ "projectId": "my-app" }`. Create one if the user doesn't already have a project.
2. **Send prompt to AI agent** — `POST /projects/my-app/agent/start`, body `{ "prompt": "Build a SaaS landing page" }`. Async, takes 6–15 min.
3. **Poll agent status (every 10–15s)** — `GET /projects/my-app/agent/status`. `"init"` = working (show `realtimeConversation`), `"done"` = finished (`creditsSpent` = total). On `"done"`, immediately call `GET /projects/:projectId` and refresh the preview URL.
   - **3.1 Refresh preview URL** (on page load, page refresh, and after every prompt finishes) — `GET /projects/my-app`, read `developmentUrlFieldToUse`: use `cachedDevelopmentUrl` (static snapshot while the server is archived) or `temporalDevelopmentProjectUrl` (live dev server); default `temporalDevelopmentProjectUrl` if `null`/`undefined`. Reload the iframe when the URL changes.
4. **Publish** — `POST /projects/my-app/deployments/deploy`. Builds, deploys, and assigns a public URL. Takes 1–3 min.
5. **Poll deployment status (every 10–15s)** — `GET /projects/my-app/deployments/status`; on `"success"`, get `productionProjectUrl` from `GET /projects/my-app`.
6. **(Optional) Custom domain** — `PUT /projects/my-app/domain`, body `{ "hostname": "app.yourdomain.com" }`; configure the returned DNS records; poll `customDomain.status` until `"active"`.

:::tip Key integration principles
- Agent and deployment are ASYNCHRONOUS — always poll for status.
- Show real-time agent conversation messages during generation.
- NEVER call the Totalum API from frontend code — always go through your backend server.
- ALWAYS refresh the dev preview URL (`GET /projects/:projectId`) on page load, page refresh, and after each prompt; use `developmentUrlFieldToUse`; default `temporalDevelopmentProjectUrl` if `null`/`undefined`.
- On the dev preview, don't show the production URL; display an open-in-blank link so the user always sees the realtime dev preview (production may be outdated until published).
- Keep the API key secret — it must never leave your backend server.
:::

## Global error codes

All errors are shaped as `{ "errors": { "errorCode": "CODE", "errorMessage": "..." }, "data": null }`.

| Code | HTTP | Meaning |
|---|---|---|
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for this operation |
| `PROJECT_CREDIT_LIMIT_REACHED` | 403 | Project monthly credit limit reached for that category |
| `PROJECT_NOT_ALLOWED` | 403 | API key doesn't have access to this project |
| `PROJECT_NOT_FOUND` | 404 | Project doesn't exist or you don't own it |
| `AGENT_RUNNING` | 409 | Agent already running on this project |
| `DEPLOYMENT_RUNNING` | 409 | A deployment is in progress |
| `RECOVERY_RUNNING` | 409 | A version recovery is in progress |
| `SERVER_NOT_READY` | 409 | Server auto-starting, poll until Active |
| `NO_DEPLOYMENT` | 404 | No deployment found. Deploy first and wait until status is "success" |
| `MISSING_PROMPT` | 400 | prompt field is required |
| `MISSING_PROJECT_ID` | 400 | projectId field is required |
| `INVALID_PROJECT_NAME` | 400 | Invalid projectId format |
| `INVALID_PROJECT_NAME_LENGTH` | 400 | Project name must be 4-35 characters |
| `PROJECT_ALREADY_EXISTS` | 409 | A project with this ID already exists |
| `PLAN_NOT_API` | 400 | Only API plan projects can be deleted |
| `PROMPT_SECURITY_VIOLATION` | 400 | Prompt failed security validation |
| `NO_PROCESS_RUNNING` | 400 | No agent process is currently running |
| `MISSING_FILE` | 400 | file field is required (multipart) |
| `UPLOAD_FAILED` | 500 | File upload failed |
| `MISSING_VERSION_ID` | 400 | versionId is required |
| `MISSING_SECRET_FIELDS` | 400 | secretName and secretValue are required |
| `INVALID_SECRET_KEY_NAME` | 400 | Invalid secret key name format |
| `MISSING_SECRET_ID` | 400 | secretId is required |
| `MISSING_HOSTNAME` | 400 | hostname is required |
| `MISSING_TABLE_NAME` | 400 | tableName is required |
| `GET_ACCOUNT_ERROR` | 400 | Internal error fetching account info |
| `LIST_PROJECTS_ERROR` | 400 | Internal error listing projects |
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests (max 10 project creations per 60s) |
| `MISSING_WEBHOOK_FIELDS` | 400 | url and event are required |
| `INVALID_WEBHOOK_URL` | 400 | Webhook URL must use HTTPS |
| `INVALID_WEBHOOK_EVENT` | 400 | Event not in allowed list |
| `WEBHOOK_EVENT_ALREADY_EXISTS` | 409 | A webhook for this event already exists |
| `WEBHOOK_NOT_FOUND` | 404 | Webhook not found |
| `MISSING_GITHUB_FIELDS` | 400 | token and repositoryFullName are required |
| `GITHUB_VALIDATION_FAILED` | 400 | Token invalid, missing permissions, or repo not found |
| `GITHUB_SECRET_STORE_ERROR` | 400 | Failed to store GitHub credentials |
| `GITHUB_SYNC_FAILED` | 400 | Initial sync with GitHub failed |
| `GITHUB_NOT_CONNECTED` | 400 | GitHub is not connected to this project |
| `GITHUB_PULL_ERROR` | 400 | Failed to pull changes from GitHub |
| `MISSING_LIMIT_FIELDS` | 400 | At least one credit limit field is required |
| `INVALID_LIMIT` | 400 | Credit limit must be a positive number |
| `INVALID_SYNC_DIRECTION` | 400 | syncDirection must be "totalum_to_github" or "github_to_totalum" |


---

# Deployments

> Publish your project to production and track the rollout.

Deployments build your project and publish it to its production URL. Deploying is **asynchronous** — start the deploy, then poll for status. All endpoints require the `api-key` header.

## Deploy to Production

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/deployments/deploy
cost: Uses credits
```

Build and deploy your project to a production URL. This endpoint is asynchronous — it returns immediately with status `"deploying"` and the deployment continues in the background for 2 to 5 minutes.

Poll `GET /projects/:projectId/deployments/status` every 10–15 seconds until status is `"success"`, then read the public URL from `GET /projects/:projectId` → `productionProjectUrl`. If the server is not active, it auto-starts (charging extra `START_SERVER` credits) and returns `SERVER_NOT_READY`; poll `GET /projects/:projectId` until `agentServerStatus` is `"Active"`, then retry.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.projectId` | string | The project ID |
| `data.status` | string | Always `"deploying"` on success |
| `data.message` | string | Instructions on how to poll for status |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/deployments/deploy
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "status": "deploying",
    "message": "Deployment started. It will take from 2 to 5 minutes. Fetch GET .../deployments/status every 10-15 seconds to track progress."
  }
}
```
:::

:::danger Error · 409
```json
{
  "errors": {
    "errorCode": "DEPLOYMENT_RUNNING",
    "errorMessage": "A deployment is already in progress"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `AGENT_RUNNING` | 409 | Cannot deploy while agent is running |
| `DEPLOYMENT_RUNNING` | 409 | A deployment is already in progress |
| `RECOVERY_RUNNING` | 409 | A version recovery is in progress |
| `SERVER_NOT_READY` | 409 | Server auto-starting, poll until Active then retry |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for deployment |

## Get Deployment Status

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

Check the current deployment status. Poll until status is `"success"`.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.status` | string \| null | `"deploying"` \| `"success"` \| `"error"` \| null (if never deployed) |
| `data.createdAt` | string \| null | ISO 8601 deployment date |
| `data.versionId` | string \| undefined | Version that was deployed |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/deployments/status
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "status": "success",
    "createdAt": "2026-03-11T11:00:00.000Z",
    "versionId": "v_abc123"
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |


---

# Server

> Control the development server and inspect its logs.

These endpoints manage your project's development server. Starting the server is **asynchronous**. All endpoints require the `api-key` header.

## Start or Restart Server

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/agent/server/start-or-restart
cost: Uses credits
```

Start or restart the development server for your project. This endpoint is asynchronous — it returns immediately with status `"starting"` and the server startup continues in the background for 2 to 4 minutes.

Poll `GET /projects/:projectId` every 10–15 seconds until `agentServerStatus` is `"Active"`.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.message` | string | Status message |
| `data.status` | string | Always `"starting"` |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/server/start-or-restart
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "message": "Server start/restart initiated",
    "status": "starting"
  }
}
```
:::

:::danger Error · 409
```json
{
  "errors": {
    "errorCode": "AGENT_RUNNING",
    "errorMessage": "Cannot restart server while agent is running"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `AGENT_RUNNING` | 409 | Cannot restart server while agent is running |
| `DEPLOYMENT_RUNNING` | 409 | Cannot restart server while deployment is in progress |
| `RECOVERY_RUNNING` | 409 | Cannot restart server while version recovery is in progress |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for server start |

## Get Dev Server Logs

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

Retrieve backend development server stdout/stderr output.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.logs` | string | Development server stdout/stderr output |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/backend/dev/logs
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": { "logs": "Server running on port 3000\n..." }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |


---

# Source Code & Files

> Get a signed download link for your source, or upload assets for the agent.

These endpoints let you download your project's source code and upload files (images, designs) for the AI agent to use as input. Both require the `api-key` header.

## Download Source Code

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/source-code
cost: Uses credits
```

Get a signed URL to download the project source code.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.filesCount` | number | Total files in project |
| `data.lastCommitSha` | string \| undefined | Latest git commit SHA |
| `data.downloadUrl` | string \| null | Signed download URL (expires in minutes) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/source-code
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "filesCount": 47,
    "lastCommitSha": "a1b2c3d",
    "downloadUrl": "https://storage.googleapis.com/..."
  }
}
```
:::

:::danger Error · 402
```json
{
  "errors": {
    "errorCode": "INSUFFICIENT_CREDITS",
    "errorMessage": "Not enough credits for source code download"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for source code download |

## Upload File

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/files/upload
cost: Uses credits
```

Upload a file to use as agent input (images, designs, etc.). Max 12MB. Send as `multipart/form-data` with the field `file`.

**Path parameters**

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

**Body parameters** (multipart/form-data)

| Field | Type | Required | Description |
|---|---|---|---|
| `file` | File | Yes | The file to upload (max 12MB) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.fileNameId` | string | Stored file name identifier |
| `data.url` | string | Signed download URL, use in agent `inputFiles` |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -F "file=@./logo.png" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/files/upload
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "fileNameId": "logo_abc123.png",
    "url": "https://storage.googleapis.com/..."
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "MISSING_FILE",
    "errorMessage": "file is required (multipart form field \"file\")"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_FILE` | 400 | file is required (multipart form field "file") |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for file upload |
| `UPLOAD_FAILED` | 500 | File upload failed |


---

# Versions

> Browse your project's version history and roll back to an earlier state.

Every completed prompt produces a version. You can list the version history and recover a previous version. Both require the `api-key` header.

## List Versions

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

Get all project versions with pagination.

**Path parameters**

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

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `limit` | number | No | Number of versions to return (default: 20) |
| `skip` | number | No | Number of versions to skip (default: 0) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.versions` | array | Array of version objects |
| `data.versions[]._id` | string | Version ID (use for recover) |
| `data.versions[].name` | string | Version display name |
| `data.versions[].commitMessage` | string \| undefined | Git commit message |
| `data.versions[].prompt` | string \| undefined | The prompt that created this version |
| `data.versions[].createdAt` | string | ISO 8601 creation date |
| `data.totalCount` | number | Total versions available |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/versions?limit=20&skip=0"
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "versions": [
      {
        "_id": "v_abc123",
        "name": "Version 3",
        "commitMessage": "Added contact form",
        "prompt": "Add a contact form to the landing page",
        "createdAt": "2026-03-11T10:45:00.000Z"
      }
    ],
    "totalCount": 3
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |

## Recover Version

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/versions/:id/recover
cost: Uses credits
```

Restore a previous version of the project. This endpoint is asynchronous: it returns immediately and the recovery continues in the background for 1 to 4 minutes.

:::warning Asynchronous
Poll `GET /projects/:projectId` every 10–15 seconds and watch the `versionRecovery` field. While the recovery is running, `versionRecovery.status` is `"recovering"`. Recovery is complete the moment `versionRecovery` becomes `null`. If `versionRecovery.status` is `"error"`, surface `versionRecovery.errorMessage` to the user. Do NOT poll `agentProcessStatus` for recovery — the agent is not involved in a version recovery.
:::

:::note
If the server is not active, it auto-starts (charges START_SERVER credits extra) and returns `SERVER_NOT_READY`.
:::

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project ID |
| `id` | string | The version ID to recover (from GET /versions) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.message` | string | Confirmation message |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/versions/v_abc123/recover
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "message": "Version recovery initiated" } }
```
:::

:::danger Error · 402
```json
{
  "errors": {
    "errorCode": "INSUFFICIENT_CREDITS",
    "errorMessage": "Not enough credits for version recovery"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_VERSION_ID` | 400 | versionId is required |
| `AGENT_RUNNING` | 409 | Cannot recover while agent is running |
| `DEPLOYMENT_RUNNING` | 409 | Cannot recover while deployment is in progress |
| `RECOVERY_RUNNING` | 409 | A version recovery is already in progress |
| `SERVER_NOT_READY` | 409 | Server auto-starting, poll until Active then retry |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for version recovery |


---

# Secrets

> Manage environment variables, encrypted at rest and synced to the sandbox .env.

Secrets are environment variables for your project. They are encrypted at rest and automatically synced to the sandbox `.env`. Secret values are never returned by the API. Both endpoints require the `api-key` header.

## Create Secret

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

Add an environment variable. Encrypted at rest, auto-synced to the sandbox `.env`.

**Path parameters**

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

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `secretName` | string | Yes | Environment variable name |
| `secretValue` | string | Yes | The secret value, stored encrypted |
| `environment` | string | No | `"development"` \| `"production"` \| `"both"` (default: `"both"`) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data._id` | string | ID of the created secret |
| `data.secretName` | string | The secret name |
| `data.environment` | string | `"development"` \| `"production"` \| `"both"` |
| `data.createdAt` | string | ISO 8601 creation date |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"secretName":"STRIPE_KEY","secretValue":"sk_live_abc123","environment":"both"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/secrets
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "secretName": "STRIPE_KEY",
    "environment": "both",
    "createdAt": "2026-03-11T10:30:00.000Z"
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "MISSING_SECRET_FIELDS",
    "errorMessage": "secretName and secretValue are required"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_SECRET_FIELDS` | 400 | secretName and secretValue are required |
| `INVALID_SECRET_KEY_NAME` | 400 | Invalid secret key name format |

## Delete Secret

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

Remove an environment variable by ID. The sandbox `.env` is synced automatically.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project ID |
| `secretId` | string | The secret ID to delete |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.success` | boolean | true on success |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/secrets/65f1a2b3c4d5e6f7a8b9c0d1
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "success": true } }
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_SECRET_ID` | 400 | secretId is required |


---

# Custom Domains

> Point your own subdomain at a deployed project.

Custom domains let you serve a deployed project from your own subdomain (e.g. `app.yourdomain.com`). Both endpoints require the `api-key` header.

## Add Custom Domain

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/domain
cost: Uses credits
```

Attach a custom subdomain (e.g. `app.yourdomain.com`) to your deployed project.

:::warning Deploy first
You MUST deploy your project first AND wait until the deployment status is `"success"` before calling this endpoint. If the deployment is still in progress or no deployment exists, this returns a `NO_DEPLOYMENT` error.
:::

**Path parameters**

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

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `hostname` | string | Yes | The subdomain to add (e.g. `app.yourdomain.com`) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.success` | boolean | true on success |
| `data.hostname` | string | The configured hostname |
| `data.status` | string | `"pending_validation"` initially |
| `data.dnsRecordsToAdd` | array \| undefined | DNS records to add at your provider |
| `data.dnsRecordsToAdd[].type` | string | `"CNAME"` or `"TXT"` |
| `data.dnsRecordsToAdd[].name` | string | DNS record name (zone-relative, e.g. `"app"` or `"_cf-custom-hostname.app"`) |
| `data.dnsRecordsToAdd[].value` | string | DNS record value (e.g. `"my-app.totalum-project.com"` or verification token) |

**Example request**

```bash
curl -X PUT \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"app.yourdomain.com"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/domain
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "success": true,
    "hostname": "app.yourdomain.com",
    "status": "pending_validation",
    "dnsRecordsToAdd": [
      { "type": "CNAME", "name": "app", "value": "my-app.totalum-project.com" },
      { "type": "TXT", "name": "_cf-custom-hostname.app", "value": "abc123-verification-token" }
    ]
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "NO_DEPLOYMENT",
    "errorMessage": "No deployment found. Deploy first and wait until status is \"success\""
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_HOSTNAME` | 400 | hostname is required (e.g. app.yourdomain.com) |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits to add custom domain |
| `NO_DEPLOYMENT` | 404 | No deployment found. Deploy first and wait until status is "success" |

:::note
After configuring DNS, poll `GET /projects/:projectId` and check `customDomain.status` until it is `"active"`.
:::

:::tip Root domain
Custom domains require a subdomain (e.g. `www.yourdomain.com`). If you want `yourdomain.com` (without www) to reach your project, add `www.yourdomain.com` as the custom domain, then create a redirect in your DNS provider from `yourdomain.com` to `www.yourdomain.com`. Most providers offer this as "URL redirect" or "domain forwarding" in their DNS settings.
:::

## Remove Custom Domain

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

Detach the custom domain from your project.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.message` | string | Confirmation message |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/domain
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "message": "Custom domain removed" } }
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `NO_DEPLOYMENT` | 404 | No deployment found. Deploy first and wait until status is "success" |


---

# Database

> Read the schema, query with rich filters, and mutate records in your project database.

The database API exposes your project's tables and records. You can read the table structure, query with filtering/sorting/pagination/aggregation and nested relations up to 6 levels deep, and create or edit records. All endpoints require the `api-key` header and are **free**.

## Get Database Tables Structure

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/database/tables-structure
cost: Free
```

Retrieve all database table definitions including field names, types, and configuration.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.tables` | array | Array of table definitions |
| `data.tables[]._id` | string | Table ID |
| `data.tables[].type` | string | Table name (snake_case) |
| `data.tables[].label` | string | Human-friendly display name |
| `data.tables[].description` | string | Table description |
| `data.tables[].icon` | string | FontAwesome icon class |
| `data.tables[].properties` | object | Map of property name to property definition |
| `data.tables[].properties.{name}.id` | string | Property ID |
| `data.tables[].properties.{name}.name` | string | Property field name |
| `data.tables[].properties.{name}.propertyType` | string | `"string"` \| `"number"` \| `"date"` \| `"options"` \| `"file"` \| `"long-string"` \| `"objectReference"` |
| `data.tables[].properties.{name}.label` | string | Human-friendly field label |
| `data.tables[].properties.{name}.description` | string | Field description |
| `data.tables[].properties.{name}.objectReference` | object | Relationship config (if objectReference type) |
| `data.tables[].properties.{name}.typeExtras` | object | Type-specific config (options values, date settings, etc.) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/tables-structure
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "tables": [
      {
        "_id": "65f1a2b3c4d5e6f7",
        "type": "customers",
        "label": "Customers",
        "description": "Customer records",
        "icon": "fa-solid fa-users",
        "properties": {
          "name": {
            "id": "prop_abc123",
            "name": "name",
            "propertyType": "string",
            "label": "Full Name"
          },
          "email": {
            "id": "prop_def456",
            "name": "email",
            "propertyType": "string",
            "label": "Email",
            "typeExtras": { "string": { "type": "link" } }
          }
        }
      }
    ]
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |

## Query Database

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/query
cost: Free
```

Query records from any table with advanced filtering, sorting, pagination, aggregations, and nested related data up to 6 levels deep.

**Path parameters**

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

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `tableName` | string | Yes | The table name to query (use `type` from tables-structure response) |
| `queryOptions` | object | No | Advanced query options |
| `queryOptions._filter` | object | No | Field filters (see filter operators below) |
| `queryOptions._sort` | object | No | Sort by fields: `{ fieldName: "asc" \| "desc" }` |
| `queryOptions._limit` | number | No | Max records to return (default 50, max 1000) |
| `queryOptions._offset` | number | No | Records to skip (for pagination) |
| `queryOptions._select` | object | No | Include only specified fields: `{ fieldName: true }`. Cannot use with `_omit` |
| `queryOptions._omit` | object | No | Exclude specified fields: `{ fieldName: true }`. Cannot use with `_select` |
| `queryOptions._count` | boolean | No | Adds `_count._total` with total matching records (before pagination) |
| `queryOptions._aggregate` | object | No | Aggregations: `{ _sum: { field: true }, _avg, _min, _max, _count }` |
| `queryOptions._groupBy` | string \| string[] | No | Group results by field(s). Requires `_aggregate` |
| `queryOptions.[property]` | true \| object | No | Expand related data. `true` = all fields, object = nested `queryOptions` |

**Filter operators** (use inside `_filter`)

- Exact match: `{ "status": "active" }`
- With operator: `{ "fieldName": { "operator": value } }`

| Operator | Description | Example |
|---|---|---|
| `gte` | Greater than or equal | `{ "age": { "gte": 18 } }` |
| `lte` | Less than or equal | `{ "age": { "lte": 65 } }` |
| `gt` | Greater than | `{ "price": { "gt": 0 } }` |
| `lt` | Less than | `{ "price": { "lt": 100 } }` |
| `ne` | Not equal | `{ "status": { "ne": "deleted" } }` |
| `in` | Matches any value in array | `{ "status": { "in": ["active", "pending"] } }` |
| `nin` | Matches none in array | `{ "role": { "nin": ["admin", "super"] } }` |
| `regex` | Regex pattern (+options for flags) | `{ "email": { "regex": "@gmail", "options": "i" } }` |
| `contains` | Case-insensitive contains | `{ "name": { "contains": "john" } }` |
| `startsWith` | Case-insensitive starts with | `{ "name": { "startsWith": "J" } }` |
| `endsWith` | Case-insensitive ends with | `{ "email": { "endsWith": ".com" } }` |
| `_or` | OR logic (array of conditions) | `{ "_or": [{ "status": "active" }, { "role": "admin" }] }` |

**Nested queries** (related data)

Expand related tables by using the property name as a key in `queryOptions`.

- Shorthand: `{ "orders": true }` — expands all related orders
- Full: `{ "orders": { "_filter": {...}, "_sort": {...}, "_limit": 5 } }` — with options (max 300 children)
- `{ "orders": { "_has": true } }` — only parents with at least one matching child
- `{ "orders": { "_has": "none" } }` — only parents with zero matching children
- `{ "orders": { "_count": true } }` — include child count per parent
- Nesting up to 6 levels deep supported

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.results` | array | Array of matching records |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"tableName":"customers","queryOptions":{"_filter":{"status":"active","age":{"gte":18}},"_sort":{"createdAt":"desc"},"_limit":10,"orders":{"_filter":{"total":{"gt":50}},"_limit":5}}}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/query
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "results": [
      {
        "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
        "name": "John Doe",
        "email": "john@example.com",
        "status": "active",
        "age": 30,
        "orders": [
          { "_id": "...", "total": 120.50, "createdAt": "2026-03-09T..." }
        ],
        "createdAt": "2026-03-10T08:00:00.000Z"
      }
    ]
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "MISSING_TABLE_NAME",
    "errorMessage": "tableName is required"
  },
  "data": null
}
```
:::

**More filter examples**

```js
// OR logic
"_filter": { "_or": [{ "status": "active" }, { "role": "admin" }] }
// IN operator
"_filter": { "status": { "in": ["active", "pending"] } }
// Regex case-insensitive
"_filter": { "email": { "regex": "@gmail", "options": "i" } }
// Aggregation with groupBy
"_aggregate": { "_sum": { "total": true }, "_count": true }, "_groupBy": "status"
// Nested: only parents with children matching filter
"orders": { "_has": true, "_filter": { "total": { "gt": 100 } } }
```

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_TABLE_NAME` | 400 | tableName is required |

## Create Database Record

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/database/records
cost: Free
```

Create a new record in any table. The `_id` field is auto-generated by the database — do not include it in the request body. The response returns the full created record including the generated `_id` and any default fields.

**Path parameters**

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

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `tableName` | string | Yes | The table name to insert into (use `type` from tables-structure response) |
| `data` | object | Yes | The record properties as key-value pairs. Do NOT include `_id` (auto-generated) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The full created record including the auto-generated `_id` and all fields |
| `data._id` | string | The auto-generated unique identifier for the new record |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"tableName":"customers","data":{"name":"John Doe","email":"john@example.com","status":"active","age":30}}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "name": "John Doe",
    "email": "john@example.com",
    "status": "active",
    "age": 30,
    "createdAt": "2026-03-25T10:00:00.000Z",
    "updatedAt": "2026-03-25T10:00:00.000Z"
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "MISSING_DATA",
    "errorMessage": "data is required and must be an object"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_TABLE_NAME` | 400 | tableName is required |
| `MISSING_DATA` | 400 | data is required and must be an object |
| `TABLE_NOT_FOUND` | 400 | Table doesn't exist in the project |

## Edit Database Record

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

Update specific fields of an existing record by its ID. Only the fields included in `data` will be modified — all other fields remain unchanged. The response returns the full updated record.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | The project ID |
| `recordId` | string | The `_id` of the record to update |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `tableName` | string | Yes | The table name (use `type` from tables-structure response) |
| `data` | object | Yes | The properties to update as key-value pairs. Only included fields are modified |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The full updated record with all fields (including unchanged ones) |
| `data._id` | string | The record ID |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"tableName":"customers","data":{"email":"newemail@example.com","age":31}}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/database/records/65f1a2b3c4d5e6f7a8b9c0d1
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "name": "John Doe",
    "email": "newemail@example.com",
    "status": "active",
    "age": 31,
    "createdAt": "2026-03-25T10:00:00.000Z",
    "updatedAt": "2026-03-25T10:05:00.000Z"
  }
}
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_PROJECT_ID` | 400 | projectId is required |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or you don't own it |
| `MISSING_RECORD_ID` | 400 | recordId is required |
| `MISSING_TABLE_NAME` | 400 | tableName is required |
| `MISSING_DATA` | 400 | data is required and must be an object |
| `TABLE_NOT_FOUND` | 400 | Table doesn't exist in the project |


---

# Analytics

> Monitor daily credit usage across development and infrastructure categories.

The analytics endpoint returns daily credit spending you can render in charts or reports. It requires the `api-key` header.

## Spending Analytics

```endpoint
method: GET
path: /api/v1/credits/spending-analytics
cost: Free
```

Get daily credit spending data for charts and reports. Returns data aggregated by day, category (development/infrastructure), and usage type.

:::note
The path is `/api/v1/credits/spending-analytics` — it is **NOT** under `/vcaas`.
:::

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `from` | string | Yes | Start date (`YYYY-MM-DD`), max 90 days before `to` |
| `to` | string | Yes | End date (`YYYY-MM-DD`) |
| `projectId` | string | No | Filter by project ID |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.daily` | array | Array of daily spending objects |
| `data.daily[].date` | string | Date (`YYYY-MM-DD`) |
| `data.daily[].development` | number | Development credits spent that day |
| `data.daily[].infrastructure` | number | Infrastructure credits spent that day |
| `data.daily[].byType` | object | Credits by usage type (e.g. prompt, deploy, chatgpt) |
| `data.totals.development` | number | Total development credits in range |
| `data.totals.infrastructure` | number | Total infrastructure credits in range |
| `data.totals.total` | number | Total credits in range |
| `data.totals.byType` | object | Total credits by usage type |
| `data.projects` | array | List of project IDs with spending data |

**Usage types in `byType`:** `prompt`, `deploy`, `start_server`, `get_source_code`, `recover_version`, `upload_file`, `add_custom_domain`, `chatgpt`, `image_generation`, `video_analysis`, `audio_transcription`, `email`, `pdf`, `document_scan`, `web_scraper`, `file_upload`.

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://api-accounts.totalum.app/api/v1/credits/spending-analytics?from=2026-04-01&to=2026-04-04"
```

Add `&projectId=my-app` to filter by a single project:

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://api-accounts.totalum.app/api/v1/credits/spending-analytics?from=2026-04-01&to=2026-04-04&projectId=my-app"
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "daily": [
      { "date": "2026-04-01", "development": 15, "infrastructure": 3, "byType": { "prompt": 10, "deploy": 5, "chatgpt": 2, "pdf": 1 } },
      { "date": "2026-04-02", "development": 20, "infrastructure": 5, "byType": { "prompt": 15, "deploy": 3, "start_server": 2, "email": 3 } }
    ],
    "totals": { "development": 35, "infrastructure": 8, "total": 43, "byType": { "prompt": 25, "deploy": 8, "chatgpt": 2, "pdf": 1, "email": 3 } },
    "projects": ["my-app", "other-project"]
  }
}
```
:::

:::info
Analytics data is available for the last 90 days; older data is automatically cleaned up.
:::


---

# Webhooks

> Register, list, and delete webhook subscriptions for project events.

Webhooks let you subscribe to project events and receive an HTTP POST to your URL when the event occurs. All endpoints require the `api-key` header and are **free**.

**Available events**

| Event | Fired when |
|---|---|
| `agent.prompt.finished` | The AI agent finishes processing a prompt |
| `project.credit_limit.reached` | A project reaches its credit limit |

**Webhook payload** delivered to your URL:

```json
{
  "event": "agent.prompt.finished",
  "timestamp": "2026-03-17T10:30:00.000Z",
  "data": {
    "projectId": "my-app",
    "status": "done",
    "prompt": "Build a SaaS landing page..."
  }
}
```

## Register Webhook

```endpoint
method: PUT
path: /api/v1/vcaas/webhooks
cost: Free
```

Subscribe to an event. You'll receive a POST to your URL when the event occurs.

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | Yes | HTTPS URL to receive webhook POST |
| `event` | string | Yes | Event type to subscribe to. See available events above |
| `headers` | object | No | Custom headers to include in webhook POST |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.id` | string | Webhook ID |
| `data.url` | string | The registered URL |
| `data.headers` | object | Custom headers |
| `data.event` | string | The subscribed event |
| `data.createdAt` | string | ISO 8601 date |

**Example request**

```bash
curl -X PUT \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yourserver.com/webhook","event":"agent.prompt.finished"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/webhooks
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "url": "https://yourserver.com/webhook",
    "event": "agent.prompt.finished",
    "createdAt": "2026-03-17T10:00:00.000Z"
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "MISSING_WEBHOOK_FIELDS",
    "errorMessage": "url and event are required"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_WEBHOOK_FIELDS` | 400 | url and event are required |
| `INVALID_WEBHOOK_URL` | 400 | Webhook URL must use HTTPS |
| `INVALID_WEBHOOK_EVENT` | 400 | Event not in allowed list |
| `WEBHOOK_EVENT_ALREADY_EXISTS` | 409 | A webhook for this event already exists |

## List Webhooks

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

Get all your registered webhooks.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.webhooks` | array | Array of webhooks |
| `data.webhooks[].id` | string | Webhook ID |
| `data.webhooks[].url` | string | Destination URL |
| `data.webhooks[].headers` | object | Custom headers |
| `data.webhooks[].event` | string | Subscribed event |
| `data.webhooks[].createdAt` | string | ISO 8601 date |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/webhooks
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "webhooks": [
      {
        "id": "65f1a2b3c4d5e6f7a8b9c0d1",
        "url": "https://yourserver.com/webhook",
        "event": "agent.prompt.finished",
        "createdAt": "2026-03-17T10:00:00.000Z"
      }
    ]
  }
}
```
:::

## Delete Webhook

```endpoint
method: DELETE
path: /api/v1/vcaas/webhooks/:webhookId
cost: Free
```

Remove a webhook subscription.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `webhookId` | string | The webhook ID |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.success` | boolean | true on success |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/webhooks/65f1a2b3c4d5e6f7a8b9c0d1
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "success": true } }
```
:::

:::danger Error · 404
```json
{
  "errors": {
    "errorCode": "WEBHOOK_NOT_FOUND",
    "errorMessage": "Webhook not found"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `WEBHOOK_NOT_FOUND` | 404 | Webhook not found |


---

# GitHub API

> Two-way GitHub sync for your project via a fine-grained personal access token.

:::note
For a step-by-step, user-facing walkthrough, see the [GitHub integration guide](/docs/developers/github). The endpoints below are the raw API reference.
:::

Connect a GitHub repository to your project for two-way sync. After connecting, Totalum auto-pushes to `develop` after every completed prompt and pushes to `main` when you publish (deploy). All endpoints require the `api-key` header and are **free**.

## Connect GitHub Repository

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/github/connect
cost: Free
```

Connect a GitHub repository to your project using a Fine-grained Personal Access Token (PAT). This validates permissions, stores credentials, sets up branches (`develop` and `main`), and performs the initial sync. After connecting, Totalum automatically pushes to `develop` after every completed prompt, and pushes to `main` when you publish (deploy) the project.

:::tip Create a GitHub token
1. Go to `https://github.com/settings/personal-access-tokens`.
2. Click "Generate new token".
3. Select "Only select repositories" and choose your repo.
4. Under "Repository permissions", set: **Contents:** Read and Write; **Pull requests:** Read and Write; **Administration:** Read and Write.
5. Click "Generate token" and copy it (it starts with `github_pat_`).
:::

:::note Branch setup
For empty repositories, Totalum creates the `develop` and `main` branches automatically. For non-empty repositories, both branches must already exist.
:::

**`syncDirection`** (controls behavior when the repo has existing content with no common git history):

- `"totalum_to_github"` (default) — pushes the Totalum project code to GitHub, replacing the repository content on both `develop` and `main` branches.
- `"github_to_totalum"` — pulls the GitHub repository code into Totalum, replacing the current project code. If the `develop` branch doesn't exist on GitHub, it is created from `main`.

**Path parameters**

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

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | Yes | GitHub Fine-grained Personal Access Token |
| `repositoryFullName` | string | Yes | Full repository name (e.g. `"owner/repo"`) |
| `syncDirection` | string | No | `"totalum_to_github"` (default) or `"github_to_totalum"`. Controls behavior when repo has existing content with no common history |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.connected` | boolean | true if connection was successful |
| `data.repositoryFullName` | string | The connected repository |
| `data.syncAction` | string | `"push_new"`, `"push"`, `"pull"`, `"merge_and_push"`, or `"already_synced"` |
| `data.repoHasContent` | boolean | Whether the repo had existing content |
| `data.requiresRebuild` | boolean | Whether a rebuild was triggered. If true, poll `GET /github/pull-status` until status is `"success"` or `"error"` |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"token":"github_pat_xxx","repositoryFullName":"myuser/my-repo","syncDirection":"totalum_to_github"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/github/connect
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "connected": true,
    "repositoryFullName": "myuser/my-repo",
    "syncAction": "push_new",
    "repoHasContent": false,
    "requiresRebuild": false
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "MISSING_GITHUB_FIELDS",
    "errorMessage": "token and repositoryFullName are required"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `MISSING_GITHUB_FIELDS` | 400 | token and repositoryFullName are required |
| `INVALID_SYNC_DIRECTION` | 400 | syncDirection must be "totalum_to_github" or "github_to_totalum" |
| `GITHUB_VALIDATION_FAILED` | 400 | Token invalid, missing permissions, or repo not found |
| `GITHUB_SECRET_STORE_ERROR` | 400 | Failed to store GitHub credentials |
| `GITHUB_SYNC_FAILED` | 400 | Initial sync with GitHub failed |
| `AGENT_RUNNING` | 409 | Cannot connect while agent is running |
| `SERVER_NOT_READY` | 409 | Server auto-starting, poll until Active then retry |

## Disconnect GitHub

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

Remove the GitHub integration from your project. Deletes the stored token and repository credentials. Code already in your project is not affected.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.success` | boolean | true on success |
| `data.message` | string | Confirmation message |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/github/connect
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "success": true, "message": "GitHub disconnected successfully" } }
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "GITHUB_NOT_CONNECTED",
    "errorMessage": "GitHub is not connected to this project"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `GITHUB_NOT_CONNECTED` | 400 | GitHub is not connected to this project |

## Get GitHub Status

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

Check whether GitHub is connected and the token is still valid.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.connected` | boolean | Whether GitHub is connected |
| `data.tokenValid` | boolean | Whether the PAT token is still valid |
| `data.tokenExpired` | boolean | True when the last linked PAT was detected as expired/revoked. Sticky until the user reconnects. Use this to prompt the user to reconnect |
| `data.repositoryFullName` | string | Connected repository, or last-known repo when tokenExpired is true |
| `data.developBranch` | string | `"develop"` (only if connected) |
| `data.productionBranch` | string | `"main"` (only if connected) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/github/status
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "connected": true,
    "tokenValid": true,
    "tokenExpired": false,
    "repositoryFullName": "myuser/my-repo",
    "developBranch": "develop",
    "productionBranch": "main"
  }
}
```
:::

## Pull from GitHub

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/github/pull
cost: Free
```

Pull the latest changes from the GitHub `develop` branch into your project. If files changed, the server rebuilds automatically in the background. Poll the pull status endpoint until complete. This is an async operation.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.status` | string | `"pulling"` (async rebuild started) or `"no_changes"` |
| `data.message` | string | Status message |
| `data.filesUpdated` | number | Number of files updated |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/github/pull
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "status": "pulling",
    "message": "Pull started successfully",
    "filesUpdated": 5
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "GITHUB_NOT_CONNECTED",
    "errorMessage": "GitHub is not connected (connect first)"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `GITHUB_NOT_CONNECTED` | 400 | GitHub is not connected (connect first) |
| `GITHUB_PULL_ERROR` | 400 | Failed to pull changes |
| `AGENT_RUNNING` | 409 | Cannot pull while agent is running |
| `SERVER_NOT_READY` | 409 | Server auto-starting, poll until Active |

## Get Pull Status

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/github/pull-status
cost: Free
```

Poll this endpoint after calling Pull from GitHub, OR after a Connect GitHub call that returned `requiresRebuild: true` (e.g. `github_to_totalum` direction). Returns the current rebuild status. Auto-completes after 5 minutes.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.status` | string | `"pulling"`, `"success"`, `"error"`, or null (no pull) |
| `data.createdAt` | string | ISO 8601 date when the pull started |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/github/pull-status
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "status": "success", "createdAt": "2026-03-17T10:00:00.000Z" } }
```
:::

## Download Environment Variables

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

Get the project's environment variables as `.env` file content for both development and production environments. Use this to set up a local development environment or configure production deployments.

**Path parameters**

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

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.envDev` | string | .env file content for development (KEY=VALUE format, one per line) |
| `data.envProd` | string | .env file content for production (KEY=VALUE format, one per line) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/github/env
```

:::success Success · 200 OK
```json
{
  "errors": null,
  "data": {
    "envDev": "# Totalum Configuration\nTOTALUM_API_KEY=tlm_sk_xxx\nBETTER_AUTH_SECRET=xxxx\nNODE_ENV=development\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n\n# User-defined environment variables\n...",
    "envProd": "# Totalum Configuration\nTOTALUM_API_KEY=tlm_sk_xxx\nBETTER_AUTH_SECRET=xxxx\nNODE_ENV=production\nNEXT_PUBLIC_APP_URL=https://my-app.totalum-project.com\n\n# User-defined environment variables\n..."
  }
}
```
:::

:::danger Error · 400
```json
{
  "errors": {
    "errorCode": "ENV_DOWNLOAD_ERROR",
    "errorMessage": "Failed to get environment variables"
  },
  "data": null
}
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `ENV_DOWNLOAD_ERROR` | 400 | Failed to get environment variables |
