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: projects created on Professional/Enterprise start on
<projectId>.webapp-project.com(pick withliveHostSuffixat create/launch); for an older project setPATCH /projects/:projectId {"liveHostSuffix":"webapp-project.com"}(the app's emails then also come fromp-<projectId>@mail.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: projects created on Professional/Enterprise start on
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; every project's development environment — preview, its database and jobs — is capped at 100 credits a month,maxDevInfrastructureCreditsPerMonth; at it only the preview stops and the next prompt resumes it). A cap is > 0 ornull;0is refused. - 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. When this month's infrastructure spend reaches the infrastructure cap, the app's built-in features (AI, e-mail, file uploads…) are refused while the site and its database keep running; at the cap plus a reserve of 10 % (at least 5 credits; 300 → 330) only that project is paused: its site and preview show a paused page and its API keys answer 429 PROJECT_CREDIT_LIMIT_REACHED. Both lift when the cap is raised (the pause once the new cap plus its reserve is above this month's spend) or removed (null), or when the next month starts (UTC) — details. Every project's development environment (preview, development database, preview jobs) also has its own cap of 100 credits a month: at it only the preview stops, never the published site (details). 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 (<projectId>.webapp-project.com on Professional and Enterprise). 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, ENGINE_V2_REQUIRES_PLATFORM_ACCOUNT) and nothing is created. New accounts create every project on the current engine, with or without it ("legacy" is refused: ENGINE_LEGACY_UNAVAILABLE); on an older account a create without it can be a legacy project |
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 |
ownerEmail | string | No | The e-mail of the app's owner: the person who may use the app's admin and staff pages once they sign up with it and confirm it in the app. Default: none — then the first person who signs up in the app becomes its admin after entering the 8-digit code e-mailed to them (the preview and the published app each have their own first sign-up). Builders on Totalum (your users build apps through your product): send your user's e-mail. Change it later with Set App Owner |
liveHostSuffix | string | No | "totalum-project.com" or "webapp-project.com": the main domain of the published site and its preview. Default: "webapp-project.com" on the Professional and Enterprise plans, "totalum-project.com" on the others. "webapp-project.com" on a lower plan is refused (403 PLAN_REQUIRED) and nothing is created. Change it later with Update Project |
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 |
ENGINE_V2_REQUIRES_PLATFORM_ACCOUNT | 403 | engineVersion: "v2" (or a current-engine export code) from an account with no Totalum platform account: sign in once at platform.totalum.app, then retry |
VCAAS_INSUFFICIENT_CREDITS | 403 | Your balance is below 1 credit |
PROJECT_ALREADY_EXISTS | 409 | The name and every suffixed candidate were taken |
ENGINE_V2_DISABLED | 409 | New projects cannot be created on the current engine right now |
ENGINE_LEGACY_UNAVAILABLE | 409 | engineVersion: "legacy", or a legacy (.zip) export code, on a new account: it creates only on the current engine |
RATE_LIMIT_EXCEEDED | 429 | You are creating projects faster than your plan allows (new projects and prompts share one per-minute allowance, see Limits) — 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.
A project's first prompt starts on any balance of 1 credit or more (there is no higher minimum): it is scoped to the credits you have (a smaller working first version on a small balance) and stops while 5 credits are still left for its wrap-up, so it ends with a working preview and a balance that does not go below 0.
#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, the default for projects created on Professional and Enterprise) |
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 |
ownerEmail | string | No | The e-mail of the app's owner (its admin once confirmed in the app). Default: none — then the first person who signs up in the app becomes its admin after entering the 8-digit code e-mailed to them (the preview and the published app each have their own first sign-up); see Set App Owner |
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 |
liveHostSuffix | string | No | "totalum-project.com" or "webapp-project.com", as in Launch Project. Default: "webapp-project.com" on Professional and Enterprise, else "totalum-project.com" |
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 |
ENGINE_V2_REQUIRES_PLATFORM_ACCOUNT | 403 | engineVersion: "v2" (or a current-engine export code) from an account with no Totalum platform account: sign in once at platform.totalum.app, then retry |
PROJECT_ALREADY_EXISTS | 409 | The name and every suffixed candidate were taken |
ENGINE_V2_DISABLED | 409 | New projects cannot be created on the current engine right now |
ENGINE_LEGACY_UNAVAILABLE | 409 | engineVersion: "legacy", or a legacy (.zip) export code, on a new account: it creates only on the current engine |
RATE_LIMIT_EXCEEDED | 429 | Too many creates for your plan (new projects and prompts share one per-minute allowance, see Limits); 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, list and delete stored files | 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 |
