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 awaitearly cancels the underlying fetch — no leaked sockets. : pingSSE heartbeats are silently dropped.- Late subscribers get the full persisted log as a single
outputevent on join, then live frames.
Bidirectional WebSocket
import { connect } from "@flowkoi/sdk/ws";
const sock = connect(flowkoi, runId);
sock.on<string>("terminal:output", (data) => process.stdout.write(data));
sock.sendMessage("write a haiku about the API");
sock.injectFiles([{ path: "README.md", content: "..." }]);
sock.close();
sock.raw exposes the underlying WebSocket if you need it.
Polyfilling on old Node
Node 22+ has globalThis.WebSocket. On older Node:
import WebSocket from "ws";
connect(flowkoi, runId, { WebSocket });
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/*).