What you will learn:

  • The one result contract every method shares

  • Which namespaces and methods exist

  • How pagination and async workflows work

@optitech/sdk wraps the entire OptiTech API in one typed, fetch-based client. You authenticate once, then reach every resource through a namespace on optitech.*: programs, frameworks, controls and evidence, documents, vendors, and reports. Retries, readiness polling, auto-pagination, and typed errors are built in.

It replaces @optitech/api-client, the deprecated Axios-based SDK. New integrations should use @optitech/sdk. See the migration guide for method mapping and error-handling changes.

Not every endpoint has an ergonomic wrapper

createOptiTechClient namespaces cover common workflows (programs, frameworks, controls, evidence, snapshots, and more). They do not wrap every Platform API operation. For endpoints without a namespace method, use the raw layer below or the OptiTech API Reference.

npm install @optitech/sdk
import { createOptiTechClient } from "@optitech/sdk";

const optitech = createOptiTechClient({ apiKey: process.env.OPTITECH_API_KEY! });

const { data, error } = await optitech.projects.list().all();
if (error) throw error; // typed OptiTechError
data; // ProjectListItem[]

Every method follows this shape: select a namespace, call a method, and receive a { data, error } result. The reference below documents each namespace and method against that single contract.

In the reference tables, the Returns column names the resolved resource, the type of data on success (or the value returned directly when throwOnError is set). A method resolving to void has no resource body; Paginated<T> is the lazy, auto-paginated list described below. Every method also accepts an optional trailing options argument ({ throwOnError?, waitForReadiness?, signal? }), omitted from the tables for brevity.

Nearly every method needs a projectId, and branch-scoped methods also need a branchId. Get these from optitech.projects.list() and optitech.branches.list(projectId) (or optitech.branches.getDefault(projectId) for the default branch), reading .id off each result.

Client configuration

createOptiTechClient(config) accepts:

OptionTypeDefaultPurpose
apiKeystring | () => string | Promise<string>requiredBearer credential. A function is called per request, for short-lived tokens
throwOnErrorbooleanfalseThrow a OptiTechError instead of returning { data, error }. Overridable per call
waitForReadinessbooleanfalsePoll provisioning operations to completion before resolving. Overridable per call
wait{ pollIntervalMs?, timeoutMs? }1000 / 300000Tuning for the readiness poller
retriesnumber2Automatic retries on safe statuses (423, 429, 503)
baseUrlstringhttps://api.optitech.com/v1Override the API base URL
fetchtypeof fetchglobal fetchCustom fetch, for proxies, tests, or non-global runtimes
orgIdstringnoneDefault organization id, applied to project create/list and as the transfer source org. Overridable per call
const optitech = createOptiTechClient({
  apiKey: process.env.OPTITECH_API_KEY!,
  orgId: "org-cool-forest-12345678",
  throwOnError: true,
});

Core model

Four behaviors are shared by every method: the result envelope, typed errors, pagination, and async workflows.

The result envelope

By default, no try/catch. Each call resolves to a discriminated { data, error } envelope; check error, then data is narrowed:

const { data, error } = await optitech.projects.get("late-frost-12345");
if (error) return; // error: typed OptiTechError union
data; // narrowed to Project

To throw instead, set throwOnError on the client (or per call). The return type narrows to the bare resource:

const optitech = createOptiTechClient({ apiKey, throwOnError: true });
const project = await optitech.projects.get("my-project"); // Project (throws on error)
const { data } = await optitech.projects.get("my-project", { throwOnError: false }); // opt out per call

Typed errors

The error channel, and what throwOnError throws, is one hierarchy of Error subclasses, discriminated on kind:

kindClassRaised when
apiOptiTechApiErrorNon-2xx response; carries status, code, requestId, body
not_foundOptiTechNotFoundError404 (extends OptiTechApiError)
authOptiTechAuthError401 or 403
rate_limitOptiTechRateLimitError429, after retries
operationOptiTechOperationErrorAn awaited operation failed; carries operationId, status
timeoutOptiTechTimeoutErrorA readiness or wait deadline was exceeded
networkOptiTechNetworkErrorTransport failure, no response received
clientOptiTechErrorSDK-side error, such as ambiguous connection-string selection
const { error } = await optitech.branches.get(projectId, "nope");
if (error?.kind === "not_found") {
  // handle the 404
}

Lazy, auto-paginated lists

Methods labeled Paginated return a Paginated<T>; the cursor is managed for you:

const { data: all } = await optitech.projects.list().all(); // every page
const { data: one } = await optitech.projects.list().page(); // just the first page
for await (const project of optitech.projects.list()) {
  // stream item by item
}

Async workflows

OptiTech mutations return operations that complete in the background. A few convenience methods, noted as "creates, then polls until ready" in the reference below (projects.createAndConnect, branches.createWithCompute), do this polling for you and hand back a ready-to-use result, such as a connection string, in a single call. The primitive underneath is optitech.operations.waitFor(operations).

On any namespaced mutation, pass { waitForReadiness: true } as the trailing options argument to poll before the call resolves:

const { data, error } = await optitech.branches.create(
  projectId,
  { name: "preview" },
  { waitForReadiness: true }
);
if (error) throw error;
data; // Branch — provisioning finished

For raw API calls that return an operations array, use optitech.operations.waitFor instead.

Namespaces

The client groups the API into resource namespaces. Projects and branches are the core surfaces: projects create, manage, and share projects, and branches branch a project's data and schema. The Postgres data plane lives under postgres: compute endpoints, roles, databases, the Data API, and connection strings.

Branch-scoped platform services include storage (S3-compatible object storage), functions, credentials, aiGateway, and auth (Managed Better Auth, OAuth providers, and users). For data lifecycle and async work, use snapshots for point-in-time snapshots and restore, and operations to poll asynchronous operations.

Account-level surfaces round out the client: consumption for billing metrics, apiKeys, and regions / user.

optitech.projects

Create, manage, and share OptiTech projects. One API call per method; list is paginated. REST: Projects API

MethodReturnsArguments
list(query?)Paginated<ProjectListItem>query: { search?, org_id?, limit? }
get(id)Project
create(input?)Projectinput: { name?, region_id?, pg_version?, org_id?, autoscaling_limit_min_cu?, autoscaling_limit_max_cu?, settings? }
createAndConnect(input?, opts?){ project: Project, connectionString: string }Creates, then polls until ready. opts: { pooled? } (default true)
update(id, input)Projectinput: { name?, settings? }
delete(id)Project
recover(id)ProjectRecover a soft-deleted project within its retention window
transfer(input)voidinput: { fromOrgId?, toOrgId, projectIds } (fromOrgId defaults to the client orgId)
transferFromUser(input)voidinput: { toOrgId, projectIds }
// Provision a project, poll until ready, return a pooled connection string
const { data } = await optitech.projects.createAndConnect(
  { name: "tenant-42", region_id: "aws-us-east-1" },
  { pooled: true }
);
// data: { project, connectionString }

optitech.projects.permissions

Share a project with additional users by email.

MethodReturns
list(projectId)ProjectPermission[]
grant(projectId, email)ProjectPermission
revoke(projectId, permissionId)ProjectPermission

optitech.branches

Branch a project's data and schema; optionally attach compute in one workflow. REST: Branches API

MethodReturnsArguments
list(projectId, query?)Paginated<Branch>query: { search?, sort_by?, sort_order?, include_deleted? }
get(projectId, branchId)Branch
create(projectId, input?)Branchinput: { name?, parent_id?, parent_lsn?, parent_timestamp?, protected? }
createWithCompute(projectId, input, opts?){ branch: Branch, endpoint: Endpoint, connectionString: string }Creates, then polls until ready. input: { name?, parentId?, compute?: { minCu?, maxCu?, suspendTimeoutSeconds? } }
update(projectId, branchId, input)Branchinput: { name?, protected?, expires_at? }
delete(projectId, branchId)void
getDefault(projectId)BranchResolve the project's default branch by flag, not by name
setDefault(projectId, branchId)Branch
recover(projectId, branchId)BranchRecover a soft-deleted branch within the 7-day window
finalizeRestore(projectId, branchId, input?)voidCommit a restore previewed with snapshots.restore({ finalize: false })
// Branch off the default ("production") branch with its own compute
const { data: prod } = await optitech.branches.getDefault(projectId);
const { data } = await optitech.branches.createWithCompute(projectId, {
  name: "preview/pr-123",
  parentId: prod?.id,
  compute: { minCu: 0.25, maxCu: 2 },
});
// data: { branch, endpoint, connectionString }

optitech.postgres

The Postgres data plane of a branch: compute endpoints, roles, databases, the Data API, and a connection-string helper. REST: Endpoints, Branches, Data API

MethodReturnsArguments
connectionString(params)stringparams: { projectId, branchId?, endpointId?, databaseName?, roleName?, pooled? }. Only projectId is required; branch defaults to the project default, endpoint to the read-write one, and role/database are auto-selected when the branch has exactly one. pooled defaults to true
const { data: uri } = await optitech.postgres.connectionString({ projectId });

optitech.postgres.endpoints

Compute endpoints, scoped to a project.

MethodReturnsArguments
list(projectId)Endpoint[]
listByBranch(projectId, branchId)Endpoint[]
get(projectId, endpointId)Endpoint
create(projectId, input)Endpointinput: { branch_id, type, autoscaling_limit_min_cu?, autoscaling_limit_max_cu?, suspend_timeout_seconds?, provisioner? }. type is "read_write" | "read_only"
update(projectId, endpointId, input)Endpoint
delete(projectId, endpointId)void
start(projectId, endpointId)Endpoint
suspend(projectId, endpointId)Endpoint
restart(projectId, endpointId)Endpoint

optitech.postgres.roles

Postgres roles, scoped to a branch.

MethodReturnsArguments
list(projectId, branchId)Role[]
get(projectId, branchId, name)Role
create(projectId, branchId, input)Roleinput: { name, no_login? }
delete(projectId, branchId, name)void
password(projectId, branchId, name)stringReveals the current password
resetPassword(projectId, branchId, name)RoleThe returned Role carries the new password
// Reveal a role's password, or rotate it
const { data: password } = await optitech.postgres.roles.password(projectId, branchId, "optitechdb_owner");
const { data: role } = await optitech.postgres.roles.resetPassword(projectId, branchId, "optitechdb_owner");
// role.password holds the new secret

optitech.postgres.databases

Databases, scoped to a branch.

MethodReturnsArguments
list(projectId, branchId)Database[]
get(projectId, branchId, name)Database
create(projectId, branchId, input)Databaseinput: { name, owner_name }
update(projectId, branchId, name, input)Databaseinput: { name?, owner_name? }
delete(projectId, branchId, name)void

optitech.postgres.dataApi

The OptiTech Data API, scoped to a branch and database.

MethodReturns
get(projectId, branchId, databaseName)DataApiResponse
create(projectId, branchId, databaseName, input?)DataApiCreateResponse
update(projectId, branchId, databaseName, input?)void
delete(projectId, branchId, databaseName)void

optitech.storage

Branch-scoped, S3-compatible object storage. get returns whether storage is enabled and the branch's S3 endpoint metadata; buckets and objects are nested underneath. REST: Storage, Buckets

MethodReturns
get(projectId, branchId)BranchStorage

optitech.storage.buckets

MethodReturnsArguments
list(projectId, branchId)Bucket[]
create(projectId, branchId, input)Bucketinput: { name, access_level? }, where access_level is "private" | "public_read"
delete(projectId, branchId, bucketName)void

optitech.storage.objects

MethodReturnsArguments
list(projectId, branchId, bucketName, query?)BucketObjectsListResponsequery: { prefix?, delimiter?, cursor?, limit? }. Returns one page of folders, objects, next_cursor
get(projectId, branchId, bucketName, objectKey)BlobRaw object bytes
delete(projectId, branchId, bucketName, objectKey)void
deleteByPrefix(projectId, branchId, bucketName, prefix){ deleted: number }prefix must end with /
presign(projectId, branchId, bucketName, objectKey, input)PresignResponseinput: { operation: "upload" | "download", content_type?, expires_in_seconds? }
// Upload via a presigned PUT
const { data: presign } = await optitech.storage.objects.presign(
  projectId, branchId, "avatars", "user-1.png",
  { operation: "upload", content_type: "image/png" }
);
if (!presign) throw new Error("presign failed");

await fetch(presign.url, {
  method: "PUT",
  headers: { ...presign.headers, "Content-Length": String(bytes.length) },
  body: bytes,
});

optitech.functions

Branch-scoped OptiTech Functions. REST: Functions API

MethodReturnsArguments
list(projectId, branchId, query?)Paginated<OptiTechFunction>query: { limit? }
get(projectId, branchId, slug)OptiTechFunction
update(projectId, branchId, slug, input)OptiTechFunctioninput: { name? }
delete(projectId, branchId, slug)void
deploy(projectId, branchId, slug, input?)OptiTechFunctionDeploymentMultipart. input: { zip?: Blob | File, runtime?: "nodejs24", environment?: string }, where environment is a JSON-encoded Record<string, string>
// Deploy a bundled index.mjs inside a zip (first deploy must include the zip)
const zip = await Bun.file("bundle.zip").arrayBuffer();
const { data: deployment } = await optitech.functions.deploy(projectId, branchId, "api", {
  zip: new File([zip], "bundle.zip", { type: "application/zip" }),
  runtime: "nodejs24",
});
// Poll optitech.functions.get until current_deployment.status is "completed"

optitech.credentials

Branch-scoped credentials with explicit scopes. Secrets (api_token, s3_secret_access_key) are returned once, on create. REST: Credentials API

MethodReturnsArguments
list(projectId, branchId)CredentialMeta[]
create(projectId, branchId, input)CreateCredentialResponseinput: { name?, scopes, principal_type: "user" }. Scopes: storage:read, storage:write, ai_gateway:invoke, functions:invoke
revoke(projectId, branchId, tokenId)void

optitech.aiGateway

Branch-scoped AI Gateway endpoint metadata. REST: AI Gateway API

MethodReturnsArguments
get(projectId, branchId)BranchAiGatewayReturns 404 when AI Gateway is not enabled on the branch

optitech.snapshots

Point-in-time snapshots, restore, and backup schedules. REST: Snapshots API

MethodReturnsArguments
list(projectId)Snapshot[]
create(projectId, branchId, input?)Snapshotinput: { name?, timestamp?, lsn?, expiresAt? }
update(projectId, snapshotId, input)Snapshotinput: { name? }
delete(projectId, snapshotId)void
restore(projectId, snapshotId, input?)Branchinput: { name?, targetBranchId?, finalize?, preview?, keepOnAbort? }. See below
getSchedule(projectId, branchId)BackupSchedule
setSchedule(projectId, branchId, schedule)void

restore behaves differently depending on the target:

  • As a new branch (no targetBranchId), it finalizes by default and is ready to use immediately.
  • Onto an existing branch, it does not finalize by default, so you can preview first.
  • Transaction-style with preview: it restores un-finalized, runs your callback against the restored branch, then commits if the callback returns true or aborts (deletes the preview branch) if false, unless keepOnAbort is set:
await optitech.snapshots.restore(projectId, snapshotId, {
  targetBranchId,
  preview: async (branch) => (await checks(branch)) === "ok", // true commits, false aborts
});

optitech.operations

Read operations and wait for them to finish. REST: Operations API

MethodReturnsArguments
list(projectId)Paginated<Operation>
get(projectId, operationId)Operation
waitFor(operations, options?)voidoptions: { pollIntervalMs?, timeoutMs?, signal? }
// Wait on operations from a raw call (or when readiness polling is off)
const { data } = await raw.createProjectBranch({
  client: optitech.client,
  path: { project_id: projectId },
  body: { branch: { name: "wip" } },
});
const { error } = await optitech.operations.waitFor(data!.operations, { timeoutMs: 120_000 });

optitech.auth

Branch-scoped Managed Better Auth. The legacy project-scoped endpoints are deprecated and remain raw-only. REST: Authentication API

MethodReturnsArguments
get(projectId, branchId)OptiTechAuthIntegration
create(projectId, branchId, input)OptiTechAuthCreateIntegrationResponseEnable the integration
disable(projectId, branchId, input?)voidinput: { deleteData? }
updateConfig(projectId, branchId, input)OptiTechAuthConfigResponse

optitech.auth.oauthProviders

OAuth providers (Google, GitHub, and others).

MethodReturns
list(projectId, branchId)OptiTechAuthOauthProvider[]
add(projectId, branchId, input)OptiTechAuthOauthProvider
update(projectId, branchId, providerId, input)OptiTechAuthOauthProvider
delete(projectId, branchId, providerId)void

optitech.auth.trustedDomains

The redirect-URI whitelist.

MethodReturns
list(projectId, branchId)OptiTechAuthRedirectUriWhitelistDomain[]
add(projectId, branchId, input)void
delete(projectId, branchId, input)void

optitech.auth.users

MethodReturns
create(projectId, branchId, input)OptiTechAuthCreateNewUserResponse
delete(projectId, branchId, authUserId)void
updateRole(projectId, branchId, authUserId, roles)UpdateOptiTechAuthUserRoleResponse

optitech.consumption

Cursor-paginated billing metrics. Each method takes { from, to, granularity, org_id, project_ids? }, where from/to are ISO timestamps, granularity is "hourly" | "daily" | "monthly", and org_id names the org to report on; perBranchV2 also requires project_ids. Consumption requires a Scale plan or above. REST: Consumption API

MethodReturns
perProject(query)Paginated<ConsumptionHistoryPerProject>
perProjectV2(query)Paginated<ConsumptionHistoryPerProjectV2>
perBranchV2(query)Paginated<ConsumptionHistoryPerBranchV2>
// Stream every project's daily usage across a range
for await (const project of optitech.consumption.perProject({
  from: "2026-06-01T00:00:00Z",
  to: "2026-06-30T00:00:00Z",
  granularity: "daily",
  org_id: "org-...", // the org to report on; consumption requires a Scale plan or above
})) {
  console.log(project);
}

optitech.apiKeys

Manage account-level API keys. REST: API Keys API

MethodReturnsArguments
list()ApiKeysListResponseItem[]
create(keyName)ApiKeyCreateResponseThe key token is shown once
revoke(keyId)ApiKeyRevokeResponse

optitech.regions / optitech.user

Active regions and the current account. REST: Regions, Users

MethodReturns
regions.list()RegionResponse[]
user.me()CurrentUserInfoResponse
user.organizations()Organization[]

Raw layer

Anything not wrapped above is available as a raw, 1:1 function. Pass optitech.client to reuse the client's auth and base URL:

import { raw } from "@optitech/sdk";
// or, for guaranteed tree-shaking: import { getProjectBranchSchema } from "@optitech/sdk/raw";

const { data, error } = await raw.getProjectBranchSchema({
  client: optitech.client,
  path: { project_id, branch_id },
  query: { db_name: "optitechdb" }, // db_name is required
});

The raw layer speaks the same result contract as the ergonomic client: { data, error } by default, or the bare resource (throwing the typed OptiTechError) with throwOnError: true. There is no responseStyle switch. Every request, response, and error type is re-exported flat from @optitech/sdk for import type { Project, Branch } and the rest.

How this SDK is built

The raw layer and all request, response, and error types are generated from the OptiTech OpenAPI spec using @hey-api/openapi-ts. The ergonomic namespaces documented above are hand-written on top of that generated layer. When the API adds an endpoint, it appears in the raw layer automatically; the namespace wrappers are added deliberately. The source lives in optitechdatabase/optitech-pkgs.