Docs
BlogHomeStart building

Projects

Manage the lifecycle of your vibe coding projects.


Projects are the top-level unit of the Totalum App Builder 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

POST/api/v1/vcaas/projectsUses credits

Create a new vibe coding project.

Body parameters

FieldTypeRequiredDescription
projectIdstringYes4-35 chars, lowercase letters + numbers + hyphens, must start with letter. Permanent — it is the organization id and the production hostname, and cannot be renamed later
descriptionstringNoProject description, max 500 characters
labelstringNoHuman display name, max 80 characters. This is the part you can change later — see Update Project. Omitted → clients show the projectId
groupIdstringNoFile the project under an existing project group. Omitted → ungrouped

Response fields

FieldTypeDescription
data.projectIdstringThe project ID
data.descriptionstringProject description
data.planstringAlways "api" for projects created through the API
data.createdAtstringISO 8601 creation date
data.labelstring | undefinedThe display name, when one was given. Absent when not set
data.groupIdstring | undefinedThe group the project was filed under. Absent when ungrouped
A bad `groupId` fails the whole request

The group is resolved before the project is created and before any credits are spent, so an unknown, malformed, someone else's or a full group returns 400 INVALID_PROJECT_GROUP and nothing is created. You are never left with a charged project that silently landed outside the folder you asked for.

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","label":"Acme storefront"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects
Success · 200 OK
json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "description": "A SaaS landing page",
    "plan": "api",
    "createdAt": "2026-03-11T10:30:00.000Z",
    "label": "Acme storefront"
  }
}
Error · 409
json
{
  "errors": {
    "errorCode": "PROJECT_ALREADY_EXISTS",
    "errorMessage": "A project with this name already exists"
  },
  "data": null
}

Error codes

CodeHTTPMeaning
MISSING_PROJECT_ID400projectId is required
INVALID_PROJECT_NAME400Invalid format. Use lowercase letters, numbers, and hyphens. Must start with a letter
INVALID_PROJECT_NAME_LENGTH400Project name must be between 4 and 35 characters
PROJECT_ALREADY_EXISTS409A project with this name already exists
INVALID_PROJECT_GROUP400The groupId does not exist, is not yours, or the group is full
INSUFFICIENT_CREDITS402Not enough credits for this operation
RATE_LIMIT_EXCEEDED429Maximum 10 project creations per 60 seconds

#List Projects

GET/api/v1/vcaas/projectsFree

Get your projects, most recently created first.

Returns at most 100 projects

A single call returns up to 100 projects. If your account has more, the response is only the first page — use skip to page through the rest, or narrow the result with search. Check the X-Has-More response header to know whether more pages exist.

Query parameters

FieldTypeRequiredDescription
limitnumberNoProjects per page, 1-100 (default: 100)
skipnumberNoProjects to skip, for paging (default: 0)
searchstringNoCase-insensitive match on project ID and description
sortDirectionstringNodesc (default) newest first, or asc for oldest first
groupIdstringNoReturn only the projects in one group. The literal value none returns only ungrouped projects
Filtering by plan is not available

This endpoint does not accept a plan filter. Each project's plan is still returned in data[].plan, but the list cannot be sliced by it — filter client-side if you need that.

An unknown `groupId` returns an empty list, not everything

A group id that is malformed, deleted, or belongs to another account is passed through and simply matches nothing. That is deliberate: the failure mode of a mistyped filter is "no projects", never "all of your projects".

Response headers

HeaderTypeDescription
X-Total-CountnumberTotal projects matching the filters, across all pages
X-LimitnumberPage size actually applied
X-SkipnumberOffset actually applied
X-Has-Morebooleantrue when more projects exist beyond this page

Response fields

FieldTypeDescription
dataarrayArray of project objects
data[].projectIdstringThe project ID
data[].descriptionstringProject description
data[].planstringAlways "api" for projects created through the API
data[].createdAtstringISO 8601 creation date
data[].labelstring | undefinedThe display name. Absent when none is set — fall back to projectId
data[].groupIdstring | undefinedThe group the project is filed under. Absent when ungrouped
data[].previewImageUrlstring | undefinedScreenshot of the project's home page, refreshed whenever a prompt finishes. Absent until the first prompt of a project completes
Thumbnails come with the list

previewImageUrl is returned on every list item on purpose, so a dashboard can draw its cards from this one call instead of a GET /projects/:projectId per tile.

Example request

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

Second page, 50 at a time:

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

Find a project by name instead of paging:

bash
curl -H "api-key: tlm_sk_your_key" \
  "https://api-accounts.totalum.app/api/v1/vcaas/projects?search=landing"

Only the projects in one group, or only the ungrouped ones:

bash
curl -H "api-key: tlm_sk_your_key" \
  "https://api-accounts.totalum.app/api/v1/vcaas/projects?groupId=65f1a2b3c4d5e6f7a8b9c0d1"

curl -H "api-key: tlm_sk_your_key" \
  "https://api-accounts.totalum.app/api/v1/vcaas/projects?groupId=none"
Success · 200 OK
json
{
  "errors": null,
  "data": [
    {
      "projectId": "my-app",
      "description": "A SaaS landing page",
      "plan": "api",
      "createdAt": "2026-03-11T10:30:00.000Z",
      "label": "Acme storefront",
      "groupId": "65f1a2b3c4d5e6f7a8b9c0d1",
      "previewImageUrl": "https://storage.totalum.app/previews/my-app.png"
    }
  ]
}
Paging through every project

Keep increasing skip by your limit while X-Has-More is true:

bash
skip=0
while :; do
  page=$(curl -s -D /tmp/h -H "api-key: tlm_sk_your_key" \
    "https://api-accounts.totalum.app/api/v1/vcaas/projects?limit=100&skip=$skip")
  echo "$page"
  grep -qi '^x-has-more: true' /tmp/h || break
  skip=$((skip + 100))
done
Error · 400
json
{
  "errors": {
    "errorCode": "LIST_PROJECTS_ERROR",
    "errorMessage": "Internal error listing projects"
  },
  "data": null
}

Error codes

CodeHTTPMeaning
LIST_PROJECTS_ERROR400Internal error listing projects

#Get Project Details

GET/api/v1/vcaas/projects/:projectIdFree

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

Path parameters

ParameterTypeDescription
:projectIdstringThe project ID

Response fields

FieldTypeDescription
data.projectIdstringThe project ID
data.labelstring | undefinedThe display name. Absent when none is set — fall back to projectId. Returned here as well as on the list so a single-project screen can show and edit the name without paging every project to find its own row
data.groupIdstring | undefinedThe group the project is filed under. Absent when ungrouped
data.previewImageUrlstring | undefinedScreenshot of the project's home page, refreshed whenever a prompt finishes. Absent until the first prompt completes
data.descriptionstringProject description
data.planstringAlways "api" for projects created through the API
data.agentProcessStatusstring | undefined"init" (running) | "done" (finished) | "idle" (not started)
data.agentServerStatusstring | undefined"Active" | "Creating" | "Starting" | "Archived" | "Unarchiving" | "Archiving"
data.createdAtstringISO 8601 creation date
data.deploymentobject | nullLatest deployment info, null if never deployed
data.deployment.statusstring"deploying" | "success" | "error"
data.deployment.createdAtstringISO 8601 deployment date
data.deployment.versionIdstring | undefinedVersion ID that was deployed
data.versionRecoveryobject | nullSet 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.statusstring"recovering" (in progress) | "error" (last recovery failed)
data.versionRecovery.versionIdstringThe version ID currently being / last attempted being recovered
data.versionRecovery.startedAtstringISO 8601 start time
data.versionRecovery.errorMessagestring | undefinedPresent when status="error" — surface this text to the user
data.secretsarrayList of secret names (values never returned)
data.secrets[]._idstringSecret ID (use for deletion)
data.secrets[].secretNamestringEnvironment variable name
data.secrets[].environmentstring"development" | "production" | "both"
data.customDomainobject | nullCustom domain info, null if none configured
data.customDomain.hostnamestringThe custom domain hostname
data.customDomain.statusstring"pending_validation" | "pending_deployment" | "active" | "blocked"
data.customDomain.sslStatusstringSSL certificate status
data.customDomain.dnsRecordsToAddarray | undefinedDNS records to configure: [{ type: "CNAME"|"TXT", name, value }]
data.temporalDevelopmentProjectUrlstring | null | undefinedLive development preview URL (from the running dev server). May be null/undefined if no server has started yet.
data.cachedDevelopmentUrlstring | null | undefinedCached development preview URL (static snapshot, available when server is not active). May be null/undefined if the project has never been archived.
data.developmentUrlFieldToUsestring | null | undefinedWhich field to use for the development preview right now: "temporalDevelopmentProjectUrl" or "cachedDevelopmentUrl". If this field is null or undefined, default to temporalDevelopmentProjectUrl.
data.productionProjectUrlstring | undefinedProduction URL — custom domain if connected, otherwise {projectId}.totalum-project.com
data.totalCreditsSpentnumberTotal credits spent on this project
data.creditLimitsobjectCurrently configured monthly credit limits for this project
data.creditLimits.maxDevelopmentCreditsPerMonthnumber | nullMax development credits/month (null = no limit)
data.creditLimits.maxInfrastructureCreditsPerMonthnumber | nullMax infrastructure credits/month (null = no limit)
data.multiPromptobject | nullPresent only when a multi-prompt batch was started via POST /agent/start with multiPrompt. Same shape as on GET /agent/status.
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.

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 · 200 OK
json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "label": "Acme storefront",
    "groupId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "previewImageUrl": "https://storage.totalum.app/previews/my-app.png",
    "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
  }
}
Error · 404
json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}

Error codes

CodeHTTPMeaning
MISSING_PROJECT_ID400projectId is required
PROJECT_NOT_FOUND404Project does not exist or you don't own it
End of Get Project DetailsNext endpointPATCHUpdate Project

#Update Project

PATCH/api/v1/vcaas/projects/:projectIdFree

Change a project's display label, its description, or the group it is filed under.

`projectId` cannot be changed

The project ID is the organization id and the production hostname, and there is no rename anywhere in the stack — which is exactly why label exists. Sending projectId in the body does nothing.

Path parameters

ParameterTypeDescription
projectIdstringThe project ID

Body parameters

FieldTypeRequiredDescription
labelstring | nullNoDisplay name, trimmed and capped at 80 characters. null or an empty string clears it, and clients fall back to projectId
descriptionstring | nullNoTrimmed and capped at 500 characters. null or an empty string clears it
groupIdstring | nullNoAn existing project group to file it under. null removes the project from its group
Absent keys are left alone; `null` clears

These are different requests and behave differently. {} is rejected as a no-op, { "label": "New name" } touches nothing but the label, and { "label": null } removes it. A PATCH is never a full replacement here — updating one field can't blank another by omission.

Response fields

FieldTypeDescription
data.projectIdstringUnchanged — the id is immutable
data.labelstring | undefinedThe label after the update. Absent when cleared or never set
data.descriptionstringThe description after the update. Empty string when none is set
data.groupIdstring | undefinedThe group after the update. Absent when the project is ungrouped

Example request

bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"label":"Acme storefront","groupId":"65f1a2b3c4d5e6f7a8b9c0d1"}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app

Remove the label and take the project out of its group:

bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"label":null,"groupId":null}' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app
Success · 200 OK
json
{
  "errors": null,
  "data": {
    "projectId": "my-app",
    "label": "Acme storefront",
    "description": "A SaaS landing page",
    "groupId": "65f1a2b3c4d5e6f7a8b9c0d1"
  }
}
Error · 400
json
{
  "errors": {
    "errorCode": "NOTHING_TO_UPDATE",
    "errorMessage": "Provide at least one of label, description or groupId"
  },
  "data": null
}
Reading back immediately is safe

GET /projects/:projectId serves a snapshot cached for a few seconds to absorb dashboard polling, and that snapshot carries label and groupId. A successful PATCH drops it, so an editing UI that saves a name and refetches straight away reads the new value, not the old one.

Error codes

CodeHTTPMeaning
MISSING_PROJECT_ID400projectId is required
PROJECT_NOT_FOUND404Project does not exist or you don't own it
NOTHING_TO_UPDATE400The body contained none of label, description or groupId
INVALID_PROJECT_GROUP400The group does not exist, is not yours, or already holds the maximum of 100,000 projects
UPDATE_PROJECT_ERROR400Internal error updating the project

#Delete Project

DELETE/api/v1/vcaas/projects/:projectIdFree

Permanently delete a project and all its data.

Path parameters

ParameterTypeDescription
:projectIdstringThe project ID to delete

Response fields

FieldTypeDescription
data.successbooleantrue 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 · 200 OK
json
{ "errors": null, "data": { "success": true } }
Error · 404
json
{
  "errors": {
    "errorCode": "PROJECT_NOT_FOUND",
    "errorMessage": "Project does not exist or you don't own it"
  },
  "data": null
}

Error codes

CodeHTTPMeaning
MISSING_PROJECT_ID400projectId is required
PROJECT_NOT_FOUND404Project does not exist or you don't own it
PLAN_NOT_API400Only API plan projects can be deleted from this API

#Update Credit Limits

PATCH/api/v1/vcaas/projects/:projectId/credit-limitsFree

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

ParameterTypeDescription
:projectIdstringThe project ID

Body parameters

FieldTypeRequiredDescription
maxDevelopmentCreditsPerMonthnumber or nullNoMax development credits per month (null to remove)
maxInfrastructureCreditsPerMonthnumber or nullNoMax infrastructure credits per month (null to remove)

Response fields

FieldTypeDescription
data.creditLimits.maxDevelopmentCreditsPerMonthnumber or nullCurrent development limit
data.creditLimits.maxInfrastructureCreditsPerMonthnumber or nullCurrent 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 · 200 OK
json
{
  "errors": null,
  "data": {
    "creditLimits": {
      "maxDevelopmentCreditsPerMonth": 500,
      "maxInfrastructureCreditsPerMonth": 100
    }
  }
}
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

CodeHTTPMeaning
MISSING_LIMIT_FIELDS400At least one of the two limit fields is required
INVALID_LIMIT400Amount must be a positive number
PROJECT_NOT_FOUND404Project doesn't exist or you don't own it
End of Update Credit Limits Back to top