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.