1. Docs
  2. Integrate with an AI Assistant

Integrate with an AI Assistant

Integrating Canopy with an AI coding assistant? Point it at the machine-readable surfaces below, and settle the one decision the endpoint catalog cannot make for it before it writes sign-in code.

Four decisions before any code

The endpoint catalog cannot make these for you, and each one changes what the code below looks like. Settle them with the person you are working for, then write to them.

Who renders the sign-in screen. Your own form calling the Direct API with a publishable key, or Canopy's Hosted Login, where your app is an OAuth client. A Hosted Login token carries aud, your client_id, and the verifier must be told to check it; a Direct API token carries none.Where the refresh token lives. Same site, a custom auth domain, a backend proxy, or a native key. The session modes below decide it; nothing fails at build time if you get it wrong, only in a real browser on the production domain.Flat, hierarchy or organizations. Every check names a node. A flat Environment checks at the root or app_wide; a hierarchy checks at the node the record belongs to; organizations check at the org_id on the verified token.The SDK guard or the evaluate API. CanopyGuard and LocalAuthorizer answer in-process from cached grants, which is what every request should use; permissions.evaluate is for the one decision that must have no staleness. See Authorization Caching.

Pick a session mode

Provisioning an Environment over the API (the API Walkthrough) is a server-to-server job and needs no decision here. Signing your end users in does, and it is the one part that cannot be inferred from the endpoint catalog, because every mode calls the same endpoints:

Custom auth domain: recommended for a browser app on its own domain. Point a subdomain you control (auth.yourapp.com) at Canopy and call it instead of the platform origin. The refresh token's cookie is then first-party on your own site, so silent refresh works in every browser and you write no backend auth code. Set it up in the Console under Access Control → Integrations → Custom Auth Domain. See Custom auth domain.Backend proxy (BFF): when DNS is out of your hands, or when you already route API traffic through your own backend. Your backend holds the session. See Backend Proxy (BFF).Native mobile: an installed iOS or Android app has no cookie jar, so register a publishable key marked native. The refresh token then comes back in the response body, and the app keeps it in the Keychain or Keystore. See Native & Mobile Apps.Same-site: your app and the API origin are already the same site; nothing to do.
Decide this before you write sign-in code

This is worth deciding before you write sign-in code rather than after. Nothing here fails at build time: curl and a same-origin dev server behave perfectly. A third-party refresh cookie only breaks in a real browser on your production domain, where the user is signed out as soon as the access token lapses.

Machine-readable surfaces

Point your agent at these. Everything the docs describe is also published in formats an agent can fetch directly, with no browser and no rendering:

/llms.txt: an index of every docs page and machine-readable resource on this site, in the llms.txt format./llms-full.txt: the same index plus the full endpoint catalog, authentication and response-envelope reference, and every webhook event type, in one fetch.Every docs page as markdown, at its own URL plus .md (this page is /docs/ai-integration.md): the same text the docs MCP server's get_guide answers with, far cheaper to read than the rendered page. /llms.txt names each page's markdown URL beside it./openapi: the OpenAPI 3 documents behind this reference, covering the public API, portal & identity auth, OAuth/OIDC, and SCIM. Point a code generator at these to get a typed client./sitemap.xml: every page on the site, including one URL per documented API operation./docs/dashboard-navigation: every page of the Developer Console and the Admin Workspace with its menu path, the capability that opens it, and a link to its walkthrough where one exists. Answer where-do-I-click questions from it and the guides it links rather than from memory; each guide names the exact controls and why one may be missing./ai/ui-map.json: every left-nav page of both signed-in applications as JSON, with its menu path, the capability that opens it, what must be selected first, and a link to its guide./mcp: an MCP server over Streamable HTTP, no authentication, with find_surface, get_guide, explain_capability, list_surfaces, and search_api. Connect an MCP client to it to answer dashboard questions from the guides rather than from memory. Connecting a client covers Claude Code, Claude Desktop, and Cursor.The API's own MCP server, at /mcp/v1 on your API origin: one tool per public API operation, named by its operation id, plus whoami. Add it by URL and the client signs you in through your browser, acting as you in the one Environment you approve; a client that cannot open a browser sends an API key in X-API-Key instead. Every call runs as that caller, so a tool can do exactly what that approval or key allows and nothing more. Connect an MCP Client covers both.
Don't guess documentation URLs

Endpoint pages live at /docs/api-reference/{operationId}, for example /docs/api-reference/ApiPermissionsController_evaluate. The ids come from the OpenAPI documents; resource-name URLs like /docs/api-reference/permissions do not exist.

The code, as the skill tests it

Each block is quoted from the Canopy agent skill, where it is type-checked against the published packages on every release. Copy it rather than writing an equivalent; the status mapping in it is the part assistants get wrong on their own.

Verify a token

One verifier per process, named for the Environment, with audience set for Hosted Login and left out for the Direct API. Never decode a token.

NestJSNode
import { Module } from "@nestjs/common";
import { CanopyModule } from "@canopy-io/nestjs";

interface AuthedRequest {
  canopyToken?: { sub: string };
  params?: Record<string, string>;
}

@Module({
  imports: [
    CanopyModule.forRoot<AuthedRequest>({
      apiKey: process.env.CANOPY_API_KEY,
      verify: {
        environmentId: process.env.CANOPY_ENVIRONMENT_ID ?? "",
        // Hosted Login: your OAuth client id. Direct API: leave this out.
        audience: process.env.CANOPY_OAUTH_CLIENT_ID,
      },
      resolveIdentity: (request) => request.canopyToken?.sub,
      resolveNode: (request) => request.params?.["nodeId"],
    }),
  ],
})
export class AppModule {}
Guard a route

Authenticate, then ask whether the identity holds the permission at the node the route names. A denial is 403, an unreachable Canopy is 503, a broken integration is 500; never collapse them.

NestJSNode
import { Controller, Get, Post, UseGuards } from "@nestjs/common";
import {
  CanopyGuard,
  CanopyTokenGuard,
  RequirePermission,
} from "@canopy-io/nestjs";

@Controller("nodes/:nodeId/reports")
@UseGuards(CanopyTokenGuard, CanopyGuard)
export class ReportsController {
  // Does the identity hold reports.view at :nodeId, or above it?
  @Get()
  @RequirePermission("reports.view")
  list() {
    return [];
  }

  @Post(":reportId/publish")
  @RequirePermission("reports.publish")
  publish() {
    return { published: true };
  }

  // Anywhere in the Environment: for showing a menu item, never for a
  // resource that belongs to a node.
  @Get("menu")
  @RequirePermission("reports.view", { scope: "app_wide" })
  menu() {
    return { showReports: true };
  }
}
Handle a webhook

Verify the signature over the raw bytes, then hand the delivery to the authorizer so a revocation reaches the next check without waiting out the cache. The same handler serves NestJS on a route that keeps the raw body.

import express from "express";
import {
  isWebhookEvent,
  LocalAuthorizer,
  verifyWebhookSignature,
  type WebhookDelivery,
} from "@canopy-io/node";

declare const app: express.Express;
declare const authorizer: LocalAuthorizer;
declare const webhookSecret: string;

app.post(
  "/webhooks/canopy",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const valid = await verifyWebhookSignature({
      headers: req.headers,
      rawBody: req.body as Buffer,
      secret: webhookSecret,
    });

    if (!valid) {
      res.sendStatus(401);

      return;
    }

    const delivery = JSON.parse(
      (req.body as Buffer).toString(),
    ) as WebhookDelivery;

    authorizer.applyEvent(delivery);

    if (isWebhookEvent(delivery, "organization.member.added")) {
      const { identity_id, role_id } = delivery.data.metadata;

      console.log(identity_id, role_id);
    }

    res.sendStatus(204);
  },
);

Never

The mistakes the skill tells an assistant to look for, in its words. Each one has a page in the skill's mistakes.md.

Decoding a token instead of verifying itThe wrong audience for the sign-in shapeVerifying without naming the issuerCalling evaluate on every requestTreating "could not decide" as "denied", or as "allowed"A fresh verifier or authorizer per requestapp_wide on a resource that belongs to a nodeChecking a node other than the one the action writes toA node or organization taken from the callerA secret in the browserMishandling the refresh tokenWebhooks verified over re-serialized JSONA scoped API key without the read scopes

Where to go next

The Canopy agent skill, released with the SDKs. In Claude Code: /plugin marketplace add canopy-identity/canopy-javascript, then /plugin install canopy@canopy-javascript. Other assistants read SKILL.md directly.Both MCP servers. /mcp on this site answers questions from these docs with no sign-in; /mcp/v1 on your API origin exposes every public API operation as a tool. Connect an MCP Client.The whole reference as text: /llms-full.txt, with every endpoint, the authentication surfaces, the response envelope and the webhook event types in one fetch.
Environment
API version
v1.0
On this page Was this page helpful?

Tell us how we can improve this guide.