AI Agent
Drive the AI agent that builds and edits your project, in single- or multi-prompt mode.
The AI agent builds and modifies your project from natural-language prompts. Agent runs are asynchronous — you start a run, then poll for status. All endpoints require the api-key header.
#Run AI Agent
/api/v1/vcaas/projects/:projectId/agent/startUses creditsStart the AI agent with a prompt to build or modify your project. Supports two modes:
Single-prompt (default, recommended): omit
multiPrompt. One prompt, typically 10 to 30 minutes and 10 to 40 credits.The API's own
messagefield quotes a wider 4 to 40 minutes — that is the full envelope a run can take, from a one-line tweak to a large build.10 to 30is where a normal prompt lands; use the wider figure only if you are writing a timeout.Multi-prompt (rare, opt-in): include
multiPrompt. A sequential batch that runs unsupervised. Each prompt still takes 10 to 30 minutes and 10 to 40 credits, so a 10-prompt batch can run for several hours and burn hundreds of credits.
Budget 10 to 40 credits per prompt. That is the range to show in your own UI, and it is what a normal run lands in.
The exact price is metered, not fixed: 10 credits are taken when the run starts, and the rest is charged when it finishes, from how much work the run actually turned out to be. Two things sit outside the typical range and are worth knowing about before they surprise you:
- A run that uses its entire time budget (the agent works until it is stopped at its ceiling) is billed a flat 50 credits, or 60 on a healthy balance, for the whole prompt.
- A failed run is refunded, so a prompt that produced nothing does not cost you the full price.
The same numbers apply whether the run was started by POST /projects/launch or POST /agent/start — it is the same run.
The API only refuses a run when your balance is below its hard minimum, which is far below the price of a prompt. That is not the number to budget against. A run started with less than it ends up costing will drain the balance to zero and the shortfall is absorbed, but the next one is refused — so keep a working balance comfortably above the 10-to-40 a prompt costs rather than topping up to the floor.
Only use multi-prompt when a single job genuinely requires several sequential AI runs AND the user has explicitly accepted the cost and time. For everything else, send one prompt at a time — it is faster, cheaper, and lets the user steer between steps. Multi-prompt is unsupervised: no human approval step, no pause between prompts. If unsure, do NOT pass multiPrompt.
Returns immediately with status "init". Poll GET /agent/status every 10–15s and show realtimeConversation messages live. Single-prompt: complete when status is "done". Multi-prompt: poll multiPrompt.status until "done"/"cancelled" — the per-prompt status flips between "init" and "done" repeatedly. After every prompt completes you MUST refresh the preview URL via GET /projects/:projectId → developmentUrlFieldToUse (default temporalDevelopmentProjectUrl if null/undefined).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project to run the agent on |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes¹ | Single-prompt: the user instruction. Multi-prompt + letTotalumDecide: the high-level goal Totalum breaks down. Multi-prompt + prompts[]: ignored. |
inputFiles | array | No | Reference images/files for the agent |
inputFiles[].name | string | Yes | File name |
inputFiles[].imageDescription | string | Yes | Description of the image content |
inputFiles[].url | string | Yes | Public URL or uploaded file URL |
multiPrompt | object | No | Opt into the unsupervised multi-prompt batch mode. Omit for normal use. |
multiPrompt.prompts | string[] | No² | Up to 50 prompts to run sequentially. Each item must be a non-empty string. |
multiPrompt.letTotalumDecide | boolean | No² | When true, Totalum plans the prompt list from the top-level prompt. |
¹ prompt is required unless multiPrompt.prompts is provided (then it is ignored).
² When multiPrompt is present, exactly one of prompts (non-empty array, ≤ 50 items) or letTotalumDecide=true must be set.
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The project ID |
data.status | string | Always "init" on success — for both single and multi-prompt mode |
data.message | string | Instructions on how to poll for status (multi-prompt includes the cost/time warning) |
data.multiPrompt | object | undefined | Present only when the request had multiPrompt. Detailed batch status is on GET /agent/status under multiPrompt. |
data.multiPrompt.totalPrompts | number | undefined | Known when the caller provided prompts; absent while Totalum is still planning a letTotalumDecide batch. |
Example request
# Single-prompt (recommended)
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Create a landing page with a contact form","inputFiles":[]}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
# Multi-prompt with explicit list
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"multiPrompt":{"prompts":["Set up auth","Add a Stripe checkout","Wire up the dashboard"]}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start
# Multi-prompt with Totalum planning
curl -X POST -H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prompt":"Build a full SaaS billing system with auth, Stripe, and a customer portal","multiPrompt":{"letTotalumDecide":true}}' \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/start{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"message": "Process started, can take from 4 to 40 minutes. Fetch GET .../agent/status every 10-15 seconds to track progress."
}
}Multi-prompt response also includes multiPrompt:
{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"message": "Multi-prompt run started with 3 prompts. ⚠️ Multi-prompt mode is expensive and slow — only use it when a job genuinely requires multiple sequential AI runs. Each prompt typically takes 10 to 30 minutes and costs 10 to 40 credits...",
"multiPrompt": { "totalPrompts": 3 }
}
}{
"errors": { "errorCode": "AGENT_RUNNING", "errorMessage": "An agent is already running on this project" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
MISSING_PROMPT | 400 | prompt is required (single-prompt mode, or letTotalumDecide=true — it is the planner goal) |
INVALID_MULTI_PROMPT | 400 | multiPrompt was set but neither (or both) of prompts[] and letTotalumDecide=true were provided |
TOO_MANY_PROMPTS | 400 | multiPrompt.prompts exceeds the 50-item limit |
INVALID_PROMPT_ITEM | 400 | A multiPrompt.prompts entry is not a non-empty string |
AGENT_RUNNING | 409 | An agent is already running on this project |
AUTO_EXECUTION_ALREADY_ACTIVE | 409 | A previous multi-prompt batch is still active (executing/paused/awaiting approval). Cancel it first. |
DEPLOYMENT_RUNNING | 409 | Cannot start agent while a deployment is in progress |
RECOVERY_RUNNING | 409 | Cannot start agent while a version recovery is in progress — poll versionRecovery until null, then retry |
INSUFFICIENT_CREDITS | 402 | Balance is below the minimum needed to start a run. A typical prompt costs 10 to 40 credits, so keep a working balance well above the floor — multi-prompt needs many times more |
PROMPT_SECURITY_VIOLATION | 400 | Prompt failed security validation |
#Get Agent Status
/api/v1/vcaas/projects/:projectId/agent/statusFreePoll to track agent progress and get real-time messages. Poll every 10–15 seconds.
When status becomes "done", you MUST refresh the preview URL by calling GET /projects/:projectId and reading developmentUrlFieldToUse ("temporalDevelopmentProjectUrl" for the live server or "cachedDevelopmentUrl" for a cached snapshot). If developmentUrlFieldToUse is null or undefined, default to temporalDevelopmentProjectUrl. The URL can change after each agent run.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project to poll |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The project ID |
data.status | string | "init" (a prompt is currently running) | "done" (last prompt finished) | "idle" (never run). For multi-prompt runs this flips between "init" and "done" repeatedly as the batch advances — poll multiPrompt.status to track the whole batch. |
data.startedAt | string | null | ISO 8601 start time of the current prompt (not the whole batch) |
data.realtimeConversation | array | Single-prompt: messages from the current run. Multi-prompt: messages from the whole batch (scoped to multiPrompt.startedAt). |
data.realtimeConversation[].author | string | "user" | "agent" |
data.realtimeConversation[].message | string | The message text |
data.realtimeConversation[].messageType | string | "regular" | "starting" | "building" | "finished" | "error" | "limit-reached" |
data.realtimeConversation[].createdAt | string | ISO 8601 message date |
data.realtimeConversation[].versionId | string | undefined | Version created at this step |
data.realtimeConversation[].secretKeysNeeded | object | undefined | Secrets the agent needs (key: { isProvided, description }) |
data.realtimeConversation[].gitDiffUrl | string | undefined | Diff URL for this step |
data.creditsSpent | number | undefined | Credits spent on the current prompt (present when status is "done") |
data.multiPrompt | object | null | Present only when a multi-prompt batch exists. The canonical signal for "is a multi-prompt run in flight" — poll its status until "done"/"cancelled". |
data.multiPrompt.status | string | "planning" | "executing" | "paused" | "done" | "cancelled" |
data.multiPrompt.totalPrompts | number | Number of prompts in the batch (0 during planning) |
data.multiPrompt.currentPromptIndex | number | Index of the in-flight prompt; -1 before the first one starts |
data.multiPrompt.prompts | array | Ordered list of prompts in the batch |
data.multiPrompt.prompts[].order | number | Position in the batch |
data.multiPrompt.prompts[].prompt | string | The prompt text |
data.multiPrompt.prompts[].status | string | "pending" | "executing" | "done" | "failed" |
data.multiPrompt.startedAt | string | ISO 8601 start time of the whole batch |
data.multiPrompt.updatedAt | string | ISO 8601 last change to the batch state |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/status{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"startedAt": "2026-03-11T10:35:00.000Z",
"realtimeConversation": [
{
"author": "agent",
"message": "Creating the project structure...",
"messageType": "building",
"createdAt": "2026-03-11T10:36:00.000Z"
},
{
"author": "agent",
"message": "Project built successfully!",
"messageType": "finished",
"versionId": "v_abc123",
"gitDiffUrl": "https://...",
"secretKeysNeeded": {
"STRIPE_SECRET_KEY": { "isProvided": false, "description": "Required for processing payments" }
},
"createdAt": "2026-03-11T10:50:00.000Z"
}
],
"creditsSpent": 3.2,
"multiPrompt": null
}
}Multi-prompt example (running prompt 2 of 3):
{
"errors": null,
"data": {
"projectId": "my-app",
"status": "init",
"startedAt": "2026-03-11T11:05:00.000Z",
"realtimeConversation": [ /* messages from the whole batch */ ],
"multiPrompt": {
"status": "executing",
"totalPrompts": 3,
"currentPromptIndex": 1,
"prompts": [
{ "order": 0, "prompt": "Set up auth", "status": "done" },
{ "order": 1, "prompt": "Add Stripe checkout", "status": "executing" },
{ "order": 2, "prompt": "Wire up dashboard", "status": "pending" }
],
"startedAt": "2026-03-11T10:35:00.000Z",
"updatedAt": "2026-03-11T11:05:00.000Z"
}
}
}{
"errors": { "errorCode": "PROJECT_NOT_FOUND", "errorMessage": "Project does not exist or you don't own it" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
#Get Full Conversation
/api/v1/vcaas/projects/:projectId/agent/full-conversationFreeRetrieve the complete conversation history across all agent runs (not just the current one).
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project whose conversation to fetch |
Response fields
| Field | Type | Description |
|---|---|---|
data.projectId | string | The project ID |
data.conversation | array | All messages from all agent runs |
data.conversation[].author | string | "user" | "agent" |
data.conversation[].message | string | The message text |
data.conversation[].messageType | string | "regular" | "starting" | "building" | "finished" | "error" | "limit-reached" |
data.conversation[].createdAt | string | ISO 8601 message date |
data.conversation[].versionId | string | undefined | Version created at this step |
data.conversation[].secretKeysNeeded | object | undefined | API keys the agent needs (key: { isProvided, description }). Add missing keys as project secrets. |
data.conversation[].gitDiffUrl | string | undefined | Diff URL for this step |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/full-conversation{
"errors": null,
"data": {
"projectId": "my-app",
"conversation": [
{ "author": "user", "message": "Create a landing page", "messageType": "regular", "createdAt": "..." },
{ "author": "agent", "message": "Building project structure...", "messageType": "building", "createdAt": "..." },
{
"author": "agent",
"message": "Project built successfully!",
"messageType": "finished",
"versionId": "v_abc123",
"gitDiffUrl": "https://...",
"secretKeysNeeded": {
"STRIPE_SECRET_KEY": { "isProvided": false, "description": "Required for processing payments" }
},
"createdAt": "..."
}
]
}
}{
"errors": { "errorCode": "PROJECT_NOT_FOUND", "errorMessage": "Project does not exist or you don't own it" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
#Stop Agent
/api/v1/vcaas/projects/:projectId/agent/stopFreeSend a stop signal to a running agent.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project whose agent to stop |
Response fields
| Field | Type | Description |
|---|---|---|
data.message | string | Confirmation message |
Example request
curl -X POST -H "api-key: tlm_sk_your_key" \
https://api-accounts.totalum.app/api/v1/vcaas/projects/my-app/agent/stop{ "errors": null, "data": { "message": "Agent stop signal sent" } }{
"errors": { "errorCode": "NO_PROCESS_RUNNING", "errorMessage": "No agent process is currently running" },
"data": null
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PROJECT_ID | 400 | projectId is required |
PROJECT_NOT_FOUND | 404 | Project does not exist or you don't own it |
NO_PROCESS_RUNNING | 400 | No agent process is currently running |
#Concurrency rules
- Only one heavy operation at a time per project: agent, deployment, version recovery, or server start.
- If deploy or recover is called and the server is not active, it auto-starts (charges
START_SERVERcredits) and returnsSERVER_NOT_READY. PollGET /projects/:projectIduntilagentServerStatusis"Active", then retry. - Agent start IS allowed during server operations (the backend waits internally).
#Complete integration flow
- Create the project and start building it —
POST /projects/launch, body{ "projectId": "my-app", "prompt": "Build a SaaS landing page" }. One call. Async, takes 10 to 30 minutes. Use theprojectIdit returns (a taken name is auto-suffixed), and checkagent.startedandwarnings.- Already have an empty project? Then start at step 2 instead:
POST /projects/my-app/agent/start, body{ "prompt": "..." }. That is the only case where the two calls are separate — see Launch Project.
- Already have an empty project? Then start at step 2 instead:
- Send follow-up prompts —
POST /projects/my-app/agent/start, body{ "prompt": "Add a pricing page" }. Same endpoint, same polling, for every change after the first. - Poll agent status (every 10–15s) —
GET /projects/my-app/agent/status."init"= working (showrealtimeConversation),"done"= finished (creditsSpent= total). On"done", immediately callGET /projects/:projectIdand refresh the preview URL.- 3.1 Refresh preview URL (on page load, page refresh, and after every prompt finishes) —
GET /projects/my-app, readdevelopmentUrlFieldToUse: usecachedDevelopmentUrl(static snapshot while the server is archived) ortemporalDevelopmentProjectUrl(live dev server); defaulttemporalDevelopmentProjectUrlifnull/undefined. Reload the iframe when the URL changes.
- 3.1 Refresh preview URL (on page load, page refresh, and after every prompt finishes) —
- Publish —
POST /projects/my-app/deployments/deploy. Builds, deploys, and assigns a public URL. Takes 2 to 5 minutes. - Poll deployment status (every 10–15s) —
GET /projects/my-app/deployments/status; on"success", getproductionProjectUrlfromGET /projects/my-app. - (Optional) Custom domain —
PUT /projects/my-app/domain, body{ "hostname": "app.yourdomain.com" }; configure the returned DNS records; pollcustomDomain.statusuntil"active".
- Agent and deployment are ASYNCHRONOUS — always poll for status.
- Show real-time agent conversation messages during generation.
- NEVER call the Totalum API from frontend code — always go through your backend server.
- ALWAYS refresh the dev preview URL (
GET /projects/:projectId) on page load, page refresh, and after each prompt; usedevelopmentUrlFieldToUse; defaulttemporalDevelopmentProjectUrlifnull/undefined. - On the dev preview, don't show the production URL; display an open-in-blank link so the user always sees the realtime dev preview (production may be outdated until published).
- Keep the API key secret — it must never leave your backend server.
#Global error codes
All errors are shaped as { "errors": { "errorCode": "CODE", "errorMessage": "..." }, "data": null }.
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_CREDITS | 402 | Not enough credits for this operation |
PROJECT_CREDIT_LIMIT_REACHED | 403 | Project monthly credit limit reached for that category |
PROJECT_NOT_ALLOWED | 403 | API key doesn't have access to this project |
PROJECT_NOT_FOUND | 404 | Project doesn't exist or you don't own it |
AGENT_RUNNING | 409 | Agent already running on this project |
DEPLOYMENT_RUNNING | 409 | A deployment is in progress |
RECOVERY_RUNNING | 409 | A version recovery is in progress |
SERVER_NOT_READY | 409 | Server auto-starting, poll until Active |
NO_DEPLOYMENT | 404 | No deployment found. Deploy first and wait until status is "success" |
MISSING_PROMPT | 400 | prompt field is required |
MISSING_PROJECT_ID | 400 | projectId field is required |
INVALID_PROJECT_NAME | 400 | Invalid projectId format |
INVALID_PROJECT_NAME_LENGTH | 400 | Project name must be 4-35 characters |
PROJECT_ALREADY_EXISTS | 409 | A project with this ID already exists |
PLAN_NOT_API | 400 | Only API plan projects can be deleted |
PROMPT_SECURITY_VIOLATION | 400 | Prompt failed security validation |
NO_PROCESS_RUNNING | 400 | No agent process is currently running |
MISSING_FILE | 400 | file field is required (multipart) |
UPLOAD_FAILED | 500 | File upload failed |
MISSING_VERSION_ID | 400 | versionId is required |
MISSING_SECRET_FIELDS | 400 | secretName and secretValue are required |
INVALID_SECRET_KEY_NAME | 400 | Invalid secret key name format |
MISSING_SECRET_ID | 400 | secretId is required |
MISSING_HOSTNAME | 400 | hostname is required |
MISSING_TABLE_NAME | 400 | tableName is required |
GET_ACCOUNT_ERROR | 400 | Internal error fetching account info |
LIST_PROJECTS_ERROR | 400 | Internal error listing projects |
RATE_LIMIT_EXCEEDED | 429 | Too many requests — project creation is rate-limited per plan (see Projects) |
MISSING_WEBHOOK_FIELDS | 400 | url and event are required |
INVALID_WEBHOOK_URL | 400 | Webhook URL must use HTTPS |
INVALID_WEBHOOK_EVENT | 400 | Event not in allowed list |
WEBHOOK_EVENT_ALREADY_EXISTS | 409 | A webhook for this event already exists |
WEBHOOK_NOT_FOUND | 404 | Webhook not found |
MISSING_GITHUB_FIELDS | 400 | token and repositoryFullName are required |
GITHUB_VALIDATION_FAILED | 400 | Token invalid, missing permissions, or repo not found |
GITHUB_SECRET_STORE_ERROR | 400 | Failed to store GitHub credentials |
GITHUB_SYNC_FAILED | 400 | Initial sync with GitHub failed |
GITHUB_NOT_CONNECTED | 400 | GitHub is not connected to this project |
GITHUB_PULL_ERROR | 400 | Failed to pull changes from GitHub |
MISSING_LIMIT_FIELDS | 400 | At least one credit limit field is required |
INVALID_LIMIT | 400 | Credit limit must be a positive number |
INVALID_SYNC_DIRECTION | 400 | syncDirection must be "totalum_to_github" or "github_to_totalum" |
