paperclipai/paperclip · error

Failed to create API key

Error message

Failed to create API key

What it means

HTTP 404 with body {"error":"Routine trigger not found"} from POST /api/routine-triggers/:id/rotate-secret, first guard: svc.getTrigger(req.params.id) returned null - no trigger exists with the given ID. Rotation of a webhook trigger's secret cannot proceed on a trigger that was deleted or never existed; the check runs before company scoping.

Solutions

  1. Refresh the trigger list from the server before rotating; rotate only IDs that still appear.
  2. On 404, remove the ID from the rotation inventory (it can never come back) and continue with the rest.
  3. Create a replacement trigger via POST /api/routines/:id/triggers if the deleted trigger's function is still needed, then rotate its secret at creation time.
  4. Log 404s during rotation runs and alert if a large fraction disappear (signals environment drift).

Example fix

// before
for (const id of storedTriggerIds) {
  await api.post(`/api/routine-triggers/${id}/rotate-secret`, {});
}

// after
for (const id of storedTriggerIds) {
  const res = await api.post(`/api/routine-triggers/${id}/rotate-secret`, {});
  if (res.status === 404) {
    retiredTriggerIds.add(id); // drop from inventory; trigger is gone
    continue;
  }
}
Defensive patterns

Strategy: validation

Validate before calling

async function buildRotationRoster(api: ApiClient): Promise<string[]> {
  // derive trigger IDs fresh each cycle so deleted triggers drop out automatically
  const routines = await api.list('/api/routines');
  const ids: string[] = [];
  for (const r of routines) {
    ids.push(...(r.triggers ?? []).map((t: { id: string }) => t.id));
  }
  return ids;
}

Type guard

function isApiErrorBody(body: unknown): body is { error: string } {
  return typeof body === 'object' && body !== null &&
    typeof (body as Record<string, unknown>).error === 'string';
}
const isTriggerNotFound = (b: unknown): boolean => isApiErrorBody(b) && b.error === 'Routine trigger not found';

Try / catch

for (const id of roster) {
  try {
    await api.post(`/api/routine-triggers/${id}/rotate-secret`, {});
  } catch (err) {
    if (err instanceof ApiError && err.status === 404 && isTriggerNotFound(err.body)) {
      retired.add(id); // trigger gone; nothing to rotate
      continue;
    }
    throw err;
  }
}

Prevention

When it happens

Trigger: Rotating the secret of a trigger deleted by another admin; rotating after the routine (and its triggers) were recreated from a revision, leaving the client holding the old trigger ID; a malformed ID in the rotate call.

Common situations: Key-rotation cron jobs that iterate a stored trigger inventory without refreshing it; incident response rotating all webhook secrets while a concurrent cleanup deletes unused triggers.

Related errors


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

Appendix: source

Thrown at cli/src/commands/client/agent.ts:797

        "--no-install-skills",
        "Skip installing Paperclip skills into ~/.codex/skills, ~/.claude/skills, and ~/.kimi-code/skills",
      )
      .action(async (agentRef: string, opts: AgentLocalCliOptions) => {
        try {
          const ctx = resolveCommandContext(opts, { requireCompany: true });
          const query = new URLSearchParams({ companyId: ctx.companyId ?? "" });
          const agentRow = await ctx.api.get<Agent>(
            `${apiPath`/api/agents/${agentRef}`}?${query.toString()}`,
          );
          if (!agentRow) {
            throw new Error(`Agent not found: ${agentRef}`);
          }

          const now = new Date().toISOString().replaceAll(":", "-");
          const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`;
          const key = await ctx.api.post<CreatedAgentKey>(apiPath`/api/agents/${agentRow.id}/keys`, { name: keyName });
          if (!key) {
            throw new Error("Failed to create API key");
          }

          const installSummaries: SkillsInstallSummary[] = [];
          if (opts.installSkills !== false) {
            const skillsDir = await resolvePaperclipSkillsDir(__moduleDir, [path.resolve(process.cwd(), "skills")]);
            if (!skillsDir) {
              throw new Error(
                "Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.",
              );
            }

            installSummaries.push(
              await installSkillsForTarget(skillsDir, codexSkillsHome(), "codex"),
              await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"),
              await installSkillsForTarget(skillsDir, kimiSkillsHome(), "kimi"),
            );
          }

View on GitHub (pinned to a7e689b3c3)