Skip to content

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/sdk

Authenticate

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 pages
await sdk.projects.listPage({ limit?, cursor? }); // one page: { projects, nextCursor }
await sdk.projects.get(idOrSlug); // one project; throws CanopyApiError 404 if none
await 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/etc
await 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 default
await 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 one
await sdk.artifacts.lineage(artifactId);
await sdk.artifacts.attestations(artifactId); // Vault-signed, not Sigstore-keyless
await sdk.artifacts.passport(artifactId); // 404 = job hasn't run yet
await 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 blocking
await 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? }); // idempotent
await 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 timeline

Registry, 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 WorkflowsResource
await 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-3
await 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 job
await 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 once
await sdk.agents.issueApprovalToken(teamId, agentId, { action, resourceId });
await sdk.agents.runs(teamId, agentId);
await sdk.sso.get(teamId); // null if not configured
await sdk.sso.upsert(teamId, { idp_entity_id, idp_sso_url, idp_certificate, ... });
await sdk.sso.setEnabled(teamId, enabled);
await sdk.sso.rotateScimToken(teamId); // plaintext, once
await sdk.sso.revokeScimToken(teamId);
await sdk.audit.list(teamId);
await sdk.audit.verify(teamId); // verifies the whole hash-chained ledger
await 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/resolved
await sdk.platformFeatures.list(); // staff-only
await sdk.platformFeatures.history(key); // staff-only
await sdk.platformFeatures.setState(key, { state, reason }); // staff-only, reason required

See 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 admin commands 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.