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 the project and start building itPOST /projects/launchreturns instantly
2Poll until it's doneGET /projects/:id/agent/status10 to 30 min
3Read the preview URLGET /projects/:idseconds
4Deploy to productionPOST /projects/:id/deployments/deployreturns instantly
5Poll the deployGET /projects/:id/deployments/status2 to 5 min
6Read the production URLGET /projects/:idseconds

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

One call creates *and* builds

POST /projects/launch is the default. It also takes the prompt's attachments, the project's secrets, its credit limits and a Figma token — all of which have to be in place before the first build runs, which is exactly why they belong in this call.

There is a separate Create Project that makes an empty project and starts nothing. It is not recommended unless you specifically want no development started.

#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://platform.totalum.app/api");
  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 AND start building it, in one call.
  //    A taken name is not an error: launch appends random characters and tells
  //    you what it created, so always use the id it returns from here on.
  const launched = await api("POST", "/projects/launch", {
    projectId: PROJECT_ID,
    prompt: PROMPT,
    description: "My first app",
  });
  const projectId: string = launched.projectId;
  if (launched.requestedProjectId) {
    console.log(`"${launched.requestedProjectId}" was taken — created "${projectId}" instead`);
  } else {
    console.log(`Created project "${projectId}"`);
  }

  // A 200 means the PROJECT exists — not that every step happened. Anything that
  // did not is in `warnings`, and each message names the endpoint that retries it.
  for (const w of launched.warnings ?? []) console.warn(`[${w.step}] ${w.errorMessage}`);
  if (!launched.agent?.started) {
    await api("POST", `/projects/${projectId}/agent/start`, { prompt: PROMPT });
  }
  console.log("Agent started. This takes 10 to 30 minutes.\n");

  // 2. 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/${projectId}/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;
    }
  }

  // 3. 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/${projectId}`);
  const preview = p[p.developmentUrlFieldToUse ?? "temporalDevelopmentProjectUrl"];
  console.log(`Preview:  ${preview ?? "(no dev server running yet)"}`);

  // 4. Deploy. If the dev server was archived it auto-starts, so retry while it wakes.
  for (;;) {
    try {
      await api("POST", `/projects/${projectId}/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.");

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

  // 6. The production URL lives on the project, not on the deployment.
  const done = await api("GET", `/projects/${projectId}`);
  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 POST /projects/:id/agent/start.

#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. projects/launch, 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. The hard floor for starting a run is well below what a run costs, so budget on the price of a prompt (10 to 40 credits), not on the floor.

#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