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.

Start with Launch Project — it creates a project and starts building it in one call, and it is the default for anything new. Everything else on this page manages a project that already exists.

#Launch Project

POST/api/v1/vcaas/projects/launchUses credits

Create a project and start building it in one call.

Getting from POST /projects to "the agent is building my app" takes four to six more calls, in an order that is not obvious and that is wrong in three different ways if you guess it. This endpoint is that sequence, done in the order that works.

The only required fields are projectId and prompt. Everything else is optional — send nothing else and it behaves exactly like a create followed by Run AI Agent.

Just want an empty project, with no development started?

Use Create Project instead — but it is not recommended for anything you intend to build. It gives you a project and nothing else: no prompt, no build, nothing running, and four to six further calls (in a specific order) before the agent can start.

It is the right call in exactly three cases: you are about to import an existing project into it, you want to connect GitHub before any code exists, or you intend to write the files yourself. Otherwise, launch.

Why one call instead of five

The order is load-bearing, and each of these is a real failure people hit doing it by hand:

  • Secrets are written into the sandbox .env when the build starts. A secret created after agent/start is not in the file the first build reads — so your app reports the key as missing even though you sent it.
  • Figma must be connected before the run, or a design link in the prompt is read by an agent that cannot reach Figma.
  • Attachments uploaded after agent/start are not attached to anything — that run already has its file list.
  • Credit limits set afterwards do not cover the first run, which is the most expensive thing the project will ever do.

Body parameters

FieldTypeRequiredDescription
projectIdstringYesYour preferred ID. Same format rules as Create Project. If it is taken, it is not an error — see the callout below
promptstringYesWhat to build. Run as the project's first prompt, exactly as if sent to POST /agent/start
descriptionstringNoProject description, max 500 characters
labelstringNoHuman display name, max 80 characters
groupIdstringNoFile the project under an existing project group
filesarrayNoAttachments for the first prompt. Max 10. Each entry needs exactly one of url or content
files[].namestringYesFile name, e.g. mockup.png
files[].descriptionstringNoWhat the file shows — the agent reads this. imageDescription is accepted as an alias
files[].urlstringNoPublic http(s) URL of an already-hosted file. Passed to the agent untouched; nothing is downloaded by Totalum
files[].contentstringNoBase64 file contents. A data:image/png;base64,… prefix is accepted and stripped. Uploaded for you, and costs UPLOAD_FILE per file
creditLimitsobjectNoMonthly per-project caps, applied at creation so they cover the first run
creditLimits.maxDevelopmentCreditsPerMonthnumber | nullNoPositive number, or null for no cap. Default: no cap
creditLimits.maxInfrastructureCreditsPerMonthnumber | nullNoPositive number, or null for no cap. Default: 250
secretsarrayNoProject secrets (.env values), stored before the agent starts. Max 50, names must be unique
secrets[].secretNamestringYesEnvironment variable name
secrets[].secretValuestringYesThe value. Never returned by any endpoint afterwards
secrets[].environmentstringNo"development" | "production" | "both" (default "both")
figmaobjectNoConnect a Figma account so the agent can read designs linked in the prompt
figma.tokenstringYesA Figma personal access token. Validated before anything is created, so a bad token costs you nothing
A taken name is not an error here

Unlike Create Project, this endpoint never answers PROJECT_ALREADY_EXISTS. Project ids are globally unique, so the short obvious names a prompt produces collide often — and a 409 on a call that also starts an agent run costs you the whole round trip and forces you to reimplement the retry.

Instead it tries the name you asked for, then adds 2 random characters, then 3, 4, 5, 6, and creates the first free one. When that happens the response carries requestedProjectId with the name you sent.

Always use the returned projectId for every later call. That is already true in every environment — outside production the API appends its own suffix — but here it can differ for this reason too.

Response fields

FieldTypeDescription
data.projectIdstringThe project ID that was actually created. Use this for every subsequent call
data.requestedProjectIdstring | undefinedThe projectId you asked for. Present only when it was taken and a suffixed name was created instead
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
data.groupIdstring | undefinedThe group the project was filed under, when any
data.creditLimitsobjectThe limits actually stored on the project, defaults included
data.creditLimits.maxDevelopmentCreditsPerMonthnumber | nullnull = no cap
data.creditLimits.maxInfrastructureCreditsPerMonthnumber | nullnull = no cap
data.filesarrayThe attachments as the agent received them. Empty when none were sent
data.files[].namestringFile name
data.files[].imageDescriptionstringThe description you gave, or ""
data.files[].urlstringThe URL the agent was given — yours for a url entry, a signed Totalum URL for an uploaded one
data.secretsarrayThe secrets that were stored. Values are never echoed
data.secrets[]._idstringSecret ID (use for deletion)
data.secrets[].secretNamestringEnvironment variable name
data.secrets[].environmentstring"development" | "production" | "both"
data.figmaobject | undefinedPresent only when the request carried figma and the connection succeeded
data.figma.connectedbooleantrue
data.figma.accountobjectThe linked Figma account: { id, handle, email, imgUrl }. The token is never returned
data.agentobjectWhether the first prompt actually started
data.agent.startedbooleanfalse means the project exists but the run did not start — see warnings
data.agent.statusstring | undefined"init" when the run started; absent otherwise
data.agent.messagestringWhat to do next, in words
data.warningsarrayEmpty on the happy path. Each entry is a step that did not happen
data.warnings[].stepstring"creditLimits" | "secrets" | "figma" | "files" | "agent"
data.warnings[].errorCodestringWhy it failed
data.warnings[].errorMessagestringThe reason, plus the endpoint that retries just that step
Always read `agent.started` and `warnings`

A 200 means the project exists — not that everything you asked for happened.

Everything judged before the project is created (the body, your balance, the Figma token, your plan's project limit, the name) returns a 4xx and leaves nothing behind. But from the moment the project exists you have been charged and have taken a name in a global namespace, so a later failure cannot answer 4xx: that would tell you nothing happened, and your retry would collide with the project this very call just created.

So a secret that would not store, a Figma connect that failed, an upload that broke — or the prompt itself not starting — are reported in warnings, and each message names the endpoint that retries only that step. If agent.started is false, call POST /projects/{projectId}/agent/start with the same prompt.

What it costs

1 credit for the project (CREATE_PROJECT) + 0.5 per inline attachment (UPLOAD_FILE; a url attachment costs nothing), then the agent run itself — typically 10 to 40 credits over 10 to 30 minutes.

The whole up-front bill is checked before anything is created, so you are never left with a project whose attachments were dropped for want of credits.

Example request

bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "barber-bookings",
    "prompt": "Build a booking system for a barbershop, with Stripe payments and email confirmations",
    "label": "Barber bookings",
    "files": [
      { "name": "brand.png", "description": "our brand colours", "url": "https://cdn.example.com/brand.png" }
    ],
    "creditLimits": { "maxDevelopmentCreditsPerMonth": 500 },
    "secrets": [
      { "secretName": "STRIPE_SECRET_KEY", "secretValue": "sk_live_xxx", "environment": "production" }
    ]
  }' \
  https://api-accounts.totalum.app/api/v1/vcaas/projects/launch
Success · 200 OK
json
{
  "errors": null,
  "data": {
    "projectId": "barber-bookings",
    "description": "",
    "plan": "api",
    "createdAt": "2026-08-24T10:30:00.000Z",
    "label": "Barber bookings",
    "creditLimits": {
      "maxDevelopmentCreditsPerMonth": 500,
      "maxInfrastructureCreditsPerMonth": 250
    },
    "files": [
      {
        "name": "brand.png",
        "imageDescription": "our brand colours",
        "url": "https://cdn.example.com/brand.png"
      }
    ],
    "secrets": [
      {
        "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
        "secretName": "STRIPE_SECRET_KEY",
        "environment": "production"
      }
    ],
    "agent": {
      "started": true,
      "status": "init",
      "message": "Process started, can take from 4 to 40 minutes."
    },
    "warnings": []
  }
}
Partial · 200 OK — the project exists, one step did not happen
json
{
  "errors": null,
  "data": {
    "projectId": "barber-bookings-k7q",
    "requestedProjectId": "barber-bookings",
    "description": "",
    "plan": "api",
    "createdAt": "2026-08-24T10:30:00.000Z",
    "creditLimits": {
      "maxDevelopmentCreditsPerMonth": null,
      "maxInfrastructureCreditsPerMonth": 250
    },
    "files": [],
    "secrets": [],
    "agent": {
      "started": false,
      "message": "The project was created but the first prompt did not start. Retry with POST /api/v1/vcaas/projects/barber-bookings-k7q/agent/start."
    },
    "warnings": [
      {
        "step": "agent",
        "errorCode": "AGENT_START_FAILED",
        "errorMessage": "An agent process is already running."
      }
    ]
  }
}

Error codes

Nothing is created and nothing is charged for any of these.

CodeHTTPMeaning
MISSING_PROMPT400prompt is required and must be a non-empty string
INVALID_FILES400files is not an array, or an entry is not an object
TOO_MANY_FILES400More than 10 attachments
INVALID_FILE_NAME400An attachment has no name
INVALID_FILE_SOURCE400An attachment has neither url nor content, or has both
INVALID_FILE_URL400files[].url is not a public http(s) URL
INVALID_FILE_CONTENT400files[].content is not valid base64
INVALID_SECRETS400secrets is not an array, or an entry is not an object
TOO_MANY_SECRETS400More than 50 secrets
MISSING_SECRET_FIELDS400A secret is missing secretName or secretValue
DUPLICATE_SECRET_NAME400The same secretName appears twice
INVALID_SECRET_ENVIRONMENT400Not one of development, production, both
INVALID_CREDIT_LIMITS400creditLimits is not an object
INVALID_LIMIT400A credit limit is not a positive number (or null)
INVALID_FIGMA400figma is not an object
MISSING_FIGMA_TOKEN400figma.token is missing or empty
FIGMA_TOKEN_MALFORMED400That does not look like a Figma token — see Check a Figma Token
FIGMA_TOKEN_INVALID400Figma rejected the token
FIGMA_TOKEN_FORBIDDEN400Valid token, missing the scopes Totalum needs
FIGMA_RATE_LIMITED400Figma is rate-limiting the check — retry shortly
FIGMA_UNREACHABLE400Figma could not be reached to check the token
RATE_LIMITED429Too many Figma token checks (10/minute per account)
MISSING_PROJECT_ID400projectId is required
INVALID_PROJECT_NAME400Invalid format. Lowercase letters, numbers and hyphens; must start with a letter
INVALID_PROJECT_NAME_LENGTH400Project name must be between 4 and 35 characters
INVALID_PROJECT_GROUP400The groupId does not exist, is not yours, or the group is full
INSUFFICIENT_CREDITS402Not enough credits for the project, its attachments, or to start an agent run
MAX_PROJECTS_REACHED403Your plan's project limit is already in use — see Create Project
RATE_LIMIT_EXCEEDED429You are creating projects faster than your plan allows
PROJECT_ALREADY_EXISTS409Only when the name and six suffixed candidates were all taken
LAUNCH_PROJECT_ERROR400The project could not be created — the message carries the reason

#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
sortFieldstringNodate (default) orders by creation date; lastModified orders by when the project last changed. Any other value falls back to date
sortDirectionstringNodesc (default) newest first, or asc for oldest first
groupIdstringNoReturn only the projects in one group. The literal value none returns only ungrouped projects
createdFromstringNoOnly projects created at or after this date. Inclusive. A full instant (2026-03-01T00:00:00.000Z) is used verbatim; a bare day (2026-03-01) is read as UTC
createdTostringNoOnly projects created at or before this date. Inclusive — a bare day is widened to that day's last millisecond, so createdFrom=X&createdTo=X returns that whole day
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
data[].lastModifiedAtstringISO 8601 date of the last change to the project — a prompt, a deploy, a rename, a file edit. Never null on any project, however old: it falls back through last finished run → screenshot time → updatedAtcreatedAt, so you can render and compare it without a null branch. "Same as createdAt" is the truthful answer for a project nobody has touched since making it
data[].productionProjectUrlstringThe project's production address, {projectId}.totalum-project.com
`productionProjectUrl` on the list does not reflect a custom domain

On the list it is always the canonical {projectId}.totalum-project.com host. Get Project Details is the one that prefers an active custom domain — resolving that costs two extra calls per project, which on a page of 100 would turn the cheapest endpoint on this API into its most expensive.

That is safe to link to: a custom hostname is additive, so the canonical host keeps serving alongside it. But if you are showing the owner their own domain, read it from the detail endpoint for the one project you are displaying.

It is also not proof of a deployment. Nothing records whether a project has ever been published, so this is the address the project would serve from, and it 404s until the first successful deploy — exactly as the same field behaves on the detail endpoint. Use Get Deployment Status for the real answer.

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.importInProgressobject | nullSet while an importProject call is restoring and rebuilding, otherwise null. This is the canonical signal for "is a project import in progress" — poll this until it is null rather than guessing from agentServerStatus, which looks identical on a cold sandbox. Reported as null once the import ends (success or failure) and also once the lock is older than 30 minutes, so it can never stay set for a job that died.
data.importInProgress.startedAtstringISO 8601 start time, from the server that started the import — not the caller's clock
data.importInProgress.errorMessagestring | undefinedPresent when the import ended in failure
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.customDomain._idstringThe custom domain record ID
data.customDomain.projectIdstringThe project the domain belongs to
data.customDomain.deploymentIdstringThe deployment the domain is attached to
data.customDomain.createdAtstringISO 8601 date the domain was added
data.customDomain.updatedAtstringISO 8601 date the domain record last changed
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,
    "importInProgress": 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. Set a field to null to remove that limit.

Defaults, on every project created through this API — whether by Launch Project or Create Project, they are the same:

CategoryDefaultMeaning
maxDevelopmentCreditsPerMonthnullNo cap. Agent runs, deploys and file writes are limited only by your account balance
maxInfrastructureCreditsPerMonth250Capped. The app's own usage — emails, PDFs, AI calls, image generation — stops at 250 credits a month until you raise or remove it

launch lets you override both in the same call that creates the project, so the caps cover the first run. This endpoint changes them afterwards.

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

Development spending is uncapped by default and limited only by your account balance; infrastructure is capped at 250 credits a month on every project created through this API (see the defaults table above). When a project reaches a 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

#Create Project (without building)

POST/api/v1/vcaas/projectsUses credits

Create an empty project: no prompt, no build, nothing running.

Prefer [Launch Project](#launch-project)

This endpoint is not deprecated and is fully supported, but it is not the one to reach for. An empty project does nothing until you start a build, and getting from here to a running app takes four to six more calls in an order that is wrong three different ways when assembled by hand — Launch Project is that sequence in one call, and it is the default.

Use this one only when you genuinely want a project with no development started: to import an existing project into it, to connect GitHub before any code exists, or to write the files yourself.

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
MAX_PROJECTS_REACHED403Your plan's project limit is already in use — see below
RATE_LIMIT_EXCEEDED429You are creating projects faster than your plan allows
A project limit and a creation rate, both set by your plan

Your account may hold a maximum number of projects at once, and may create them at a maximum rate. Both come from the plan on the account:

PlanProjectsNew projects
Free21 every 5 min · 12/hour
Starter105/min · 60/hour
Basic5010/min · 120/hour
Professional30030/min · 360/hour
Enterpriseunlimited100/min · 1 200/hour

The two refusals need different handling, which is why they are different codes:

  • 403 MAX_PROJECTS_REACHED — retrying never succeeds. Delete a project (the slot frees immediately) or move up a plan. The response carries errorDetails with maxProjects, projectsUsed, plan and upgradePlan so a client can act without parsing the message.
  • 429 RATE_LIMIT_EXCEEDED — clears on its own. Back off and retry.

A create that fails validation or hits a name collision does not consume your rate allowance.

End of Create Project (without building) Back to top