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
- Inspect manifestJson.apiRoutes[].companyResolution for the route to learn where companyId must come from.
- Include the required value: companyId in the JSON body or query string, or use an issue-scoped path param whose issue actually exists.
- 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
- Read the route's companyResolution from the manifest and always supply the declared source field.
- When testing board-facing flows, verify the route declares body/query resolution before assuming board access works.
- For issue-scoped routes, confirm the issue still exists before dispatching (it carries the company).
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
- Plugin is not ready (current status: ${plugin.status})
- Plugin worker is not running
- Plugin does not expose scoped API routes
- Plugin API route not found
- Plugin API routes accept JSON requests only
AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18).
Data as JSON: /api/errors/84fdde3de98f5af7.
Report an issue: GitHub.