Unseal Space

TypeScript SDK

Drive unseal-space from Node, Bun, Deno, or an edge runtime with a fully typed client.

@unseal-ai/unseal-space-sdk is a typed adapter over the public unseal-space API. Every method mirrors the API contract 1:1 and returns plain DTOs — no wrapper objects, no hidden state.

npm install @unseal-ai/unseal-space-sdk

Requirements — ESM only (import, no require). Any runtime with a global fetch and async iterators: Node.js 18+, Bun, Deno, or an edge runtime.

A complete round trip

import { createUnsealSpaceClient } from "@unseal-ai/unseal-space-sdk";

const client = createUnsealSpaceClient({
  baseUrl: "https://api.unseal.space",
  apiKey: process.env.UNSEAL_SPACE_API_KEY!,
});

// 1. Create a project. The initial prompt starts the first turn for you.
const project = await client.projects.create({
  initialPrompt: "Build a landing page for a specialty coffee roaster",
});

// 2. Wait for that turn to reach a terminal state.
if (project.initialMessageId) {
  const turn = await client.messages.wait(project.projectId, project.initialMessageId);
  if (turn.status !== "completed") throw new Error(turn.error?.message ?? turn.status);
}

// 3. Look at the live preview.
const preview = await client.preview.get(project.projectId);

// 4. Iterate.
const message = await client.messages.send(project.projectId, {
  content: "Make the hero image full-bleed and add a newsletter form",
});
await client.messages.wait(project.projectId, message.messageId);

// 5. Publish and wait for the deployment.
const accepted = await client.deployments.create(project.projectId);
const deployment = await client.deployments.wait(project.projectId, accepted.deploymentId);

accept → wait

Every long-running operation returns an id immediately and gives you a wait() helper that streams or polls to a terminal state.

  • messages.wait consumes the event stream, reconnects with exponential backoff on transient failures, and resumes from the last cursor — a dropped connection does not lose the turn.
  • deployments.wait and registeredDomains.wait poll.

All of them accept timeoutMs (default 10 minutes) and an AbortSignal.

A timeout or abort ends your local wait only. The turn, deployment, or registration keeps running server-side. Persist the id and resume — never re-issue the mutation, or you will start a second one.

Streaming a turn

messages.events yields AI SDK v5 UI Message Stream chunks — text, reasoning, dynamic tool calls, typed data-* parts — so they fold directly into a UIMessage.

for await (const { chunk, cursor } of client.messages.events(projectId, messageId, { cursor })) {
  if (chunk.type === "text-delta") process.stdout.write(String(chunk.delta));
  if (chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort") break;
}

Persist cursor. Passing it back on a later call replays only what you missed.

Environment variables are write-only

client.environment.* manages a project's variables. You can create, replace, or delete a value, but the platform never returns one — responses carry metadata plus runtime freshness only.

const env = await client.environment.get(projectId);

await client.environment.create(projectId, {
  name: "DATABASE_URL",
  value: process.env.DATABASE_URL!, // cannot be read back later
  kind: "secret",
  preview: true,
  production: true,
});

Mutations take the variable id plus an optimistic-lock expectedVersion from a fresh get(). A stale version fails with engine.ENVIRONMENT_CHANGED — re-read and retry. After a write, preview.stale stays true until the preview dev server restarts, and production.pending stays true until the next deployment.

Errors

Every failure throws UnsealSpaceError — never a bare Error.

import { UnsealSpaceError } from "@unseal-ai/unseal-space-sdk";

try {
  await client.messages.send(projectId, { content: "…" });
} catch (error) {
  if (error instanceof UnsealSpaceError) {
    error.code; // "MESSAGE_IN_FLIGHT"
    error.status; // 409
    error.why; // machine-written cause, when the server provides one
    error.fix; // the suggested recovery action
    error.data; // structured payload, e.g. { messageId }
  }
  throw error;
}

Most server codes are namespaced (app.*, engine.*, billing.*, domain.*). A few declared directly on the contract use a bare UPPER_SNAKE_CASE code and carry a typed payload — MESSAGE_IN_FLIGHT is the one you will meet first, since only one turn runs per project at a time.

Failures raised locally by the SDK carry SDK_* codes: SDK_INVALID_CONFIG, SDK_INVALID_ARGUMENT, SDK_REQUEST_FAILED, SDK_TIMEOUT, SDK_ABORTED, SDK_RECONNECT_EXHAUSTED, SDK_UPLOAD_FAILED.

Idempotency

projects.create, messages.send, deployments.create, versions.restore, registeredDomains.purchase, and the domain bind/assign mutations accept an idempotencyKey. Reuse a key only when retrying the exact same mutation; generate a new one whenever any argument changes.

Types

The root entry exports the client, UnsealSpaceError, and every DTO. Type-only subpaths let you import DTOs without pulling in runtime code:

import type { UnsealSpaceProject } from "@unseal-ai/unseal-space-sdk/projects";
import type { MessageEvent } from "@unseal-ai/unseal-space-sdk/messages";

Available subpaths: /projects, /preview, /messages, /attachments, /environment, /versions, /deployments, /domains.

On this page