SDK Reference
@canopy/sdk is a resource-typed TypeScript/JavaScript client for the
Canopy Host API. It lives at sdk/packages/sdk in the monorepo and wraps
@canopy/sdk-core’s HTTP client (retries, idempotency keys, cursor
pagination) with one class per API resource.
Related packages in the same sdk/ workspace, not covered on this page:
@canopy/sdk-core (shared client primitives, used internally),
@canopy/analytics (framework-neutral analytics), @canopy/cortex-sdk
(the equivalent client for the Cortex API, including a typed SSE
streaming helper), @canopy/features (the platform-feature manifest -
see Platform Features).
Install
npm install @canopy/sdkAuthenticate
import { createCanopySDK } from "@canopy/sdk";
const sdk = createCanopySDK({ apiKey: process.env.CANOPY_API_KEY, // or: token: "..."});There’s no env-var auto-detection - baseUrl (default
https://api.canopy.pm) and apiKey/token are always passed
explicitly. Create an API key from Account → API Keys in the
dashboard, or canopy keys create <name>.
export type CanopySDKOptions = { baseUrl?: string; // default "https://api.canopy.pm" apiKey?: string; // sent as `Authorization: Bearer <apiKey>` token?: string; // alias for apiKey environmentId?: string; defaultHeaders?: Record<string, string>; debug?: boolean; timeoutMs?: number; // default 8_000 maxRetries?: number; // default 2};Non-GET requests get an auto-generated Idempotency-Key (a fresh one
per call, reused across that call’s own retries) unless you pass your
own via a call’s options.idempotencyKey. Requests retry with
exponential backoff on 429/5xx/network errors up to maxRetries.
Failures throw CanopyApiError with status, body, and retryable.
GET /projects returns 50 projects per page. sdk.projects.list() follows
the pages for you and returns every project; use listPage() to read one
page at a time:
const projects = await sdk.projects.list(); // every project
let page = await sdk.projects.listPage({ limit: 25 });while (true) { for (const project of page.projects) { // ... } if (!page.nextCursor) break; page = await sdk.projects.listPage({ limit: 25, cursor: page.nextCursor });}Most other list() calls below return an array directly; check the
resource’s own return type.
Resources
Every resource is a property on the client: sdk.<resource>.<method>().
This is the full list, grouped by area, as of @canopy/sdk 0.10.0. Each
method signature below is real - copied from the resource’s source file,
not inferred.
Core: projects, environments, deploys
await sdk.projects.list({ spaceId? }); // every project, following pagesawait sdk.projects.listPage({ limit?, cursor? }); // one page: { projects, nextCursor }await sdk.projects.get(idOrSlug); // one project; throws CanopyApiError 404 if noneawait sdk.projects.create({ name, teamId });await sdk.projects.delete(projectId);await sdk.projects.deployTemplate(projectId, templateId, name?);
await sdk.environments.get(environmentId);await sdk.environments.create(projectId, options); // repository/branch/template/etcawait sdk.environments.update(environmentId, options);await sdk.environments.delete(environmentId);await sdk.environments.nginxLogs(environmentId, "access" | "error");await sdk.environments.deployAndWait(environmentId, { pollIntervalMs?, pollTimeoutMs? });
await sdk.builds.list(environmentId, page?);await sdk.builds.get(environmentId, buildId);await sdk.builds.create(environmentId, { idempotencyKey? });await sdk.builds.cancel(environmentId, buildId);await sdk.builds.rollback(environmentId, buildId);await sdk.builds.trace(environmentId, buildId); // {traceId, signozUrl}await sdk.builds.unlock(environmentId, force?);await sdk.builds.deployApproval(environmentId, buildId, "approve" | "reject");await sdk.builds.streamLogs(environmentId, buildId, onEvent); // raw SSE
await sdk.variables.list(environmentId);await sdk.variables.set(environmentId, name, value);await sdk.variables.update(environmentId, id, name, value);await sdk.variables.delete(environmentId, id, name);await sdk.variables.setMany(environmentId, { NAME: "value", ... });
await sdk.domains.list(environmentId);await sdk.domains.create(environmentId, domain);await sdk.domains.delete(environmentId, domainId);await sdk.domains.enableSsl(environmentId);
await sdk.storage.list(environmentId);await sdk.storage.create(environmentId, hostPath, containerPath);await sdk.storage.delete(environmentId, storageMountId);
await sdk.database.get(environmentId);await sdk.database.create(environmentId, databaseType, { image?, imageVersion? });await sdk.database.delete(environmentId);await sdk.testDatabase.get(environmentId); // /create /delete - same shape, separate route
await sdk.buildSteps.list(environmentId);await sdk.buildSteps.add(environmentId, command);await sdk.buildSteps.delete(environmentId, stepId);await sdk.buildSteps.move(environmentId, stepId, "up" | "down");
await sdk.webhooks.list(environmentId);await sdk.webhooks.delete(environmentId, hookId);await sdk.webhooks.deliveries(environmentId, { limit? }); // Canopy's own inbound-delivery ledger
await sdk.templates.list();await sdk.templates.get(templateId);await sdk.templates.create({ name, slug, repository, visibility, description?, teamId? });await sdk.templates.createFromEnvironment(environmentId, { name, slug?, description?, visibility?, requiredVariables? });await sdk.templates.update(templateId, { name?, description?, buildPath?, buildCommand? });await sdk.templates.delete(templateId);await sdk.templates.resync(templateId);await sdk.templates.replaceServices(templateId, services);Teams, account, billing
await sdk.teams.list();await sdk.teams.create(name, members, { idempotencyKey? });await sdk.teams.update(teamId, { name?, description?, avatarUrl?, kind? });await sdk.teams.delete(teamId);await sdk.teams.billing(teamId);await sdk.teams.inviteMember(teamId, email, role = "developer");await sdk.teams.acceptInvite(token);await sdk.teams.revokeInvite(teamId, inviteId);await sdk.teams.updateMemberRole(teamId, userId, role);await sdk.teams.removeMember(teamId, userId);await sdk.teams.setFailureSignatureSharing(teamId, enabled); // opt-in, off by defaultawait sdk.teams.listFailureSignatures(teamId);await sdk.teams.setDebtBenchmarkSharing(teamId, enabled);
await sdk.account.whoami();await sdk.account.plan();await sdk.account.updateUsername(username);await sdk.account.totpStatus(); // .totpEnroll() .totpConfirm(code) .totpDisable()await sdk.account.changePassword(currentPassword, newPassword);
await sdk.apiKeys.list();await sdk.apiKeys.create(name);await sdk.apiKeys.revoke(id);
await sdk.billing.checkout({ plan, successUrl, cancelUrl });await sdk.billing.portal(returnUrl?);await sdk.plans.list();await sdk.repositories.list({ q?, page? });Releases, artifacts, policies
await sdk.releases.list(projectId);await sdk.releases.get(projectId, releaseId);await sdk.releases.create(projectId, { version, notes?, artifactIds });
await sdk.artifacts.list(teamId);await sdk.artifacts.get(artifactId);await sdk.artifacts.impact(teamId, { digest? , commit?, packageName? }); // exactly oneawait sdk.artifacts.lineage(artifactId);await sdk.artifacts.attestations(artifactId); // Vault-signed, not Sigstore-keylessawait sdk.artifacts.passport(artifactId); // 404 = job hasn't run yetawait sdk.artifacts.attestEvaluation(teamId, artifactId, { suiteId, suiteName, score, evaluatedAt, methodologyVersion, caseResults });
await sdk.promotions.list(environmentId);await sdk.promotions.create(environmentId, releaseId); // status stays 'pending'await sdk.promotions.approve(environmentId, promotionId);
await sdk.policies.list(teamId);await sdk.policies.get(teamId, policyId);await sdk.policies.create(teamId, { name, document, projectId?, environmentId? });await sdk.policies.updateDocument(teamId, policyId, document);await sdk.policies.enable(teamId, policyId); // .disable(...)await sdk.policies.simulate(teamId, policyId, limit?); // replays without blockingawait sdk.policies.decisions(teamId, policyId);Canopy Connect (CI)
See Canopy Connect for the full lifecycle. SDK surface:
await sdk.ci.listConnections(teamId);await sdk.ci.createConnection(teamId, { provider, repositoryHost, repositoryPath, projectId?, defaultBranch?, ingestMode? });await sdk.ci.getConnection(connectionId);await sdk.ci.updateConnection(connectionId, { defaultBranch?, ingestMode?, status? });await sdk.ci.deleteConnection(connectionId);await sdk.ci.listPipelines(connectionId);await sdk.ci.listRunsForPipeline(pipelineId);await sdk.ci.getRun(runId);await sdk.ci.listRunJobs(runId);await sdk.ci.report({ ciConnectionId, externalRunId, externalAttempt?, status?, trigger?, commitSha?, branch?, environmentId?, externalUrl? }); // idempotentawait sdk.ci.reportJobs(runId, jobs);await sdk.ci.complete(runId, status, durationMs?);await sdk.ci.reportAndWait(data, { pollIntervalMs?, timeoutMs? });
await sdk.timeline.get(environmentId, { cursor?, limit? }); // unified build+CI timelineRegistry, marketplace, MCP hosting, generations
await sdk.registryNamespaces.list(teamId);await sdk.registryNamespaces.create(teamId, scope);await sdk.registryNamespaces.requestVerification(teamId, namespaceId, domain);await sdk.registryPackages.get(scope, name);await sdk.registryPackages.stats(scope, name);await sdk.registryPackages.dailyPulls(scope, name, version, sinceDays?);
await sdk.workflows.list(teamId); // note: catalog.ts exports WorkflowsResourceawait sdk.workflows.get(teamId, workflowId);await sdk.workflows.create(teamId, { namespaceId, name, slug, description? });await sdk.workflows.publishVersion(teamId, workflowId, { version, steps });await sdk.workflows.search(teamId, query?);await sdk.workflows.listInstallations(teamId);await sdk.workflows.install(teamId, { workflowId, grantedPermissions });await sdk.workflows.uninstall(teamId, workflowId);await sdk.workflows.run(teamId, installationId);
await sdk.mcpHosting.list(teamId);await sdk.mcpHosting.create(teamId, { namespaceId, name, slug, visibility? });await sdk.mcpHosting.createVersion(teamId, serverId, { version, artifactId, manifest });await sdk.mcpHosting.publish(teamId, serverId, versionId);await sdk.mcpHosting.start(teamId, serverId); // .stop(...)await sdk.mcpHosting.setCredential(teamId, serverId, keyName, value);await sdk.mcpHosting.invoke(teamId, serverId, toolName);See MCP for what “hosted MCP server” means here - a REST tool-invocation marketplace, not a connection to Canopy’s own MCP servers.
await sdk.apiSpecs.list(projectId);await sdk.apiSpecs.create(projectId, { source: "url" | "upload" | "environment", sourceRef?, spec? });await sdk.generations.list(projectId);await sdk.generations.get(projectId, generationId);await sdk.generations.create(projectId, { target: "sdk-typescript" | "mcp-python" | "docs", specSource, spec?, sourceRef? });await sdk.mcpCatalog.list(teamId); // { generated, hosted }Spaces, engineering graph (preview)
Both are lifecycle: 'preview' per the Platform Features registry - stable enough to build against, but the shape may still change.
await sdk.spaces.list();await sdk.spaces.create(name, members?);await sdk.spaces.update(spaceId, { name?, description?, avatarUrl?, kind? });await sdk.spaces.search(spaceId, query, kinds?);await sdk.spaces.overview(spaceId);await sdk.spaces.ai(spaceId, messages); // 503 if not configured
// or, pre-bound to one space:const space = sdk.space(spaceId);await space.projects.list();await space.search(query, kinds?);await space.overview();await space.ai(messages);
await sdk.graph.node(ref); // ref like "graph:environment:<id>"await sdk.graph.edges(ref, depth?); // depth clamped server-side to 0-3await sdk.graph.commit(sha);See Spaces for what’s actually built today.
Cost, budgets, compliance, residency, incidents, recommendations, maintenance
await sdk.costs.summary(teamId, { since?, until? });await sdk.costs.anomalies(teamId, thresholdPct?);await sdk.costs.exportJson(teamId, options); // .exportCsv(...)await sdk.costs.listCreditLedger(teamId);await sdk.costs.creditBalance(teamId);await sdk.costs.createCreditCheckout(teamId, { amountCents, successUrl, cancelUrl });
await sdk.budgets.list(teamId);await sdk.budgets.set(teamId, { id?, projectId?, environmentId?, monthlyLimit, currency?, enabled? });await sdk.budgets.delete(teamId, budgetId);
// PREVIEW (unstable) - threshold alerts over a fixed metric vocabulary:// open_incidents_count, budget_percent_used, deploy_lag_minutes,// failed_ci_runs_on_default_branch.await sdk.alerts.list(teamId);await sdk.alerts.create(teamId, { name, metric, threshold, channel, webhookUrl? });await sdk.alerts.delete(teamId, ruleId);await sdk.alerts.events(teamId);
await sdk.compliance.list(teamId);await sdk.compliance.assemble(teamId, periodStart, periodEnd); // async jobawait sdk.compliance.get(teamId, packageId);await sdk.compliance.markLegalReviewed(teamId, packageId);await sdk.compliance.export(teamId, packageId); // 403 until legal-reviewed
await sdk.residency.listForEnvironment(environmentId);await sdk.residency.attestEnvironment(environmentId);await sdk.residency.listForArtifact(artifactId);await sdk.residency.attestArtifact(artifactId);
await sdk.incidents.list(environmentId);await sdk.incidents.get(environmentId, incidentId);await sdk.incidents.mitigate(environmentId, incidentId); // .resolve(...) .dismiss(...)
await sdk.recommendations.list(environmentId);await sdk.recommendations.inbox(teamId);await sdk.recommendations.apply(environmentId, recommendationId); // .dismiss(...) .revert(...)
await sdk.maintenance.listTasks(teamId, repositoryId);await sdk.maintenance.updateSettings(teamId, repositoryId, category, { enabled?, monthlyBudgetCents? });await sdk.maintenance.triggerRun(teamId, repositoryId, { category, proposal, estimatedCostCents, agentIdentityId? });Several of these back currently-disabled platform features
(host.compliance.residency_chain, host.automation.maintenance,
host.incidents) - calling them today against production will succeed
at the HTTP layer but the underlying capability may be inert or return
empty data. Check Platform Features before
building against one of these.
Agents, SSO/SCIM, audit, notifications
await sdk.agents.list(teamId);await sdk.agents.create(teamId, { name, kind, permissions });await sdk.agents.setEnabled(teamId, agentId, enabled);await sdk.agents.issueKey(teamId, agentId, { expiresInMs? }); // plaintext key shown onceawait sdk.agents.issueApprovalToken(teamId, agentId, { action, resourceId });await sdk.agents.runs(teamId, agentId);
await sdk.sso.get(teamId); // null if not configuredawait sdk.sso.upsert(teamId, { idp_entity_id, idp_sso_url, idp_certificate, ... });await sdk.sso.setEnabled(teamId, enabled);await sdk.sso.rotateScimToken(teamId); // plaintext, onceawait sdk.sso.revokeScimToken(teamId);
await sdk.audit.list(teamId);await sdk.audit.verify(teamId); // verifies the whole hash-chained ledgerawait sdk.audit.export(teamId, onLine);
await sdk.notifications.list({ limit?, offset? });await sdk.notifications.markRead(id);await sdk.notifications.markAllRead();Platform Features
await sdk.platformFeatures.resolved(); // public - GET /platform-features/resolvedawait sdk.platformFeatures.list(); // staff-onlyawait sdk.platformFeatures.history(key); // staff-onlyawait sdk.platformFeatures.setState(key, { state, reason }); // staff-only, reason requiredSee Platform Features for the full mechanism.
Scope notes
- Staff-only admin routes with little SDK-consumer value (dokku-hosts,
host-scripts, network-selftest) are deliberately not wrapped - use
the CLI’s
admincommands for those. - Some resources -
spaces,graph, and anything backing a currently-disabled feature - are genuinely less stable than the rest. Treat them as preview APIs, not a guaranteed-stable contract.