Docs
BlogHomeStart building

Quickstart: Build and Deploy Your First App

One script, start to finish. Get an API key, create a project, send a prompt, watch it build, and deploy it to a public URL.


These docs are for using the AI app builder via the API. If you'd rather build visually, click and prompt in your browser, use the app builder directly — no code required:

This is the shortest complete path through the Totalum App Builder API: one script that takes a natural-language prompt and returns a live, deployed application — with database, auth and hosting already wired up. Copy it, run it, and you have a production URL.

Using an AI agent? If you work with Claude Code, Codex, Cursor or any other coding agent, give it the docs and it can drive the whole API for you — copy them to your clipboard or hand it the file:

Download the docs fileComplete reference as a single .md file

#The flow

#StepEndpointTime
1Create a projectPOST /projectsseconds
2Run the AI agentPOST /projects/:id/agent/startreturns instantly
3Poll until it's doneGET /projects/:id/agent/status10 to 30 min
4Read the preview URLGET /projects/:idseconds
5Deploy to productionPOST /projects/:id/deployments/deployreturns instantly
6Poll the deployGET /projects/:id/deployments/status2 to 5 min
7Read the production URLGET /projects/:idseconds

Steps 2 and 5 are asynchronous: they return immediately and you poll for completion. Everything else is a plain, instant request.

#The complete script

Save as quickstart.ts, then run it:

bash
npm install -g tsx          # or: npx tsx quickstart.ts
export TOTALUM_API_KEY=tlm_sk_your_key
npx tsx quickstart.ts

This is the whole program — nothing is elided, and there are no other files:

typescript
/**
 * Totalum App Builder API — end-to-end quickstart.
 * Creates a project, builds it with the AI agent, and deploys it to production.
 *
 * Run:  TOTALUM_API_KEY=tlm_sk_your_key npx tsx quickstart.ts
 */

const API_KEY = process.env.TOTALUM_API_KEY;

// 4-35 chars, lowercase letters/numbers/hyphens, must start with a letter.
const PROJECT_ID = "my-first-app";
const PROMPT =
  "Build a task tracker: a landing page explaining the product, and a dashboard " +
  "where a logged-in user can create, complete and delete tasks.";

if (!API_KEY) {
  console.error("Missing TOTALUM_API_KEY. Get one at https://accounts.totalum.app/vcaas");
  process.exit(1);
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

/** Every call goes through here. Unwraps `{ errors, data }`; throws on failure. */
async function api(method: string, path: string, body?: unknown) {
  const res = await fetch(`https://api-accounts.totalum.app/api/v1/vcaas${path}`, {
    method,
    // The key is a server-side secret. Never ship it to a browser.
    headers: { "api-key": API_KEY!, "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  });

  const r = await res.json();
  // Check the status too, not just `errors`: a rejected API key answers 401 with
  // a bare { error } instead of the envelope, which would otherwise slip through
  // as a success with `data: undefined`.
  if (!res.ok || r.errors) {
    throw Object.assign(new Error(r.errors?.errorMessage ?? r.error ?? res.statusText), {
      code: r.errors?.errorCode ?? res.status,
    });
  }
  return r.data;
}

async function main() {
  // 1. Create the project. Re-running the script reuses the existing one.
  try {
    await api("POST", "/projects", { projectId: PROJECT_ID, description: "My first app" });
    console.log(`Created project "${PROJECT_ID}"`);
  } catch (e: any) {
    if (e.code !== "PROJECT_ALREADY_EXISTS") throw e;
    console.log(`Project "${PROJECT_ID}" already exists — reusing it`);
  }

  // 2. Start the agent. Returns instantly — the build runs in the background.
  await api("POST", `/projects/${PROJECT_ID}/agent/start`, { prompt: PROMPT });
  console.log("Agent started. This takes 10 to 30 minutes.\n");

  // 3. Poll until it finishes, printing each new message. Status reads are free.
  for (let printed = 0; ; ) {
    await sleep(12_000);
    const s = await api("GET", `/projects/${PROJECT_ID}/agent/status`);

    for (const m of s.realtimeConversation.slice(printed)) {
      console.log(`  [${m.messageType}] ${m.message}`);
      // The agent tells you here if it needs a secret from you (a Stripe key, etc).
      for (const [key, info] of Object.entries(m.secretKeysNeeded ?? {}) as any) {
        if (!info.isProvided) console.log(`  ! Needs secret: ${key}${info.description}`);
      }
    }
    printed = s.realtimeConversation.length;

    if (s.status === "done") {
      console.log(`\nAgent finished. Credits spent: ${s.creditsSpent ?? "n/a"}`);
      break;
    }
  }

  // 4. Read the preview URL. Re-read it after every run — it can change, and the
  //    field that holds it is named by developmentUrlFieldToUse.
  const p = await api("GET", `/projects/${PROJECT_ID}`);
  const preview = p[p.developmentUrlFieldToUse ?? "temporalDevelopmentProjectUrl"];
  console.log(`Preview:  ${preview ?? "(no dev server running yet)"}`);

  // 5. Deploy. If the dev server was archived it auto-starts, so retry while it wakes.
  for (;;) {
    try {
      await api("POST", `/projects/${PROJECT_ID}/deployments/deploy`);
      break;
    } catch (e: any) {
      if (e.code !== "SERVER_NOT_READY") throw e;
      console.log("Server waking up, retrying in 20s...");
      await sleep(20_000);
    }
  }
  console.log("Deploying. This takes 2 to 5 minutes.");

  // 6. Poll the deploy until it goes live.
  for (;;) {
    await sleep(12_000);
    const d = await api("GET", `/projects/${PROJECT_ID}/deployments/status`);
    if (d.status === "success") break;
    if (d.status === "error") throw new Error("Deployment failed — check the build logs");
  }

  // 7. The production URL lives on the project, not on the deployment.
  const done = await api("GET", `/projects/${PROJECT_ID}`);
  console.log(`\nLive:     ${done.productionProjectUrl}`);
  console.log(`Credits:  ${done.totalCreditsSpent} spent on this project`);
}

main().catch((e) => {
  console.error(e.code ? `API error [${e.code}]: ${e.message}` : e);
  process.exit(1);
});

#What you should see

text
Created project "my-first-app"
Agent started. This takes 10 to 30 minutes.

  [starting] Analyzing your request...
  [building] Creating the tasks table...
  [building] Building the dashboard page...
  [finished] Done! Your task tracker is ready.

Agent finished. Credits spent: 23
Preview:  https://dev-my-first-app.totalum-project.com
Deploying. This takes 2 to 5 minutes.

Live:     https://my-first-app.totalum-project.com
Credits:  31 spent on this project

That URL is a real, deployed Next.js app with a database and auth behind it. Open it, then keep prompting: every follow-up change is another call to runAgent().

#The five things that trip people up

Keep the API key on your server

tlm_sk_ keys carry full account access. The correct topology is User → your frontend → your backend → Totalum API. Never put the key in browser code, a mobile app, or a public repo. If a key leaks, delete it from the API section of your account.

  1. Everything slow is asynchronous. agent/start and deployments/deploy return in milliseconds; the work happens in the background. Poll every 10 to 15 seconds — status reads are free.
  2. Re-read the preview URL after every run. It can change between runs. Always resolve it through developmentUrlFieldToUse, and fall back to temporalDevelopmentProjectUrl when that field is null.
  3. The production URL lives on the project, not on the deployment: GET /projects/:idproductionProjectUrl.
  4. One heavy operation at a time per project. Starting a deploy while the agent runs returns 409 AGENT_RUNNING. Let each finish.
  5. Credits gate the call, not the plan. A zero balance returns 403 VCAAS_INSUFFICIENT_CREDITS, and an agent run needs at least 50 credits to start.

#Try it in Postman

Import the OpenAPI 3.1 spec and every endpoint appears as a ready-to-send request:

  1. In Postman, click Import → Link.
  2. Paste https://www.totalum.app/openapi.json and confirm.
  3. Open the new Totalum App Builder API collection, set the apiKey variable to your tlm_sk_ key, and send any request.

#Where to go next