What you will learn:
The one result contract every method shares
Which namespaces and methods exist
How pagination and async workflows work
Related resources
Source code
@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/sdkimport { 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:
| Option | Type | Default | Purpose |
|---|---|---|---|
apiKey | string | () => string | Promise<string> | required | Bearer credential. A function is called per request, for short-lived tokens |
throwOnError | boolean | false | Throw a OptiTechError instead of returning { data, error }. Overridable per call |
waitForReadiness | boolean | false | Poll provisioning operations to completion before resolving. Overridable per call |
wait | { pollIntervalMs?, timeoutMs? } | 1000 / 300000 | Tuning for the readiness poller |
retries | number | 2 | Automatic retries on safe statuses (423, 429, 503) |
baseUrl | string | https://api.optitech.com/v1 | Override the API base URL |
fetch | typeof fetch | global fetch | Custom fetch, for proxies, tests, or non-global runtimes |
orgId | string | none | Default 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 ProjectTo 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 callTyped errors
The error channel, and what throwOnError throws, is one hierarchy of Error subclasses, discriminated on kind:
| kind | Class | Raised when |
|---|---|---|
api | OptiTechApiError | Non-2xx response; carries status, code, requestId, body |
not_found | OptiTechNotFoundError | 404 (extends OptiTechApiError) |
auth | OptiTechAuthError | 401 or 403 |
rate_limit | OptiTechRateLimitError | 429, after retries |
operation | OptiTechOperationError | An awaited operation failed; carries operationId, status |
timeout | OptiTechTimeoutError | A readiness or wait deadline was exceeded |
network | OptiTechNetworkError | Transport failure, no response received |
client | OptiTechError | SDK-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 finishedFor 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
| Method | Returns | Arguments |
|---|---|---|
list(query?) | Paginated<ProjectListItem> | query: { search?, org_id?, limit? } |
get(id) | Project | |
create(input?) | Project | input: { 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) | Project | input: { name?, settings? } |
delete(id) | Project | |
recover(id) | Project | Recover a soft-deleted project within its retention window |
transfer(input) | void | input: { fromOrgId?, toOrgId, projectIds } (fromOrgId defaults to the client orgId) |
transferFromUser(input) | void | input: { 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.
| Method | Returns |
|---|---|
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
| Method | Returns | Arguments |
|---|---|---|
list(projectId, query?) | Paginated<Branch> | query: { search?, sort_by?, sort_order?, include_deleted? } |
get(projectId, branchId) | Branch | |
create(projectId, input?) | Branch | input: { 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) | Branch | input: { name?, protected?, expires_at? } |
delete(projectId, branchId) | void | |
getDefault(projectId) | Branch | Resolve the project's default branch by flag, not by name |
setDefault(projectId, branchId) | Branch | |
recover(projectId, branchId) | Branch | Recover a soft-deleted branch within the 7-day window |
finalizeRestore(projectId, branchId, input?) | void | Commit 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
| Method | Returns | Arguments |
|---|---|---|
connectionString(params) | string | params: { 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.
| Method | Returns | Arguments |
|---|---|---|
list(projectId) | Endpoint[] | |
listByBranch(projectId, branchId) | Endpoint[] | |
get(projectId, endpointId) | Endpoint | |
create(projectId, input) | Endpoint | input: { 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.
| Method | Returns | Arguments |
|---|---|---|
list(projectId, branchId) | Role[] | |
get(projectId, branchId, name) | Role | |
create(projectId, branchId, input) | Role | input: { name, no_login? } |
delete(projectId, branchId, name) | void | |
password(projectId, branchId, name) | string | Reveals the current password |
resetPassword(projectId, branchId, name) | Role | The 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 secretoptitech.postgres.databases
Databases, scoped to a branch.
| Method | Returns | Arguments |
|---|---|---|
list(projectId, branchId) | Database[] | |
get(projectId, branchId, name) | Database | |
create(projectId, branchId, input) | Database | input: { name, owner_name } |
update(projectId, branchId, name, input) | Database | input: { name?, owner_name? } |
delete(projectId, branchId, name) | void |
optitech.postgres.dataApi
The OptiTech Data API, scoped to a branch and database.
| Method | Returns |
|---|---|
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
| Method | Returns |
|---|---|
get(projectId, branchId) | BranchStorage |
optitech.storage.buckets
| Method | Returns | Arguments |
|---|---|---|
list(projectId, branchId) | Bucket[] | |
create(projectId, branchId, input) | Bucket | input: { name, access_level? }, where access_level is "private" | "public_read" |
delete(projectId, branchId, bucketName) | void |
optitech.storage.objects
| Method | Returns | Arguments |
|---|---|---|
list(projectId, branchId, bucketName, query?) | BucketObjectsListResponse | query: { prefix?, delimiter?, cursor?, limit? }. Returns one page of folders, objects, next_cursor |
get(projectId, branchId, bucketName, objectKey) | Blob | Raw 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) | PresignResponse | input: { 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
| Method | Returns | Arguments |
|---|---|---|
list(projectId, branchId, query?) | Paginated<OptiTechFunction> | query: { limit? } |
get(projectId, branchId, slug) | OptiTechFunction | |
update(projectId, branchId, slug, input) | OptiTechFunction | input: { name? } |
delete(projectId, branchId, slug) | void | |
deploy(projectId, branchId, slug, input?) | OptiTechFunctionDeployment | Multipart. 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
| Method | Returns | Arguments |
|---|---|---|
list(projectId, branchId) | CredentialMeta[] | |
create(projectId, branchId, input) | CreateCredentialResponse | input: { 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
| Method | Returns | Arguments |
|---|---|---|
get(projectId, branchId) | BranchAiGateway | Returns 404 when AI Gateway is not enabled on the branch |
optitech.snapshots
Point-in-time snapshots, restore, and backup schedules. REST: Snapshots API
| Method | Returns | Arguments |
|---|---|---|
list(projectId) | Snapshot[] | |
create(projectId, branchId, input?) | Snapshot | input: { name?, timestamp?, lsn?, expiresAt? } |
update(projectId, snapshotId, input) | Snapshot | input: { name? } |
delete(projectId, snapshotId) | void | |
restore(projectId, snapshotId, input?) | Branch | input: { 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 returnstrueor aborts (deletes the preview branch) iffalse, unlesskeepOnAbortis 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
| Method | Returns | Arguments |
|---|---|---|
list(projectId) | Paginated<Operation> | |
get(projectId, operationId) | Operation | |
waitFor(operations, options?) | void | options: { 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
| Method | Returns | Arguments |
|---|---|---|
get(projectId, branchId) | OptiTechAuthIntegration | |
create(projectId, branchId, input) | OptiTechAuthCreateIntegrationResponse | Enable the integration |
disable(projectId, branchId, input?) | void | input: { deleteData? } |
updateConfig(projectId, branchId, input) | OptiTechAuthConfigResponse |
optitech.auth.oauthProviders
OAuth providers (Google, GitHub, and others).
| Method | Returns |
|---|---|
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.
| Method | Returns |
|---|---|
list(projectId, branchId) | OptiTechAuthRedirectUriWhitelistDomain[] |
add(projectId, branchId, input) | void |
delete(projectId, branchId, input) | void |
optitech.auth.users
| Method | Returns |
|---|---|
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
| Method | Returns |
|---|---|
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
| Method | Returns | Arguments |
|---|---|---|
list() | ApiKeysListResponseItem[] | |
create(keyName) | ApiKeyCreateResponse | The key token is shown once |
revoke(keyId) | ApiKeyRevokeResponse |
optitech.regions / optitech.user
Active regions and the current account. REST: Regions, Users
| Method | Returns |
|---|---|
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.