Totalum API for AI Agents
Everything an AI agent needs to build and ship apps with Totalum, in one file.
#What Totalum is
Totalum turns a natural-language prompt into a working, hosted, full-stack web app, and keeps changing it with more prompts. You call the API; Totalum's own AI coding agent writes the app; Totalum builds it, gives it a preview, publishes it, and runs it. There is no infrastructure to set up and no key to configure for the built-in features.
- Base URL:
https://builder-api.totalum.app/api/v1/vcaas(https://api-accounts.totalum.appanswers the same paths). - Auth: header
api-key: tlm_sk_…on every request. Call the API from a server only; never put the key in browser or mobile code. - Envelope:
{ "errors": null, "data": … }, or{ "errors": { "errorCode", "errorMessage", "errorDetails?" }, "data": null }. Branch onerrorCode. - Async: a prompt and a publish answer at once; poll their status every 10–15 seconds.
- Ids: always use the
projectIdthe API returns (a taken id gets a random suffix). - Legacy projects: projects created before 8 October 2026, or without the v2 engine, are legacy projects and do not have every endpoint below — see https://www.totalum.app/docs/api/migration-v2.
#What a generated app is
A standard Next.js app (App Router, React, TypeScript, Tailwind CSS) running on Cloudflare Workers, with:
- Its own SQL database (SQLite on Cloudflare D1, used through Drizzle ORM), in two environments: dev (agent + preview) and live (published site). Schema changes are migrations; a publish applies them to live. A database can be moved to Turso past 10 GB. Nightly backups on paid plans, 30-day point-in-time restore.
- Sign-in (e-mail + password, password reset, Google), with the owner as admin.
- Built-in features through
totalum-sdk— no keys: e-mail, AI (chat, structured output, vision, images, video, speech, embeddings), file storage, document scanning (OCR), web scraping and search, a cloud browser, PDFs, payments (the owner's Stripe account), real-time channels, scheduled jobs, vector search, logs, site analytics, SEO. - Mobile apps (Android and iOS, paid plans) from the same web app: a shell that opens the published site, built by Totalum and sent to the stores with the owner's own Google Play and Apple developer accounts.
- The code is the owner's: downloadable, syncable to GitHub, runnable locally.
To use a feature, say it in the prompt ("send me an e-mail on every booking", "take card payments", "let users upload photos"). For another service, add its key as a secret and name it in the prompt.
#The development cycle
launch ─▶ status "done" ─▶ preview ─▶ prompt ─▶ … ─▶ publish ─▶ live sitePOST /projects/launch— create the project and run the first prompt.GET /projects/:projectId/agent/statusevery 10–15 s untilstatusis"done"(showGET …/agent/progressmeanwhile).GET /projects/:projectId→temporalDevelopmentProjectUrlis the preview. Read it again after every prompt.- More prompts:
POST /projects/:projectId/agent/start, then poll again. POST /projects/:projectId/deployments/deploy→ pollGET …/deployments/statusuntil"success"→ the live site isproductionProjectUrl(<projectId>.totalum-project.com,<projectId>.webapp-project.comwhen chosen as the main address withPATCH /projects/:projectId {"liveHostSuffix":"webapp-project.com"}on Professional/Enterprise, where both addresses serve the site, or the custom domain).- No "totalum" in the address: on Professional/Enterprise set
PATCH /projects/:projectId {"liveHostSuffix":"webapp-project.com"}, or attach a custom domain (PUT /projects/:projectId/domain) or buy one (Custom Domains). Domains are per project; an account-wide domain for all projects is not supported.
- No "totalum" in the address: on Professional/Enterprise set
Prompts, file edits and restores change dev only (each code change is a version); only a publish changes live. One prompt, publish or restore runs at a time per project (409 AGENT_RUNNING, DEPLOYMENT_RUNNING, OPERATION_IN_PROGRESS).
#Limits and credits
- Every plan can use the API. Everything is paid in credits; spending actions need a balance of at least 1 credit (
403 VCAAS_INSUFFICIENT_CREDITS,402 INSUFFICIENT_CREDITS). - A prompt costs at least 3 credits, usually 3–50, by the work done; a failed run that built nothing is refunded. Fixed prices (create 2, publish 0.5, …) and monthly fees:
GET /credit-costs. - A prompt's agent works up to about 40 minutes, then wraps up and saves; most prompts take a few minutes. Prompt ≤ 40,000 characters, ≤ 10 attachments.
- Projects per plan: Free 2, Starter 10, Builder 50, Professional 300, Enterprise unlimited (
403 MAX_PROJECTS_REACHED); creation rate per plan (429 RATE_LIMIT_EXCEEDED). - JSON body ≤ 1 MB; upload ≤ 20 MB. Project caps:
PATCH /projects/:projectId/credit-limits(defaults for API projects: development 5,000 and infrastructure 300 credits a month; setcreditLimitsat create). - The running app: 100 built-in calls a second per project, 20 paid AI/web calls a minute per visitor.
Full list: https://www.totalum.app/docs/api/errors
#Essential endpoints
#Launch Project
/api/v1/vcaas/projects/launchapi-key2 credits + the first promptCreate a project and start building it with its first prompt, in one call. Secrets, attachments, credit limits and a Figma token sent here are in place before the first build starts, which is the order the build needs.
Every project has two monthly caps: development (what the AI agent and project actions spend) and infrastructure (what the running app spends on hosting, database, AI, e-mail…). Projects created through the API start at 5,000 and 300 credits a month. Set your own in creditLimits here or in Create Project, and change them any time with Update Credit Limits.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Your preferred id: 4–35 characters, lowercase letters, digits and single hyphens, starting with a letter, never containing -dev-. It becomes the address <projectId>.totalum-project.com. If it is taken, a short random suffix is added and requestedProjectId is returned |
prompt | string | Yes | What to build (the first prompt, up to 40,000 characters) |
tier | string | No | "auto" (default), "cheap", "normal" or "expensive" — see agent tiers. Also accepted as model or modelTier |
engineVersion | string | No | "v2" to require the current engine. A request that cannot get it is refused (ENGINE_V2_DISABLED, ENGINE_REQUEST_NOT_ELIGIBLE) and nothing is created. New accounts create every project on the current engine, with or without it ("legacy" is refused: ENGINE_LEGACY_UNAVAILABLE); on an older account a create without it can be a legacy project |
label | string | No | Display name, up to 80 characters. Can be changed later; the id cannot |
description | string | No | Up to 500 characters |
groupId | string | No | An existing project group |
dataResidency | string | No | "global" or "eu" (data kept in the EU only). Default: your account's setting (Set Data Residency), else "global". Fixed for the project's life |
audienceCountry | string | No | Two-letter country code of the app's users ("US"); the database is placed nearest them. Default: your account's country. Never makes a project EU-only |
files | object[] | No | Up to 10 attachments for the first prompt. Each has exactly one of url or content |
files[].name | string | Yes | File name, e.g. logo.png |
files[].description | string | No | What the file is for ("use as the logo"). imageDescription is accepted too |
files[].url | string | No | Public http(s) URL of a hosted file |
files[].content | string | No | The file as base64 (a data:…;base64, prefix is accepted). Uploaded for you, free |
secrets | object[] | No | Up to 50 environment variables, stored before the build starts |
secrets[].secretName | string | Yes | Uppercase letters, digits and _, starting with a letter (STRIPE_KEY) |
secrets[].secretValue | string | Yes | 1–10,000 characters. Never returned again |
secrets[].environment | string | No | "development", "production" or "both" (default) |
creditLimits | object | No | Monthly caps applied from the first prompt. Set them to control your costs — see Spending & Rate Limits |
creditLimits.maxDevelopmentCreditsPerMonth | number | null | No | Cap on what the AI agent and project actions spend (prompts, builds, publishing). Default 5000 for projects created through the API; null = no cap |
creditLimits.maxInfrastructureCreditsPerMonth | number | null | No | Cap on what the running app spends (hosting, database, AI, e-mail…). Default 300 for projects created through the API; null = no cap |
figma | object | No | Connect Figma so the agent can read designs linked in the prompt |
figma.token | string | Yes | A Figma personal access token, checked before anything is created |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The id that was created. Use this one from now on |
data.requestedProjectId | string | undefined | The id you asked for, only when the created one differs |
data.description | string | Project description |
data.plan | string | Always "api" |
data.createdAt | string | ISO 8601 creation time |
data.label | string | undefined | The label, when one was given |
data.groupId | string | undefined | The group, when one was given |
data.dataResidency | string | "global" or "eu" |
data.engineVersion | string | "v2" |
data.engineState | string | undefined | Present only when the project was still being set up when the call answered ("provisioning"); the build starts by itself |
data.creditLimits | object | The caps stored on the project, defaults included |
data.files | object[] | The attachments the agent received: { name, imageDescription, url } |
data.secrets | object[] | The secrets stored: { _id, secretName, environment } (never the value) |
data.figma | object | undefined | { connected: true, account } when Figma was connected |
data.agent.started | boolean | false = the project exists but the first prompt did not start (see warnings) |
data.agent.status | string | undefined | "init" when the run started |
data.agent.runId | string | undefined | The run's id |
data.agent.message | string | What to do next |
data.agent.expectedMinutes | number | undefined | Estimated minutes for the first prompt — an estimate, never a deadline |
data.agent.expectedFinishAt | string | undefined | Start time + expectedMinutes (ISO 8601) |
data.agent.warnings | string[] | undefined | Run options that were ignored |
data.warnings | object[] | Steps that did not happen after the project was created: { step, errorCode, errorMessage }; step is "secrets", "figma", "files" or "agent", and the message names the endpoint that retries it |
Everything checked before creation (body, balance, Figma token, project limit, name) answers 4xx and creates nothing. After creation a failed step is a warning, never an error: if agent.started is false, call Run AI Agent with the same prompt.
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"projectId":"barber-bookings","prompt":"Build a booking site for a barbershop with online payments","label":"Barber bookings","creditLimits":{"maxDevelopmentCreditsPerMonth":500}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/launchconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/launch", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"projectId": "barber-bookings",
"prompt": "Build a booking site for a barbershop with online payments",
"label": "Barber bookings",
"creditLimits": {
"maxDevelopmentCreditsPerMonth": 500
}
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"description": "",
"plan": "api",
"createdAt": "2026-10-08T10:30:00.000Z",
"label": "Barber bookings",
"dataResidency": "global",
"creditLimits": { "maxDevelopmentCreditsPerMonth": 500, "maxInfrastructureCreditsPerMonth": 300 },
"files": [],
"secrets": [],
"agent": {
"started": true,
"status": "init",
"runId": "run_01k6x8m2c9",
"message": "Process started. Estimated time: about 9 minutes. This is only an approximation…",
"expectedMinutes": 9,
"expectedFinishAt": "2026-10-08T10:39:00.000Z"
},
"warnings": [],
"engineVersion": "v2"
}
}Error codes
Nothing is created or charged for any of these.
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROMPT | 400 | prompt is missing or empty |
MISSING_PROJECT_ID | 400 | projectId is missing |
INVALID_PROJECT_NAME | 400 | Not lowercase letters, digits and single hyphens starting with a letter |
INVALID_PROJECT_NAME_LENGTH | 400 | Not 4–35 characters |
RESERVED_PROJECT_NAME | 400 | The id contains -dev- |
INVALID_FILES / TOO_MANY_FILES | 400 | files is not an array of objects, or has more than 10 items |
INVALID_FILE_NAME / INVALID_FILE_SOURCE / INVALID_FILE_URL / INVALID_FILE_CONTENT | 400 | An attachment has no name, has both or neither of url/content, a non-http(s) URL, or invalid base64 |
INVALID_SECRETS / TOO_MANY_SECRETS / MISSING_SECRET_FIELDS / DUPLICATE_SECRET_NAME / INVALID_SECRET_ENVIRONMENT | 400 | A malformed secrets list |
INVALID_CREDIT_LIMITS / INVALID_LIMIT | 400 | creditLimits is not an object, or a cap is not a positive number or null |
INVALID_FIGMA / MISSING_FIGMA_TOKEN / FIGMA_TOKEN_MALFORMED / FIGMA_TOKEN_INVALID / FIGMA_TOKEN_FORBIDDEN | 400 | The Figma token is missing or was rejected |
INVALID_PROJECT_GROUP | 400 | The group does not exist, is not yours, or is full |
INVALID_DATA_RESIDENCY | 400 | dataResidency is not "global" or "eu" |
INVALID_AUDIENCE_COUNTRY | 400 | audienceCountry is not a two-letter code |
INVALID_ENGINE_VERSION | 400 | engineVersion has an unknown value |
INSUFFICIENT_CREDITS | 402 | Your balance cannot pay the project (or is below 1 credit, needed to start the agent) |
MAX_PROJECTS_REACHED | 403 | Your plan's project limit is in use. errorDetails: maxProjects, projectsUsed, plan, upgradePlan |
ENGINE_REQUEST_NOT_ELIGIBLE | 403 | engineVersion: "v2" was asked but this account cannot create projects on it yet |
VCAAS_INSUFFICIENT_CREDITS | 403 | Your balance is below 1 credit |
PROJECT_ALREADY_EXISTS | 409 | The name and every suffixed candidate were taken |
ENGINE_V2_DISABLED | 409 | New projects cannot be created on the current engine right now |
ENGINE_LEGACY_UNAVAILABLE | 409 | engineVersion: "legacy", or a legacy (.zip) export code, on a new account: it creates only on the current engine |
RATE_LIMIT_EXCEEDED | 429 | You are creating projects faster than your plan allows (new projects and prompts share one per-minute allowance, see Limits) — retry after Retry-After seconds |
RATE_LIMITED | 429 | Your account has the most prompts running at once its plan allows, or too many Figma token checks (10 a minute) — retry after Retry-After seconds |
LAUNCH_PROJECT_ERROR | 503 | A temporary failure; nothing was created. Retry after Retry-After seconds |
#Run AI Agent
/api/v1/vcaas/projects/:projectId/agent/startapi-keyMetered, at least 3 creditsSend one prompt to the agent (or a multi-prompt batch). One run at a time per project. Once the running prompt's change is in the preview and the run is only finishing, the next prompt is accepted and waits: it starts as soon as that run ends. One prompt can wait; Stop Agent cancels the waiting one first.
A prompt is priced by the work the agent does: at least 3 credits, usually 3 to 50. A question that changes nothing is answered in seconds for 3 credits and creates no version. A run that fails without producing anything is refunded. The agent works up to about 40 minutes on one prompt, then wraps up and saves what it built; most prompts take a few minutes.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes¹ | The instruction, up to 40,000 characters |
tier | string | No | "auto" (default), "cheap", "normal" or "expensive" — see Agent tiers. model and modelTier are accepted as aliases |
inputFiles | object[] | No | Up to 10 files for this prompt. Say in the prompt what each one is for ("use this as the logo") |
inputFiles[].name | string | Yes | File name |
inputFiles[].imageDescription | string | No | What the file shows |
inputFiles[].url | string | Yes | Public URL, or the url returned by Upload File |
multiPrompt | object | No | Run a batch of prompts one after another, unattended. Omit for normal use |
multiPrompt.prompts | string[] | No² | 1–50 prompts, run in order |
multiPrompt.letTotalumDecide | boolean | No² | true = Totalum plans the prompts from prompt (the goal) |
¹ Not needed when multiPrompt.prompts is given.
² Exactly one of prompts or letTotalumDecide: true.
fastMode and effort are accepted for compatibility and ignored (an ignored option is listed in data.warnings; the run still starts).
#Agent tiers
| Tier | Use it for |
|---|---|
"auto" | The default. Totalum picks the tier for each prompt from the prompt, the recent conversation and the project: cheap for small edits and questions, normal for builds and features, expensive only when a fix keeps failing or you ask for the best |
"cheap" | The lowest price for real work: landing pages, redesigns, small apps, edits |
"normal" | Full apps and features, with more thinking |
"expensive" | The most capable agent, for complex apps and hard bugs. Uses the most credits |
With "auto", the run's final message carries autoMode: true and modelUsed: the tier that ran ("cheap", "normal" or "expensive"), charged as that tier.
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.status | string | Always "init" |
data.message | string | How to follow the run |
data.expectedMinutes | number | undefined | Estimated minutes — an estimate, never a deadline |
data.expectedFinishAt | string | undefined | Start + expectedMinutes (ISO 8601) |
data.warnings | string[] | undefined | Options that were ignored |
data.runId | string | undefined | The run's id (single prompt) |
data.multiPrompt.totalPrompts | number | undefined | Batches only: number of prompts (absent while Totalum plans them) |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Add a contact form that emails me every message"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/startconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/start", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"prompt": "Add a contact form that emails me every message"
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"status": "init",
"message": "Process started. Estimated time: about 6 minutes. This is only an approximation… Fetch GET /api/v1/vcaas/projects/barber-bookings/agent/status every 10-15 seconds to track progress; the status there is the only reliable signal that the run has finished.",
"expectedMinutes": 6,
"expectedFinishAt": "2026-10-08T11:16:00.000Z",
"runId": "run_01k6xc4n7d"
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROMPT | 400 | prompt is missing |
INVALID_MULTI_PROMPT | 400 | multiPrompt needs exactly one of prompts or letTotalumDecide: true |
TOO_MANY_PROMPTS | 400 | More than 50 prompts |
INVALID_PROMPT_ITEM | 400 | A prompt in prompts is empty |
VALIDATION_ERROR | 400 | The prompt is over 40,000 characters, or a field has the wrong type |
INSUFFICIENT_CREDITS | 402 | Your balance is below 1 credit |
PROJECT_CREDIT_LIMIT_REACHED | 403 | The project's monthly development cap is reached — raise it with Update Credit Limits |
AGENT_RUNNING | 409 | A prompt is already running and its change is not in the preview yet, or a prompt is already waiting (a batch answers 400 AGENT_ALREADY_RUNNING) |
PROJECT_PAUSED_BY_USER | 409 | The project is paused — resume it first |
OPERATION_IN_PROGRESS | 409 | A restore, import or other long operation holds the project |
PROJECT_NOT_FOUND | 404 | No such project in your account |
RATE_LIMITED | 429 | Your account sent more prompts this minute than its plan allows, or has the most prompts running at once (all projects together, see Limits) — retry after Retry-After seconds |
If your balance reaches 0 during a run, the run stops: what it built is saved as a version, it ends with INSUFFICIENT_CREDITS, and you are charged only what it used. A run that crosses the project's development cap stops the same way with PROJECT_CREDIT_LIMIT_REACHED.
#Get Agent Status
/api/v1/vcaas/projects/:projectId/agent/statusapi-keyFreeWhether a prompt is running, and the messages of the current (or last) run. Poll every 10–15 seconds. When status becomes "done", read the preview URL again from Get Project.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.status | string | "init" (running), "done" (the last run finished) or "idle" (never prompted) |
data.startedAt | string | null | Start of the current or last run |
data.runId | string | undefined | The current or last run |
data.realtimeConversation | object[] | The run's messages, oldest first (the user's prompt is in Get Full Conversation) |
data.realtimeConversation[].author | string | "user" or "agent" |
data.realtimeConversation[].message | string | Text |
data.realtimeConversation[].messageType | string | "regular", "starting", "building" (a status note), "finished" (the run's summary), "error", "limit-reached" or "individual" (a one-off notice) |
data.realtimeConversation[].createdAt | string | ISO 8601 |
data.realtimeConversation[].versionId | string | undefined | The version this message created |
data.realtimeConversation[].secretKeysNeeded | object | undefined | Secrets the app needs: { NAME: { isProvided, description } }. Add them with Create Secret |
data.realtimeConversation[].files | object[] | undefined | Files attached to the message |
data.realtimeConversation[].autoMode | boolean | undefined | true on the final message of an auto run |
data.realtimeConversation[].modelUsed | string | undefined | The tier an auto run used |
data.creditsSpent | number | undefined | What the last run cost, once "done" |
data.expectedMinutes | number | null | Estimate of the running prompt |
data.expectedFinishAt | string | null | Start + estimate |
data.multiPrompt | object | null | The batch, when one exists: { status, totalPrompts, currentPromptIndex, prompts[{ order, prompt, status }], startedAt, updatedAt }. status: "executing", "paused", "done", "cancelled" |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/statusconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/status", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"status": "done",
"startedAt": "2026-10-08T11:10:00.000Z",
"realtimeConversation": [
{
"author": "agent",
"message": "Added a contact form on /contact. Every message is saved and e-mailed to you. I sent a test message: saved, 200.",
"messageType": "finished",
"versionId": "ver_01k6xd2m8q",
"createdAt": "2026-10-08T11:15:40.000Z",
"autoMode": true,
"modelUsed": "normal"
}
],
"creditsSpent": 7.4,
"expectedMinutes": null,
"expectedFinishAt": null,
"multiPrompt": null,
"runId": "run_01k6xc4n7d"
}
}For a few seconds after a run ends, the preview may answer 503 with the header x-totalum-preview: updating while the new version goes live. Retry after the retry-after header; the preview never serves the previous version in that window.
#Get Agent Progress
/api/v1/vcaas/projects/:projectId/agent/progressapi-keyFreeA step-by-step view of the current run (or the last one): its stage, the files it changed, the builds and checks it ran. Use it to show a live "building" timeline. data is null before the first run.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.runId | string | The run |
data.state | string | "queued", "provisioning", "running", "finishing", "done", "failed", "cancelled" or "incomplete" |
data.stage | string | Where the run is: "queued", "sandbox", "starting", "thinking", "working", "build", "deploy", "fixing", "wrapping-up", "entry-page", "done", "failed", "cancelled", "incomplete" |
data.startedAt | string | Start time |
data.finishedAt | string | undefined | End time |
data.turns | number | Agent turns so far |
data.steps | object[] | Up to the 40 latest steps: { at, kind, file?, task?, ok?, added?, removed? }. kind: read, search, create, edit, delete, command, web, browser, integration, helper, plan, waiting; task (commands): build, test, install, database, check, save, other |
data.totalSteps | number | All steps of the run |
data.files | object[] | Files changed: { path, change: "create" | "edit" | "delete", added, removed } |
data.counts | object | { reads, searches, edits, commands, failedCommands } |
data.build | object | undefined | { ok } of the last build |
data.tests | object | undefined | { runs, failed, failedFlows? }: the checks the agent ran on the pages it changed |
data.versionId | string | undefined | The version the run saved |
data.failure | string | undefined | Why it failed: INSUFFICIENT_CREDITS, PROJECT_CREDIT_LIMIT_REACHED, DEPLOY_FAILED, RUN_TIMED_OUT, RUN_INTERRUPTED, CONTENT_BLOCKED, … or OTHER |
data.creditsSpent | number | undefined | What the run cost, once it ended |
data.previewAt | string | undefined | When the preview last started serving this run's new build (reload your preview frame when it changes) |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/progressconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/agent/progress", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"runId": "run_01k6xc4n7d",
"state": "running",
"stage": "working",
"startedAt": "2026-10-08T11:10:00.000Z",
"turns": 12,
"steps": [
{ "at": "2026-10-08T11:12:01.000Z", "kind": "edit", "file": "src/app/contact/page.tsx", "added": 64, "removed": 0 },
{ "at": "2026-10-08T11:12:30.000Z", "kind": "command", "task": "database", "ok": true }
],
"totalSteps": 18,
"files": [{ "path": "src/app/contact/page.tsx", "change": "create", "added": 64, "removed": 0 }],
"counts": { "reads": 6, "searches": 2, "edits": 4, "commands": 3, "failedCommands": 0 }
}
}#Get Project
/api/v1/vcaas/projects/:projectIdapi-keyFreeEverything about one project: its URLs, the agent's state, the last publish, its domain, secrets and spending. Read it again after every prompt: the preview address can change.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.label | string | undefined | Display name, when set |
data.groupId | string | undefined | Group, when filed in one |
data.description | string | Description |
data.plan | string | "api" |
data.createdAt | string | ISO 8601 creation time |
data.agentProcessStatus | string | "init" (a prompt is running), "done" (the last one finished) or "idle" (never prompted) |
data.agentServerStatus | string | "Creating" while the project is set up, "Archived" while it sleeps after a long inactivity (the next prompt wakes it), else "Active" |
data.temporalDevelopmentProjectUrl | string | The development preview URL |
data.developmentUrlFieldToUse | string | Always "temporalDevelopmentProjectUrl" |
data.productionProjectUrl | string | The published host: the custom domain once it is active, else <projectId>.totalum-project.com (it answers only after the first publish) |
data.deployment | object | null | The last publish: { createdAt, status, versionId }, status = "deploying", "success" or "error", versionId = the version it put online (or tried to); null if never published |
data.customDomain | object | null | { _id, projectId, deploymentId, hostname, status, sslStatus, dnsRecordsToAdd, createdAt, updatedAt, source? } — see Custom Domains |
data.secrets | object[] | { _id, secretName, environment } (values are never returned) |
data.versionRecovery | object | null | While a version restore runs or after it failed: { status: "recovering" | "error", versionId, startedAt, errorMessage? } |
data.importInProgress | object | null | { startedAt } while an import runs |
data.previewImageUrl | string | undefined | Screenshot of the home page |
data.totalCreditsSpent | number | Credits this project has spent in total |
data.creditLimits | object | { maxDevelopmentCreditsPerMonth, maxInfrastructureCreditsPerMonth }, null = no cap |
data.multiPrompt | object | null | The multi-prompt batch, as on Get Agent Status |
data.engineVersion | string | "v2" |
data.engineState | string | "queued", "provisioning", "ready", "error", "suspended" or "deleting" |
data.liveHostSuffix | string | "totalum-project.com" or "webapp-project.com": the domain of the main published address and preview (Update Project) |
data.dataResidency | string | "global" or "eu" |
data.sandbox | object | The builder: { state, lastPromptAt } |
data.lifecycle | object | { state, nextStep, lastActivityAt, pausedAt, resumable } — see Pause Project |
data.preview | object | { url, token8, status, deployState }: the preview address and whether its latest build is live (status: "live", "deploying", "failed", "parked", "none") |
data.lastDeployError | object | null | Why the last preview or publish build failed, when it did |
data.currentRun | object | null | The running prompt: { id, state, startedAt, batchId?, promptPreview } |
data.state | string | "active" or "paused-by-user" |
data.pausedAt | string | null | When it was paused |
data.resumable | boolean | Whether a paused project can be resumed |
data.runawayPause | object | null | Set while the emergency stop has paused the site: { pausedAt, resumesAt, metric, value?, threshold? } — see Release Runaway Pause |
data.contextClearPending | boolean | The next prompt starts with a fresh context |
data.operations | object[] | Long operations running now (publish, restore, import…): { kind, id, status, startedAt, errorMessage?, versionId?, progress? } |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookingsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"label": "Barber bookings",
"description": "",
"plan": "api",
"agentProcessStatus": "done",
"createdAt": "2026-10-08T10:30:00.000Z",
"agentServerStatus": "Active",
"deployment": { "createdAt": "2026-10-08T11:02:00.000Z", "status": "success", "versionId": "ver_01k6xd2m4n6p8q0r2s4t6v8w0y" },
"versionRecovery": null,
"importInProgress": null,
"secrets": [{ "_id": "sec_01k6x9", "secretName": "STRIPE_KEY", "environment": "production" }],
"customDomain": null,
"developmentUrlFieldToUse": "temporalDevelopmentProjectUrl",
"temporalDevelopmentProjectUrl": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com",
"productionProjectUrl": "barber-bookings.totalum-project.com",
"totalCreditsSpent": 24.6,
"creditLimits": { "maxDevelopmentCreditsPerMonth": 500, "maxInfrastructureCreditsPerMonth": 300 },
"multiPrompt": null,
"engineVersion": "v2",
"engineState": "ready",
"dataResidency": "global",
"sandbox": { "state": "active", "lastPromptAt": "2026-10-08T10:30:05.000Z" },
"preview": { "url": "https://barber-bookings-dev-temporal-link-preview-k3y9x2ab.totalum-project.com", "token8": "k3y9x2ab", "status": "live", "deployState": "idle" },
"lastDeployError": null,
"currentRun": null,
"state": "active",
"pausedAt": null,
"resumable": false,
"contextClearPending": false,
"operations": []
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
PROJECT_NOT_FOUND | 404 | No project with this id in your account |
API_ACCESS_DISABLED | 403 | API access is switched off for this project in your account settings |
PROJECT_NOT_ALLOWED | 403 | Your API key is restricted to other projects |
#List Projects
/api/v1/vcaas/projectsapi-keyFreeYour projects, newest first, up to 100 per page. The total is in the X-Total-Count header and X-Has-More says whether another page exists.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
limit | number | No | 1–100 (default 100) |
skip | number | No | Projects to skip, for paging (default 0) |
search | string | No | Case-insensitive match on id, label and description |
sortField | string | No | date (default, creation time) or lastModified |
sortDirection | string | No | desc (default) or asc |
groupId | string | No | Only the projects of one group; none = only ungrouped projects |
createdFrom | string | No | Created at or after this date (ISO 8601; a bare day is UTC) |
createdTo | string | No | Created at or before this date (a bare day includes the whole day) |
Response fields
| Field | Type | Description |
|---|---|---|
data[].projectId | string | Project id |
data[].description | string | Description |
data[].plan | string | "api" |
data[].createdAt | string | ISO 8601 creation time |
data[].label | string | undefined | Display name, when set |
data[].groupId | string | undefined | Group, when filed in one |
data[].previewImageUrl | string | undefined | Screenshot of the home page, once a prompt has finished |
data[].lastModifiedAt | string | Last change (prompt, publish, edit); never null |
data[].productionProjectUrl | string | The published host, as on Get Project: the custom domain once it is active, else <projectId>.totalum-project.com (or <projectId>.webapp-project.com when chosen) |
data[].engineVersion | string | "v2" |
data[].liveHostSuffix | string | undefined | "webapp-project.com" when chosen (Update Project); absent otherwise |
Example request
curl -H "api-key: tlm_sk_your_key" \
"https://builder-api.totalum.app/api/v1/vcaas/projects?limit=20&sortField=lastModified"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects?limit=20&sortField=lastModified", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": [
{
"projectId": "barber-bookings",
"description": "",
"plan": "api",
"createdAt": "2026-10-08T10:30:00.000Z",
"label": "Barber bookings",
"previewImageUrl": "https://files.totalum.app/previews/barber-bookings.png",
"lastModifiedAt": "2026-10-08T10:41:00.000Z",
"productionProjectUrl": "barber-bookings.totalum-project.com",
"engineVersion": "v2"
}
]
}#Deploy to Production
/api/v1/vcaas/projects/:projectId/deployments/deployapi-key0.5 creditsPublish the project's current version. It takes about 2 to 5 minutes. If the build or a migration fails, Totalum's agent tries to repair it once or twice before the publish ends in error; the live site keeps serving the previous version until a publish succeeds.
Migrations that delete data. A migration that drops a table or column, or deletes or overwrites existing live rows, runs only when the prompt that made it asked for it, or when you publish with confirmDestructive: true. Otherwise the publish ends in error with DESTRUCTIVE_MIGRATION_NEEDS_CONFIRMATION and nothing changes on live. Rows already in the live database that break a new rule end the publish with the list of those rows; Totalum never changes production data on its own.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
confirmDestructive | boolean | No | true = run the pending migrations that drop columns or tables or delete/overwrite live rows |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | Project id |
data.status | string | "deploying" |
data.deploymentId | string | This publish |
data.message | string | How to follow it |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/deployments/deployconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/deployments/deploy", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "barber-bookings",
"status": "deploying",
"message": "Deployment started. It will take from 2 to 5 minutes depending on project size. Fetch GET /api/v1/vcaas/projects/barber-bookings/deployments/status every 10-15 seconds to track progress.",
"deploymentId": "dep_01k6xf3a9b"
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
AGENT_RUNNING | 409 | A prompt is running; publish when it is done |
DEPLOYMENT_RUNNING | 409 | A publish is already running |
OPERATION_IN_PROGRESS | 409 | A restore, import or other long operation holds the project, or it is still being set up |
INSUFFICIENT_CREDITS | 402 | Not enough credits |
PROJECT_CREDIT_LIMIT_REACHED | 403 | The project's monthly development cap is reached |
#Get Deployment Status
/api/v1/vcaas/projects/:projectId/deployments/statusapi-keyFreeThe newest publish and where it is. Poll every 10–15 seconds until status is "success" or "error".
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.status | string | null | "deploying", "success", "error", or null if never published |
data.createdAt | string | null | When the publish started |
data.deploymentId | string | undefined | The publish |
data.step | string | undefined | "build", "migration", "upload", "probe", "fixing" (the agent is repairing a failed step) or "done" |
data.progress | object | undefined | While step is "fixing": { stage: "fixing", failedStage, fixAttempt, fixAttempts } |
data.errorDetails | object | undefined | On "error": { stage, title, message, logExcerpt, versionId, deploymentId?, migrationName?, fixSummary?, at } — fixSummary says what the repair tried and what you need to do |
data.liveUrl | string | The live address |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/deployments/statusconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/deployments/status", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"status": "success",
"createdAt": "2026-10-08T11:20:00.000Z",
"deploymentId": "dep_01k6xf3a9b",
"step": "done",
"liveUrl": "https://barber-bookings.totalum-project.com"
}
}When it is "success", the address to show is productionProjectUrl on Get Project (the custom domain once active).
#Create Project
/api/v1/vcaas/projectsapi-key2 creditsCreate an empty project: no prompt, nothing built. Use it only when you will import a project into it, connect GitHub first, or write the files yourself; otherwise use Launch Project.
The project is set up in the background (about 15 seconds); a write sent before it is ready waits for it.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Same rules as Launch Project; a taken id gets a suffix |
label | string | No | Display name, up to 80 characters |
description | string | No | Up to 500 characters |
groupId | string | No | An existing project group |
engineVersion | string | No | "v2" to require the current engine (new accounts always get it), as in Launch Project |
dataResidency | string | No | "global" or "eu", as in Launch Project |
audienceCountry | string | No | Two-letter country code of the app's users |
sourceImportCode | string | No | An importCode from Export Project: the new project is created to receive it (checked before anything is created) |
creditLimits | object | No | The project's monthly spending caps, as in Launch Project |
creditLimits.maxDevelopmentCreditsPerMonth | number | null | No | Cap on what the AI agent and project actions spend. Default 5000; null = no cap |
creditLimits.maxInfrastructureCreditsPerMonth | number | null | No | Cap on what the running app spends. Default 300; null = no cap |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The id that was created. Use this one |
data.requestedProjectId | string | undefined | The id you asked for, when it differs |
data.description | string | Project description |
data.plan | string | Always "api" |
data.createdAt | string | ISO 8601 creation time |
data.label | string | undefined | The label, when given |
data.groupId | string | undefined | The group, when given |
data.dataResidency | string | "global" or "eu" |
data.engineVersion | string | "v2" |
data.engineState | string | "provisioning" — the project is being set up |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"projectId":"crm-import","label":"CRM (imported)","creditLimits":{"maxDevelopmentCreditsPerMonth":1000,"maxInfrastructureCreditsPerMonth":500}}' \
https://builder-api.totalum.app/api/v1/vcaas/projectsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"projectId": "crm-import",
"label": "CRM (imported)",
"creditLimits": {
"maxDevelopmentCreditsPerMonth": 1000,
"maxInfrastructureCreditsPerMonth": 500
}
}),
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"projectId": "crm-import",
"description": "",
"plan": "api",
"createdAt": "2026-10-08T10:30:00.000Z",
"label": "CRM (imported)",
"dataResidency": "global",
"engineVersion": "v2",
"engineState": "provisioning"
}
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID / INVALID_PROJECT_NAME / INVALID_PROJECT_NAME_LENGTH / RESERVED_PROJECT_NAME | 400 | The id is missing or malformed |
INVALID_PROJECT_GROUP | 400 | Unknown or full group |
INVALID_DATA_RESIDENCY / INVALID_AUDIENCE_COUNTRY / INVALID_ENGINE_VERSION | 400 | An invalid option |
INVALID_CREDIT_LIMITS / INVALID_LIMIT | 400 | creditLimits is not an object, or a cap is not a positive number or null |
INSUFFICIENT_CREDITS | 402 | Not enough credits for the project |
MAX_PROJECTS_REACHED | 403 | Your plan's project limit is in use |
PROJECT_ALREADY_EXISTS | 409 | The name and every suffixed candidate were taken |
ENGINE_V2_DISABLED | 409 | New projects cannot be created on the current engine right now |
ENGINE_LEGACY_UNAVAILABLE | 409 | engineVersion: "legacy", or a legacy (.zip) export code, on a new account: it creates only on the current engine |
RATE_LIMIT_EXCEEDED | 429 | Too many creates for your plan (new projects and prompts share one per-minute allowance, see Limits); retry after Retry-After seconds |
CREATE_PROJECT_ERROR | 503 | A temporary failure; nothing was created. Retry after Retry-After seconds |
Project limits per plan:
| Plan | Projects at once | New projects |
|---|---|---|
| Free | 2 | 1 a minute |
| Starter | 10 | 5 a minute |
| Builder | 50 | 10 a minute |
| Professional | 300 | 30 a minute |
| Enterprise | Unlimited | 100 a minute |
#Everything else
Each link is the complete reference for one area: the page, and the same page as Markdown for agents.
| Need | Docs |
|---|---|
| Read/write the app's data (rows, SQL, relations), migrations | Database · database.md |
| Backups, restore, time travel, move to Turso | Database Backups & Restore · database-backups.md |
| Versions, diffs, restore a version | Versions · versions.md |
| Read or edit code files, rebuild | Project Files · project-files.md |
| Download source, upload files for prompts | Source Code & Uploads · source-files.md |
| Stop a run, conversation, multi-prompt, agent instructions, MCP servers | AI Agent · agent.md |
| Update, delete, pause, resume a project, its live state | Projects · projects.md |
| Custom domain on a project | Custom Domains · domains.md |
| Buy a domain | Buy a Domain · domain-registration.md |
| Mobile apps, signing, store uploads | Mobile Apps · mobile.md |
| Payments (the app's Stripe) | Payments · payments.md |
| Webhooks (prompt finished, publish ended…) | Webhooks · webhooks.md |
| Scheduled jobs | Scheduled Jobs · cron-jobs.md |
| Secrets, project API keys, .env | Secrets, Keys & .env · secrets.md |
| Logs of the preview and the live site | Logs · server.md |
| Usage, spending, site analytics | Usage & Analytics · analytics.md |
| Spending caps, feature switches, rate limit | Spending & Rate Limits · limits.md |
| Copy a project / templates | Export & Import · transfer.md |
| GitHub sync | GitHub · github.md |
| Build from Figma designs | Figma · figma.md |
| Visual edits without a prompt | Visual Edits · visual-edit.md |
| Folders for projects | Project Groups · project-groups.md |
| Balance, prices, EU-only data | Account · account.md |
| An app that itself creates apps | Building on Totalum's API · build-on-totalum.md |
| Error codes and limits | Errors & Limits · errors.md |
| Every endpoint in one file | full.md · OpenAPI |
| Inside the generated app: auth, e-mail, AI, files, payments, real-time, cron, database driver | Totalum SDK · totalum-sdk.md |
| Use Totalum from Claude Code, Codex, Cursor | Totalum MCP |
| Stack, running locally, self-hosting | For Developers |
| Legacy projects | Legacy projects: what differs |
