Documentation
Build an API client integration
Use the Promise SDK, full Effect contract client, or generated Effect adapters with their actual inputs and error behavior.
Choose a client
The repository has three related client surfaces. They share endpoint declarations, but their construction, input shapes, runtime dependencies, and error channels differ. Choose one deliberately and keep examples from that surface together.
| Surface | Entry point | Execution |
|---|---|---|
| Promise SDK | @cadcamfun/api/sdk, exported CadCam.make | Promises for ordinary results; async iterables for SSE. Uses a supplied or global fetch. |
| Full contract client | ApiClient from @cadcamfun/api | Effect service with transport and bearer middleware supplied by its layer. |
| Generated Effect adapters | packages/api/src/client/generated/index.ts | Flattened methods around the contract client; caller supplies the required transport and middleware. |
The API package is a private workspace with source exports. The imports below are intended for this monorepo or a distribution that actually provides those exports; this page does not claim a separately available public npm package. Independent clients can use the OpenAPI document or plain HTTP.
For a first request without client-library setup, start with the captured HTTP walkthrough. It establishes how success and failure bodies differ between operations.
Resolve the connection
Use the URL and application bearer token belonging to the current sidecar launch. Tauri obtains them through get_sidecar_endpoint. A manual development process prints its assigned port in a ready event. The origin must point at the sidecar or admitted companion gateway, rather than the documentation site or Vite assets.
After a sidecar restart, resolve the connection again. Reusing the previous port can produce connection refusal or accidentally address another process. Reusing a stale launch token produces an authorization failure. A successful public health response does not prove that a protected request will be admitted.
Account selection, application admission, device admission, and external agent access are distinct. The optional x-session-token selects an account after application admission. A project-scoped bridge token is limited to its action bridge endpoints. A phone token works through the gateway's allow-list. Follow the authentication reference before choosing the connection for an integration.
Promise SDK
The exported namespace is CadCam, and its constructor is make. Supply a base URL and default headers. The create and get methods below use flattened fields rather than a nested payload or params object.
import { CadCam } from "@cadcamfun/api/sdk"
const client = CadCam.make({
baseUrl,
headers: { Authorization: "Bearer " + token },
})
const projects = await client.projects.list()
const created = await client.projects.create({
name: "Fixture plate",
description: "An integration-created project",
})
const detail = await client.projects.get({ id: created.id })
console.log(projects.length, detail.name, detail._count.drawings) Each call resolves to that endpoint's success payload directly. projects.list() returns an array; projects.create() returns a project row. There is no universal { data, error } wrapper and no throwOnError constructor option in this SDK. Some endpoint payloads themselves contain data or success because that is their HTTP contract.
The SDK prepares a URL from the endpoint path and configured base URL, encodes path identifiers, serializes declared JSON input, and checks the operation's exact declared success status. It then parses JSON and returns its typed value. The Promise runtime does not apply an Effect schema decoder to every returned payload; TypeScript output types do not validate an arbitrary server implementation.
Declared and transport errors
Declared HTTP failures reject with the decoded response body. That value can be a tagged plain object rather than an instance of JavaScript Error. Transport and protocol failures use the SDK's ClientError.
import { ClientError } from "@cadcamfun/api/sdk"
try {
await client.projects.create({})
} catch (error) {
if (error instanceof ClientError) {
console.error(error.reason, error.message)
} else if (error && typeof error === "object" && "message" in error) {
console.error(String(error.message))
} else {
console.error("Unrecognized API failure", error)
}
} | ClientError reason | Interpretation |
|---|---|
Transport | Fetch setup, connection, abort, or stream-read failure. Inspect its cause and the current connection. |
UnexpectedStatus | The server returned a status outside the operation's declared success and failure statuses. |
UnsupportedContentType | A stream response did not have its expected event-stream content type. |
MalformedResponse | JSON or an expected response stream could not be decoded. |
SseEventTooLarge | The event-stream buffer exceeded the generated runtime's frame guard. |
For example, a missing project name is an HTTP 400 business failure even though the create input type permits omission. A connection reset while creating a project is different: it leaves the client uncertain whether the write completed. Read the resource state before repeating a mutation. The SDK does not add an application-wide idempotency protocol or automatically retry every rejected call.
Cancellation and headers
ClientOptions accepts baseUrl, an optional fetch implementation, and default headers. Per-call RequestOptions accepts a signal and extra headers. For methods without endpoint input, request options are the first argument; methods with input take request options after it.
const controller = new AbortController()
const pending = client.projects.get(
{ id: projectId },
{
signal: controller.signal,
headers: { "x-session-token": accountSessionToken },
},
)
// Call controller.abort() when the operation is no longer needed.
const project = await pending The request builder applies default headers, endpoint-declared headers, and then per-call headers. A per-call value can override a default; avoid accidentally replacing Authorization when adding account or tracing headers. Do not put credentials into endpoint URLs copied into logs.
Injecting fetch supports an in-process test transport or a controlled adapter. The adapter must still honor request headers, body, cancellation, content types, and response status semantics. The desktop's own Effect integration and the phone's pinned native transport do additional work; substituting an arbitrary fetch does not reproduce those native boundaries.
Full Effect client
ApiClient.layer constructs the full HttpApi client, fetch transport, and bearer middleware. It exposes endpoint methods with the underlying HttpApi request shape. A mutation's payload goes under payload, and path values go under params.
import { ApiClient } from "@cadcamfun/api"
import { Effect } from "effect"
const createProject = Effect.gen(function* () {
const api = yield* ApiClient
return yield* api.projects.create({
payload: { name: "Fixture plate" },
})
}).pipe(Effect.provide(ApiClient.layer({ baseUrl, token })))
const project = await Effect.runPromise(createProject) This client derives from the whole Api declaration and is the option for endpoints omitted from generated targets. It preserves Effect's typed error channel and schema-aware transport. Resource lifetime, stream interruption, and layer provision belong to the calling Effect program; creating an Effect does not execute it.
The reusable layer supplies the application token. The desktop builds its own layer in apps/desktop/src/app/client.ts to attach optional account session state and injected transport. Reuse that integration when working inside the desktop instead of creating a second connection/runtime per component.
Generated Effect adapters
The generated Effect client imports the real schemas and adapts methods into flattened fields. Its CadCam.make returns an Effect that constructs the adapted client. It does not provide its own fetch transport or application authorization layer, so the caller must provide the corresponding services and middleware.
The generated error adapter maps transport/schema/SSE failures into a tagged ClientError while retaining declared domain failures. This error class is separate from the Promise SDK's JavaScript error class with a reason string. Name similarity does not make their handling interchangeable.
Keep input examples explicit. The Promise and generated Effect forms use projects.get({ id }); the raw contract form uses projects.get({ params: { id } }). Avoid passing one surface's input object to another through an unchecked type assertion.
Streaming
Promise SSE operations return async iterables. Network setup occurs when iteration begins. The runtime checks the success status and event-stream content type, reads incremental UTF-8 data, normalizes frame boundaries, and JSON-decodes data frames. A malformed frame fails the iteration; reader cleanup runs when iteration ends or exits.
The runtime's guard is 16 * 1024 * 1024 buffered JavaScript string units. It limits accumulation before a frame boundary; it is not a promise that every arbitrary 16 MB payload is useful or valid for the application's event schemas.
Consume AI events according to their discriminators and preserve the session identifier. An ended transport is not proof of a terminal conversation event. Use AI streaming and recovery for queue, abort, permission, question, and state recovery behavior. EventSource is not a drop-in replacement for a POST request with authorization headers and a JSON body.
Coverage and regeneration
The generator has explicit omission lists for shapes it cannot represent. Shared omissions include multipart profile/plugin uploads, MCP creation with multiple success schemas, colliding input names, and plugin serving with response headers on its stream. Additional Promise omissions include raw signed webhooks, JavaScript plugin bootstrap, binary download, and free-form generator requests.
Read the exact current lists in packages/api/scripts/generate-clients.ts and the API overview. An absent generated method does not establish that the server endpoint is unavailable. Use the full contract client or suitable raw transport for that operation.
bun run generate
bun run generate:check
bun run --filter @cadcamfun/web generate:api-reference
bun run --filter @cadcamfun/web check:api-reference Client regeneration and documentation regeneration are separate outputs from the same contract. Keep both current after endpoint changes. Do not edit generated method bodies to change the server's input requirements.
Generated code is written to packages/api/src/client/generated and packages/api/src/sdk/generated. Do not edit those files by hand; change the contract and regenerate.
Integration verification
The Promise create/read/plain-result behavior and thrown declared failure shown here were exercised against the real sidecar handler stack using a migrated disposable database. The HTTP walkthrough contains the corresponding observed responses. Repeat focused checks when changing the generator, authentication, or endpoint schemas.
For an integration acceptance check, verify public health; a protected request without a token; the same request with the current token; a valid create/read cycle; a declared validation failure; a cancelled request; and a stream whose terminal state is checked. Use a disposable project and never infer successful persistence from a frontend fixture alone.
Implementation sources are packages/api/src/sdk/generated/client.ts, its client-error.ts, packages/api/src/client.ts, packages/api/src/client/generated, and packages/httpapi-codegen/src/index.ts. The generator and committed outputs together establish the actual client behavior.
Promise SDK options reference
| Option | Where | Effect |
|---|---|---|
baseUrl | CadCam.make | Resolved with new URL(path, baseUrl); include the scheme and port. |
fetch | CadCam.make | Replace the transport, for example to add logging or to inject a test double. Defaults to globalThis.fetch. |
headers | CadCam.make | Sent with every request. Put authorization here. |
signal | per call | Aborts the request; the rejection is ClientError("Transport"). |
headers | per call | Override or add headers for one call, such as x-session-token. Per-call values win over client values. |
When a body is present and no content-type header was set, the SDK sets application/json and serializes with JSON.stringify. Path parameters are always passed through encodeURIComponent, so a key such as docs.walkthrough.units or one containing / stays a single segment.
ClientError reasons
| Reason | Raised when | Typical response |
|---|---|---|
Transport | fetch threw (refused, reset, DNS, abort) or a stream read failed | Re-resolve the sidecar URL; retry reads; re-read state before retrying writes. |
UnexpectedStatus | The status is neither the success status nor a declared error status. The cause holds { status }. | Treat as a contract mismatch, often a different server version. |
UnsupportedContentType | A stream operation answered with something other than text/event-stream, or a JSON operation with a non-JSON type | Check proxies and the server version. |
MalformedResponse | Empty or invalid JSON, a missing body, or an SSE data: block that is not JSON | Log the raw response; report it. |
SseEventTooLarge | The unparsed SSE buffer exceeded 16 MiB | The event is refused; restart the stream. |
The message of a ClientError is the reason followed by the cause message (for example Transport: fetch failed), so logging error.message alone is already diagnostic.
import { CadCam, ClientError } from "@cadcamfun/api/sdk"
async function withRetry<T>(read: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await read()
} catch (error) {
const transient = error instanceof ClientError && error.reason === "Transport"
if (!transient || attempt >= attempts) throw error
await new Promise((resolve) => setTimeout(resolve, 250 * 2 ** attempt))
}
}
}
const projects = await withRetry(() => client.projects.list()) Do not blindly retry writes
A write that failed with Transport may have been applied. Read the resource again and only resubmit when it is not there; the API has no idempotency keys.
Consuming a stream with the SDK
const controller = new AbortController()
const events = client.ai.chatStream(
{ message: "Pocket the 40 x 30 rectangle, 3 mm deep", projectId },
{ signal: controller.signal },
)
for await (const event of events) {
switch (event.type) {
case "delta": render(event); break
case "permission_request": askUser(event); break
case "done": finish(event); break
case "error": showError(event); break
}
} The iterator parses each SSE block: it joins the data: lines of the block, parses the JSON and yields it. Comments and blocks without data: are skipped. Leaving the loop early (break, an exception, or abort) cancels the reader and releases the connection. The event type values are the ones listed in the assistant streaming guide; check the generated AiChatStreamOutput type for the exact union.
Example: preferences through the SDK
const key = "docs.walkthrough.units"
try {
await client.kv.get({ key })
} catch (error) {
if ((error as { _tag?: string })._tag !== "NotFound") throw error // first run: 404
}
const saved = await client.kv.set({ key, value: { units: "mm" } })
// saved: { key, value: { units: "mm" }, updatedAt: "2026-…" }
const { entries } = await client.kv.list({ prefix: "docs." })
await client.kv.remove({ key }) // { message: "…" } Keys are limited to 512 characters and must contain non-whitespace text; values must be JSON-compatible (no cycles, functions or non-finite numbers).