TypeScript SDK

@flowkoi/sdk — types, async iterators, WS helper, webhook verification.

The official TypeScript SDK for FlowKoi. Works in Node 18+, modern browsers, Bun, Deno, and Cloudflare Workers.

npm install @flowkoi/sdk
# or pnpm add / yarn add

Source: github.com/FlowKoi/platform/tree/main/packages/sdk

Hello world

import { FlowKoi } from "@flowkoi/sdk";

const flowkoi = new FlowKoi({ apiKey: process.env.FLOWKOI_API_KEY! });

const run = await flowkoi.flows.run("flow_xxx", {
  prompt: "Summarise the latest README change.",
});

for await (const event of flowkoi.runs.stream(run.runId)) {
  if (event.type === "output") process.stdout.write(event.data);
  if (event.type === "finished") break;
}

Construction

new FlowKoi({
  apiKey: string;        // required
  baseUrl?: string;      // default 'https://api.flowkoi.com'
  fetch?: typeof fetch;  // for tests / Workers / custom proxies
});

The constructor only validates apiKey is present — no network round-trip — so it’s cheap to new FlowKoi() per request if you prefer.

Surface

Flows

flowkoi.flows.list();                          // Flow[]
flowkoi.flows.run(flowId, {
  prompt?: string,
  callbackUrl?: string,
  idempotencyKey?: string,
});                                            // Run

Runs

flowkoi.runs.get(runId);                       // Run
flowkoi.runs.logs(runId, { offset?, limit? }); // RunLogsSlice
flowkoi.runs.stop(runId);                      // { runId, stopping?: true, alreadyStopped?: true }
flowkoi.runs.delete(runId);                    // void

// Async-iterable SSE stream
for await (const ev of flowkoi.runs.stream(runId)) { /* ... */ }

Files & messages

flowkoi.runs.files.list(runId);                // FileEntry[]
flowkoi.runs.files.get(runId, path);           // RunFileContent
flowkoi.runs.files.inject(runId, [
  { path: "input.txt", content: "hi" },
]);                                            // { runId, persisted, liveDelivered }

flowkoi.runs.messages.send(runId, "now run the tests"); // { runId, delivered: true }

The SDK appends \n to messages so the agent’s PTY actually submits them — don’t add it yourself.

Streaming

The stream is an async iterable of typed events:

type StreamEvent =
  | { type: "output";        data: string }
  | { type: "status";        status: "running" | "sleep"; claudeStatus: ClaudeStatus | null }
  | { type: "claude_status"; status: "working" | "idle" | "waiting" | "error" }
  | { type: "finished";      sessionId: string; reason?: string; exitCode?: number }
  | { type: "filetree";      tree: unknown };

Behaviour worth knowing:

  • Stream auto-closes on finished.
  • Breaking out of the for await early cancels the underlying fetch — no leaked sockets.
  • : ping SSE heartbeats are silently dropped.
  • Late subscribers get the full persisted log as a single output event on join, then live frames.

Conversations

There is no WebSocket client. Runs accept no input; a conversation goes through flows.chat(), which keeps context across turns via sessionId.

const a = await flowkoi.flows.chat(flowId, { message: "hello" });
if (a.truncated) {
  // the 120s deadline fired mid-answer — `reply` is a fragment and the flow
  // is still running. Stream instead, or send another message.
}
const b = await flowkoi.flows.chat(flowId, {
  message: "and again?",
  sessionId: a.sessionId,
});

To stream a conversation, open the session’s stream first and send with stream: true:

const s = await flowkoi.flows.session(flowId);
const stream = flowkoi.runs.stream(s.runId);

await flowkoi.flows.chat(flowId, { message: "hi", sessionId: s.sessionId, stream: true });

for await (const ev of stream) {
  if (ev.type === "chat_output") process.stdout.write(ev.text);
  if (ev.type === "turn_end") break;   // this turn; the stream stays open
}

@flowkoi/sdk/ws was removed — it could only reach an internal socket that accepts file writes and deletes, and was never safe to expose.

Webhook verification

import { verifyWebhook } from "@flowkoi/sdk/webhook";

app.post("/webhooks/flowkoi", express.raw({ type: "application/json" }), async (req, res) => {
  const result = await verifyWebhook(
    req.body,
    req.header("x-flowkoi-signature"),
    process.env.FLOWKOI_WEBHOOK_SECRET!,
  );
  if (!result.valid) return res.status(401).end();
  await handleCompletion(result.payload);     // typed CompletionPayload
  res.status(200).end();
});

verifyWebhook is cross-runtime — uses WebCrypto when available (browser, Workers, Deno, Bun, modern Node), falls back to node:crypto dynamic import on older Node. The fallback is tree-shaken out of browser bundles automatically.

Pass the raw body — see Webhooks for why.

Errors

Every typed error subclass:

import {
  FlowKoiError,
  FlowKoiAuthError,
  FlowKoiNotFoundError,
  FlowKoiValidationError,
  FlowKoiConflictError,
  FlowKoiUpstreamError,
  FlowKoiRateLimitError,
  FlowKoiNetworkError,
} from "@flowkoi/sdk";

FlowKoiRateLimitError carries retryAfterSeconds, limit, current. The base FlowKoiError carries .code, .status, .body so you can introspect codes the SDK hasn’t given a dedicated class to yet.

Versioning

The SDK follows semver. 0.x while the surface settles; breaking changes get a release note. The SDK pins to a major version of the public API (/api/v1/*).