Back
API & MCP

Calling the API from TypeScript

A small typed client over fetch for the VULK REST API — no package to install.

Overview

The VULK API is plain HTTPS and JSON, so it needs no SDK: fetch in Node 18+, Deno, Bun or any server runtime is enough. This page is a complete, typed client you can paste into a project, covering the whole endpoint reference. Keep the key on the server — never in browser code.

The client

// vulk.ts — a typed client for https://app.vulk.dev/api/v1
const BASE = 'https://app.vulk.dev/api/v1';

export class VulkApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string,
    readonly retryAfter: number | null = null,
  ) {
    super(message);
  }
}

export interface Project {
  id: string;
  title: string;
  kind: 'app' | 'image' | 'video' | 'mesh';
  createdAt: string;
  url: string;
}

export interface Receipt {
  requestId: string;
  accepted: boolean;
  operationStatus: 'running' | 'waiting' | 'done' | 'failed' | 'orphan' | 'unconfirmed' | null;
  projectId?: string;
  editorUrl?: string;
  nextAction: string;
}

export interface Operation {
  requestId: string;
  projectId: string | null;
  operationStatus: 'running' | 'waiting' | 'done' | 'failed' | 'orphan' | null;
  stopStatus: 'running' | 'stopping' | 'stopped' | 'already-delivered';
  pendingProvider: boolean;
  decision: 'GENERATE' | 'RESPOND' | 'CLARIFY' | 'ERROR' | null;
  startedAt: string | null;
  finishedAt: string | null;
  editorUrl: string | null;
  currentProjectPreviewUrl: string | null;
  currentProjectLiveUrl: string | null;
  responses: { text: string; questions?: { question: string; options: string[] }[] }[];
  responsesTruncated: boolean;
}

export class Vulk {
  constructor(private readonly apiKey: string) {}

  private async call<T>(method: string, path: string, body?: unknown): Promise<T> {
    const response = await fetch(`${BASE}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    const data = await response.json();
    if (!response.ok) {
      throw new VulkApiError(data.status ?? response.status, data.code ?? 'SERVER_ERROR', data.error, data.retryAfter ?? null);
    }
    return data as T;
  }

  listProjects(limit = 20, offset = 0) {
    return this.call<{ projects: Project[]; count: number; pagination: { limit: number; offset: number; count: number; hasMore: boolean } }>(
      'GET', `/projects?limit=${limit}&offset=${offset}`,
    );
  }

  getProject(id: string) {
    return this.call<Record<string, unknown>>('GET', `/projects/${id}`);
  }

  deleteProject(id: string) {
    return this.call<{ success: true; id: string; deletedAt: string }>('DELETE', `/projects/${id}`);
  }

  /** Starts a build. Keep the requestId: it is how you follow, retry and stop the build. */
  createProject(prompt: string, options: { model?: string; requestId?: string } = {}) {
    const requestId = options.requestId ?? crypto.randomUUID();
    return this.call<Receipt>('POST', '/projects', { prompt, requestId, ...(options.model ? { model: options.model } : {}) });
  }

  /** Sends the next instruction to a project — including the answer to a question VULK asked. */
  continueProject(projectId: string, prompt: string, options: { model?: string; requestId?: string } = {}) {
    const requestId = options.requestId ?? crypto.randomUUID();
    return this.call<Receipt>('POST', `/projects/${projectId}/messages`, { prompt, requestId, ...(options.model ? { model: options.model } : {}) });
  }

  getOperation(requestId: string) {
    return this.call<Operation>('GET', `/operations/${requestId}`);
  }

  stopOperation(requestId: string) {
    return this.call<Operation>('POST', `/operations/${requestId}/stop`);
  }

  listFiles(projectId: string) {
    return this.call<{ projectId: string; version: number; files: { path: string; bytes: number }[]; totalFiles: number; totalBytes: number; truncated: boolean }>(
      'GET', `/projects/${projectId}/files`,
    );
  }

  readFile(projectId: string, path: string) {
    return this.call<{ projectId: string; path: string; bytes: number; content: string; truncated: boolean }>(
      'GET', `/projects/${projectId}/files?path=${encodeURIComponent(path)}`,
    );
  }

  listModels() {
    return this.call<{ models: { id: string; name: string; credits: number; locked: boolean; requiredPlan: string | null }[]; count: number }>('GET', '/models');
  }

  usage() {
    return this.call<{ plan: string | null; status: 'active' | 'trialing' | 'past_due' | 'none'; creditBalance: number; creditsUsedLast30Days: number }>('GET', '/usage');
  }

  /** Polls an operation until it is no longer running. Resolves on done, waiting, failed or orphan. */
  async waitFor(requestId: string, everyMs = 5000): Promise<Operation> {
    for (;;) {
      const operation = await this.getOperation(requestId);
      if (operation.operationStatus !== 'running' && operation.operationStatus !== null) return operation;
      await new Promise((resolve) => setTimeout(resolve, everyMs));
    }
  }
}

Using it

import { Vulk, VulkApiError } from './vulk';

const vulk = new Vulk(process.env.VULK_API_KEY!);

const receipt = await vulk.createProject(
  'A landing page for a bakery in Porto: hero, the bread of the week, opening hours, a map and a contact form.',
);
console.log(receipt.editorUrl);

let operation = await vulk.waitFor(receipt.requestId);

// VULK may ask before building. Show the questions, then answer with the next instruction.
if (operation.operationStatus === 'waiting') {
  const questions = operation.responses.flatMap((reply) => reply.questions ?? []);
  console.log(questions);
  const answer = await vulk.continueProject(operation.projectId!, 'Show the bread of the week in the hero.');
  operation = await vulk.waitFor(answer.requestId);
}

if (operation.operationStatus === 'done') {
  const { files } = await vulk.listFiles(operation.projectId!);
  console.log(files.map((file) => file.path));
}

Errors

Every error is a VulkApiError with the API's status, code and sentence, and retryAfter on a 429:

try {
  await vulk.createProject('…');
} catch (error) {
  if (error instanceof VulkApiError) {
    switch (error.code) {
      case 'UNAUTHORIZED':      // the key is missing, revoked or expired
      case 'FORBIDDEN':         // the key lacks projects.write — create one with it
      case 'PAYMENT_REQUIRED':  // the plan or balance cannot cover the brief
      case 'RATE_LIMITED':      // wait error.retryAfter seconds
        console.error(error.status, error.code, error.message);
    }
  }
}

The codes and what to do about each are in API authentication issues.

Retrying safely

A build is submitted once per requestId. If the network drops after you sent POST /projects, send it again with the same requestId and body: the API answers 200 with the operation it already created, and nothing is spent twice. The client above makes a requestId for you when you do not pass one — keep it from the receipt.

Other languages

Any HTTP client works the same way. In Python:

import os, requests

api = "https://app.vulk.dev/api/v1"
headers = {"Authorization": f"Bearer {os.environ['VULK_API_KEY']}"}

projects = requests.get(f"{api}/projects", headers=headers).json()
for project in projects["projects"]:
    print(project["id"], project["title"])

GET https://app.vulk.dev/api/v1 returns the list of endpoints with their scopes, so a client can discover the surface at runtime.

On this page

VULK Support

Online

Hi! How can I help you today?

Popular topics

AI support • support.vulk.dev