paperclipai/paperclip · error

Unable to resolve company for plugin API route

Error message

Unable to resolve company for plugin API route

What it means

Returned as HTTP 400 when resolveScopedApiCompanyId cannot determine which company the scoped API call belongs to. The company is derived from the route's companyResolution declaration (from "body", "query", or a path param resolved through an issue lookup), or — when the route declares no companyResolution — from the authenticated agent actor's companyId. A board actor calling a route with no resolution always fails here, since board context has no single company.

Source

Thrown at server/src/routes/plugins.ts:1875

      res.status(404).json({ error: "Plugin does not expose scoped API routes" });
      return;
    }

    const requestPath = req.path || "/";
    const routes = plugin.manifestJson.apiRoutes ?? [];
    const match = routes
      .map((route) => ({ route, params: matchScopedApiRoute(route, req.method, requestPath) }))
      .find((candidate) => candidate.params !== null);
    if (!match || !match.params) {
      res.status(404).json({ error: "Plugin API route not found" });
      return;
    }

    try {
      assertScopedApiAuth(req, match.route);
      const companyId = await resolveScopedApiCompanyId(match.route, match.params, req);
      if (!companyId) {
        res.status(400).json({ error: "Unable to resolve company for plugin API route" });
        return;
      }
      assertCompanyAccess(req, companyId);
      await enforceScopedApiCheckout(req, match.route, match.params, companyId);
      if (req.method !== "GET" && req.headers["content-type"] && !req.is("application/json")) {
        res.status(415).json({ error: "Plugin API routes accept JSON requests only" });
        return;
      }
      const requestBody = req.body ?? null;
      const bodySize = Buffer.byteLength(JSON.stringify(requestBody));
      if (bodySize > PLUGIN_API_BODY_LIMIT_BYTES) {
        res.status(413).json({ error: "Plugin API request body is too large" });
        return;
      }

      const actor = getActorInfo(req);
      const input: PluginScopedApiRequest = {
        routeKey: match.route.routeKey,

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Inspect manifestJson.apiRoutes[].companyResolution for the route to learn where companyId must come from.
  2. Include the required value: companyId in the JSON body or query string, or use an issue-scoped path param whose issue actually exists.
  3. For board actors on routes without a companyResolution there is no fallback — either call with an agent API key or extend the plugin manifest to declare body/query resolution.

Example fix

// before — route declares companyResolution { from: "body", key: "companyId" }
await fetch(`/api/plugins/${id}/api/issues`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "New issue" }),
});

// after
await fetch(`/api/plugins/${id}/api/issues`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ companyId, title: "New issue" }),
});
Defensive patterns

Strategy: validation

Validate before calling

function buildScopedApiBody(
  route: { companyResolution?: { from: string; key?: string } | null },
  body: Record<string, unknown>,
  agentCompanyId: string | undefined,
): Record<string, unknown> {
  const res = route.companyResolution;
  if (!res) {
    if (!agentCompanyId) throw new Error("Route has no companyResolution; caller must be an agent with a companyId");
    return body;
  }
  if (res.from === "body" && res.key && typeof body[res.key] !== "string") {
    throw new Error(`Missing '${res.key}' in body for company resolution`);
  }
  return body;
}

Type guard

interface CompanyResolution { from: "body" | "query" | "issue"; key?: string; param?: string }
function hasCompanyResolution(route: { companyResolution?: CompanyResolution | null }): boolean {
  return route.companyResolution != null;
}

Prevention

When it happens

Trigger: Route without companyResolution called by a board user; companyResolution {from:"body"} but the JSON body lacks the key (e.g. companyId) or it is not a string; {from:"query"} without the query param; path-param resolution where the issueId does not exist or belongs to a deleted issue (issue?.companyId is null).

Common situations: Operator testing an agent-oriented route from the board UI without passing companyId; client dropping the companyId field during a refactor; the target issue deleted between page load and the API call; agent token missing its companyId claim.

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18). Data as JSON: /api/errors/84fdde3de98f5af7. Report an issue: GitHub.