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
- Refresh the trigger list from the server before rotating; rotate only IDs that still appear.
- On 404, remove the ID from the rotation inventory (it can never come back) and continue with the rest.
- 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.
- 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
- Rebuild the rotation roster from live listings every run; never store it durably.
- On 404, retire the ID immediately - trigger IDs are never resurrected.
- Update webhook receivers with new secrets only after a 200 from rotation; 404 means old secret still applies.
- Alert when a large share of the roster 404s - it signals environment drift.
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
- Could not locate local Paperclip skills directory. Expected…
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
- Company ID is required. Pass --company-id, set…
- Environment variable
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)