Base URL http://localhost:3333/api/ · Authentication · Conventions

Agent Routes

Base path: /api/agents

Health Check

GET /health

  • Authentication: None
  • Description: Check if the agent service is running
  • Response:
{
  "status": "OK"
}

Get All Agents

GET /

  • Authentication: Required
  • Description: Retrieve all agents for the authenticated user- Note: Include trailing slash (/api/agents/) for best compatibility
  • Response:
{
  "agents": [
    {
      "id": "67b9bf15-a5c7-4153-936b-5959dc83b03c",
      "name": "Content Manager",
      "description": "Main content manager",
      "status": "active",
      "icon": "data:image/png;base64,...",
      "category": "Content & Media",
      "provider": "openai",
      "model": "gpt-4o",
      "assignedTools": ["web_search"],
      "assignedWorkflows": [],
      "systemPrompt": "Persona / directives text",
      "assignedSkills": [],
      "toolAccessMode": "restricted",
      "resourceId": 1,
      "creditsUsed": 0,
      "creditLimit": 1000,
      "workflows": 0,
      "lastActive": null,
      "successRate": null
    }
  ]
}

Important: The response wraps agents in an agents array property, not a direct array. icon can be a large base64 data URL — use GET /summary for lightweight lists.

Get All Agents (Summary)

GET /summary

  • Authentication: Required
  • Description: Lightweight agent list. Drops the three biggest per-row payload contributors (base64 icon, systemPrompt, and the tools/workflows/skills arrays) and returns counts instead. Use for rendering agent cards; hit GET /:id when you need the full record.
  • Response:
{
  "agents": [
    {
      "id": "67b9bf15-a5c7-4153-936b-5959dc83b03c",
      "name": "Content Manager",
      "description": "Main content manager",
      "status": "active",
      "category": "Content & Media",
      "provider": "openai",
      "model": "gpt-4o",
      "toolCount": 3,
      "skillCount": 1,
      "workflows": 0,
      "creditsUsed": 0,
      "creditLimit": 1000,
      "lastActive": null,
      "successRate": null,
      "createdAt": "2026-01-01T00:00:00Z",
      "updatedAt": "2026-01-01T00:00:00Z"
    }
  ]
}

Save/Update Agent

POST /save

  • Authentication: Required- Description: Create a new agent or update an existing one
  • Body:
{
  "id": "optional-agent-id",
  "name": "Agent Name",
  "description": "Agent description",
  "systemPrompt": "Optional persona / directives",
  "assignedTools": ["web_search", "web_scrape"],
  "assignedSkills": ["skill-id-or-slug"],
  "toolAccessMode": "restricted",
  "config": {}
}

toolAccessMode (string, "restricted" | "open", default "restricted") controls the agent's runtime tool surface in chat:

Mode Tool surface
restricted (default) assignedTools are the ceiling. The agent sees only its assigned tools plus a baseline (web_search, skill activation via activate_skill, memory read/write via recall/save_agent_memory/get_agent_memories, discover_tools) and universal primitives.
open Full main-chat dynamic tool surface (default tools + keyword-triggered groups + persistent discover_tools loads), with assignedTools pinned always-on regardless of keyword matching.

Any value other than "open" is sanitized to "restricted" on save. In both modes the agent's system prompt is built by the unified prompt builder: persona-first, full skills catalog with the agent's assignedSkills highlighted as specialty skills, and persistent agent-scoped memory.- Response (200):

{
  "message": "New agent created",
  "agentId": "agent-id"
}

message is "New agent created" or "Agent updated". If id is omitted (or belongs to another user's agent), a new agent is created with a fresh UUID. New agents inherit the user's selected provider/model when provider/model are not supplied; if neither the body nor user settings yield a provider+model the request fails with 400.

Get Agent by ID

GET /:id

  • Authentication: Required
  • Parameters:
    • id (path): Agent ID- Description: Retrieve a specific agent by ID (full record, owner only — 403 otherwise, 404 if not found)
  • Response:
{
  "id": "agent-id",
  "name": "Agent Name",
  "description": "Agent description",
  "status": "active",
  "icon": "data:image/png;base64,...",
  "category": "Content & Media",
  "provider": "openai",
  "model": "gpt-4o",
  "systemPrompt": "Persona / directives text",
  "assignedTools": ["web_search"],
  "assignedWorkflows": [],
  "assignedSkills": [],
  "toolAccessMode": "restricted",
  "created_by": "user-id",
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-01T00:00:00Z"
}

Note: this endpoint returns the raw DB row plus parsed arrays — timestamps are snake_case (created_at), unlike the camelCase list endpoints.

Update Agent

PUT /:id

  • Authentication: Required
  • Parameters:
    • id (path): Agent ID
  • Description: Same handler as POST /save — identical body fields (name, description, systemPrompt, assignedTools, assignedSkills, assignedWorkflows, toolAccessMode, provider, model, icon, category, status, creditLimit) and identical response:
{
  "message": "Agent updated",
  "agentId": "agent-id"
}

Delete Agent

DELETE /:id

  • Authentication: Required
  • Parameters:
    • id (path): Agent ID- Description: Delete an agent by ID. Also deletes the agent's memories and broadcasts AGENT_DELETED to the user's connected clients.
  • Response:
{
  "message": "Agent <id> deleted successfully."
}

Chat with Agent

POST /:id/chat

  • Authentication: Required
  • Parameters:
    • id (path): Agent ID- Note: Agent chats run through the same unified orchestrator as the main chat — persona-first system prompt, full skills catalog (assigned skills highlighted), persistent agent-scoped memory, and the tool surface determined by the agent's toolAccessMode (see Save/Update Agent). External bridges (Discord, Mattermost, etc.) get identical behavior through this endpoint.
  • Body:
{
  "message": "Your message here",
  "history": [{ "role": "user", "content": "earlier turn" }],
  "conversationId": "optional-stable-conversation-key",
  "provider": "optional-override",
  "model": "optional-override",
  "enabledTools": ["web_search"]
}

All fields except message are optional. provider/model default to the agent's saved provider/model, then the user's selected settings. conversationId keys persistent conversation history. enabledTools narrows (never widens) the agent's tool surface.

  • Response: Server-sent events stream (same event vocabulary as Universal Chatconversation_started, content_delta, tool_start/tool_end, final_content, done, …). There is no JSON response mode.

Stream Chat with Agent

POST /:id/chat-stream

  • Authentication: Required
  • Parameters:
    • id (path): Agent ID
  • Description: Identical to POST /:id/chat (both routes call the same universal handler and both stream). Kept for backward compatibility.
  • Response: Server-sent events stream

Get Agent Suggestions

POST /:id/suggestions

  • Authentication: Required
  • Parameters:
    • id (path): Agent ID
  • Body:
{
  "lastUserMessage": "the user's last message",
  "lastAssistantMessage": "the agent's last reply",
  "history": []
}

provider/model are auto-filled from the agent's saved config; the agent's persona and assigned-tool list are injected into the suggestion prompt.

  • Response (JSON, not a stream):
{
  "suggestions": [
    { "id": "suggestion_1", "text": "Draft the outline", "icon": "\ud83d\udcdd" },
    { "id": "suggestion_2", "text": "Search for sources", "icon": "\ud83d\udd0d" },
    { "id": "suggestion_3", "text": "Summarize so far", "icon": "\ud83d\udccb" }
  ]
}

On LLM failure the endpoint still returns 200 with three generic fallback suggestions plus an error field.

Export Agent

GET /:id/export

  • Authentication: Required (owner only — 403 otherwise)
  • Parameters:
    • id (path): Agent ID
  • Description: Export a portable agent envelope (persona, provider/model, assigned tools/skills/workflows by name/id — no memories, no credentials).
  • Response:
{
  "_format": "agnt-agent",
  "_version": 1,
  "payload": {
    "name": "Agent Name",
    "description": "...",
    "icon": null,
    "provider": "openai",
    "model": "gpt-4o",
    "systemPrompt": "...",
    "category": "",
    "status": "ACTIVE",
    "creditLimit": 1000,
    "assignedTools": ["web_search"],
    "assignedSkills": [],
    "assignedWorkflows": []
  },
  "exported_at": "2026-01-01T00:00:00Z"
}

Import Agent

POST /import

  • Authentication: Required
  • Body: An export envelope (either the envelope itself or wrapped as { "envelope": { ... } })
  • Response (201):
{
  "success": true,
  "agentId": "new-agent-id",
  "missingRefs": { "tools": [], "skills": [], "workflows": [] }
}

missingRefs lists referenced tools/skills/workflows that don't exist in this installation (the agent is still created). Malformed envelopes return 400.


Orchestrator Routes

Base path: /api/orchestrator

Health Check

GET /health

  • Authentication: None
  • Description: Check if the orchestrator service is running
  • Response:
{
  "status": "OK"
}

Get Available Tools

GET /tools

  • Authentication: Required
  • Description: Get the list of tools available to the orchestrator (native tools, registry tools, and installed plugin tools). Used by the frontend tool selector to render available actions.
  • Response:
{
  "tools": [
    {
      "name": "tool_name",
      "description": "What the tool does",
      "parameters": { "type": "object", "properties": {} },      "category": "native|plugin|registry"
    }
  ]
}

Universal Chat

POST /chat

  • Authentication: Required
  • Content-Type: multipart/form-data (or JSON when no files are attached)
  • Body (common parameters, all optional except message):
    • message (string): Chat message
    • messages (array): Full message array (alternative to message + history)
    • history (array): Prior turns as { role, content } objects
    • conversationId (string): Stable key for persistent conversation state (context, loaded tool groups, activated skills)
    • provider / model (string): LLM override. Resolution order: body → agent record (saved-agent chats) → user's selected settings → first provider with valid credentials
    • enabledTools (array): Explicit tool whitelist from the tool selector. Narrows the surface; an empty array means zero tools
    • reasoningEnabled (boolean) / reasoningValue (string): Extended-thinking controls for reasoning-capable models
    • files (file[]): Optional file attachments (max 20MB each)
    • Surface-context parameters (any one of these routes the chat to that surface): agentId/agentContext/agentState, workflowId/workflowContext/workflowState, toolId/toolContext/toolState, widgetId/widgetContext/widgetState, goalId/goalContext, codeId/codeContext, skillId/skillInstructions/skillName/skillDescription/skillAllowedTools
  • Description: Universal chat handler. The chat type is detected from the route path and/or which context parameters are present; all typed routes below share this handler and differ only in the injected page context.
  • Response: Server-sent events stream. Key named events:
Event Payload highlights
conversation_started conversationId
files_processed attached-file summaries
assistant_message assistantMessageId for the turn
content_delta / reasoning_delta delta, accumulated text
tool_pending / tool_start / tool_end tool call id, name, args / result / error
tool_executions per-round tool execution details
image_generated image refs for generated images
data_content / data_offloaded large tool results moved to the offload store
context_status / context_managed context-window usage / trimming info
steering_applied mid-run user steering acknowledged
frontend_event UI-targeted tool side effects (highlights, tours)
error error message for the turn
final_content complete assistant response text
done stream end

Agent Chat

POST /agent-chat

  • Authentication: Required
  • Content-Type: multipart/form-data
  • Body: Universal Chat parameters plus:
    • agentId (string): A saved agent ID for a persona chat, or the sentinel "agent-chat" for the AgentForge builder surface (Annie persona + agent-management functions)
  • Description: Saved-agent chats get the unified prompt (agent persona first, full skills catalog with specialty highlights, agent-scoped memory) and the tool surface defined by the agent's toolAccessMode. Equivalent to POST /api/agents/:id/chat, which resolves the agent server-side.
  • Response: Server-sent events stream

Workflow Chat

POST /workflow-chat

  • Authentication: Required
  • Content-Type: multipart/form-data
  • Body:
    • workflowId (string): Workflow ID
    • message (string): Chat message
    • files (file[]): Optional file attachments
  • Description: Chat with a specific workflow
  • Response: Server-sent events stream

Tool Chat

POST /tool-chat

  • Authentication: Required
  • Content-Type: multipart/form-data
  • Body:
    • toolId (string): Tool ID
    • message (string): Chat message
    • files (file[]): Optional file attachments
  • Description: Chat with a specific tool
  • Response: Server-sent events stream

Goal Chat

POST /goal-chat

  • Authentication: Required
  • Content-Type: multipart/form-data
  • Body:
    • goalId (string): Goal ID
    • message (string): Chat message
    • files (file[]): Optional file attachments
  • Description: Chat with a specific goal
  • Response: Server-sent events stream

Artifact Chat

POST /artifact-chat

  • Authentication: Required
  • Content-Type: multipart/form-data
  • Body:
    • message (string): Chat message
    • codeContext (object, optional): Context from the artifacts workspace (active file, selection, open files)
    • files (file[]): Optional file attachments (max 20MB each)
  • Description: Chat handler for the Artifacts workspace ("Annie" assistant). Streams responses and calls the workspace file-operation tools (read_file, write_file, edit_file, list_files) against the user's workspace root. See the Artifacts section for full details on storage, tools, and events.
  • Response: Server-sent events stream

Widget Chat

POST /widget-chat

  • Authentication: Required
  • Content-Type: multipart/form-data
  • Body:
    • message (string): Chat message
    • files (file[]): Optional file attachments (max 20MB each)
  • Description: Widget-specific chat with streaming. Used for creating and editing custom dashboard widgets.
  • Response: Server-sent events stream### Get Suggestions

POST /suggestions

  • Authentication: Required
  • Body:
{
  "lastUserMessage": "the user's last message",
  "lastAssistantMessage": "the assistant's last reply",
  "history": [],
  "provider": "openai",
  "model": "gpt-4o",
  "agentContext": {}
}

provider and model are required (400 without them). agentContext is optional and shapes suggestions to an agent's persona/tools.

  • Response (JSON, not a stream):
{
  "suggestions": [
    { "id": "suggestion_1", "text": "Draft the outline", "icon": "\ud83d\udcdd" },
    { "id": "suggestion_2", "text": "Search for sources", "icon": "\ud83d\udd0d" },
    { "id": "suggestion_3", "text": "Summarize so far", "icon": "\ud83d\udccb" }
  ]
}

On LLM failure the endpoint returns 200 with three generic fallback suggestions plus an error field.


Async Tool Routes

Base path: /api/async-tools

\u26A0\uFE0F Critical Behaviour Notes (read before integrating)

These behaviours were discovered during empirical stress-testing on 2026-05-04. Most of the silent-degradation cases were fixed in the same dated commit \u2014 see `ASYNC-TOOLS-REFERENCE.md` \u00A7 Recent Fixes for the fix table.

\ud83d\udd34 In-batch ordering is NOT guaranteed. When multiple async tool calls are submitted in the same function-calls block, the orchestrator fires them all in the same millisecond with no ordering. Submitting a write followed by a read of the same path can produce a read failure (ENOENT) 2+ seconds before the write completes. This is by design \u2014 async tools are independent and unordered. Use synchronous calls for any read-after-write or dependent operation. Non-LLM integrators (workflow nodes, webhook handlers, third-party callers) must enforce ordering themselves: await the queued result before issuing the next dependent call.

\u2705 Silent-degradation parameter patterns FIXED (2026-05-04). The four shapes below used to silently fall back to a one-shot. They now return a structured validation error at queue time so callers can self-correct:

  1. _interval: 0 (or negative / non-numeric) \u2014 rejected.
  2. _stopAfter: N without _interval \u2014 rejected.
  3. _duration: N without _interval \u2014 rejected.
  4. _delayFirst: true without _interval \u2014 rejected.

\u2705 Failure observability FIXED (2026-05-04). The GET /api/async-tools/status endpoint now reports three failure-related counters:

  • failed \u2014 system-level failures only (worker crashed/aborted). Same semantics as before.
  • businessFailed \u2014 completed executions whose inner result.success === false (ENOENT, EPERM, per-iteration errors in periodic runs).
  • totalFailed \u2014 failed + businessFailed, exact (the source sets are disjoint).

\u2705 Autonomous-message banner FIXED (2026-05-04). The wrapper that delivers async results into the conversation now inspects the inner result and emits \u26A0\uFE0F ASYNC TOOL FINISHED WITH ERROR with an honesty directive when the operation reported failure, instead of always claiming success.

\u2705 Sub-second intervals work (validated down to _interval: 0.1), but at intervals shorter than the tool\u2019s own execution time the actual gap is dominated by tool throughput, not the timer. Note: _interval: 0 is no longer accepted (see fix above).

\u2705 True OS-level concurrency is real \u2014 five parallel ping shells took 5.2 s total instead of 21 s.

\u26a0\ufe0f Whole feature is experimental and OFF by default (2026-05-04). A per-user setting (asyncToolsEnabled on PUT /api/users/settings) gates whether the LLM can use async tools at all. With it OFF (default), the universal async control params drop off every tool schema AND the async-guidance prompt section is omitted, so the LLM has no way to know async exists. In-flight tasks that started before the toggle flipped run to completion. New users see the chat behave like a conventional sync-only assistant until they explicitly enable the feature in Settings. See `ASYNC-TOOLS-REFERENCE.md` \u00a7 Async tools toggle for the full contract.

Tool-Call Async Parameters (How to Use Async)

Every tool in the AGNT registry — native, plugin, registry, MCP — supports background and recurring execution via a set of universal underscore-prefixed parameters. These are not part of the tool's own schema; they are intercepted by the orchestrator before the tool runs.

📘 For the comprehensive guide with worked examples, error handling patterns, edge cases, and empirically-validated test results, see `ASYNC-TOOLS-REFERENCE.md`.

Universal Async Parameters

Parameter Type Default Description
_executeAsync boolean false Run the tool in the background. Returns an executionId immediately; results arrive later via autonomous message.
_interval integer (seconds) Re-run the tool every N seconds. Requires _executeAsync.
_stopAfter integer Stop after N iterations. Requires _interval.
_duration number (minutes) Stop after N minutes total. Decimals allowed (e.g. 0.1 = 6 seconds). Requires _interval.
_delayFirst boolean false Skip the immediate first run — wait one full _interval before the first execution. Requires _interval.
_estimatedMinutes number UI hint for expected duration. No functional impact on scheduling — purely cosmetic.

Common Patterns

Run once in the background:

{ "query": "latest AI news", "_executeAsync": true }

Run a real tool once after a delay (e.g. send a reminder email in 1 hour):

{
  "to": "user@example.com",
  "subject": "Reminder",
  "body": "Don't forget!",
  "_executeAsync": true,
  "_interval": 3600,
  "_stopAfter": 1,
  "_delayFirst": true
}

Run every 60 seconds, exactly 5 times:

{ "...": "...", "_executeAsync": true, "_interval": 60, "_stopAfter": 5 }

Scrape a site every 5 minutes for 1 hour:

{ "url": "https://example.com", "_executeAsync": true, "_interval": 300, "_duration": 60 }

Safety-belt pattern (stop at whichever limit hits first):

{ "...": "...", "_executeAsync": true, "_interval": 60, "_stopAfter": 100, "_duration": 60 }

Response Shapes

Sync (no async params) — returns the tool's native payload directly:

{ "success": true, "total": 20, "individualRolls": [20], "error": null }

Async, queued — returned immediately when _executeAsync: true:

{
  "success": true,
  "status": "queued",
  "executionId": "8604c5b0-8515-4cb1-8894-918acb31ac25",
  "message": "<toolName> started in the background. You'll receive updates as it progresses.",
  "estimatedDuration": null
}

Async, completed (one-shot) — arrives via autonomous message:

{
  "success": true,
  "status": "completed",
  "executionId": "8604c5b0-8515-4cb1-8894-918acb31ac25",
  "result": "{\"success\":true,\"total\":20,...}",
  "duration": 480
}

The result field is often a JSON-stringified payload. Clients must JSON.parse() it before consuming.

Periodic execution completed — a single combined payload after the full schedule finishes:

{
  "success": true,
  "status": "completed",
  "executionId": "af4cd05e-0c19-4e3b-96fb-fc38c57d81bc",
  "result": {
    "periodicExecution": true,
    "totalIterations": 3,
    "results": [
      { "iteration": 1, "result": "{...}", "timestamp": 1777909033895 },
      { "iteration": 2, "result": "{...}", "timestamp": 1777909043897 },
      { "iteration": 3, "result": "{...}", "timestamp": 1777909053906 }
    ],
    "totalDuration": 20011
  },
  "duration": 20011
}

Two-Layer Status Model (Critical)

Async responses have two independent layers that must both be checked:

  1. System layer — the outer envelope (success, status). Tells you whether the orchestrator queued/ran the task.
  2. Business-logic layerresult.success or the parsed payload. Tells you whether the tool's operation actually succeeded.

A task can be status: "completed" at the system level while result.success: false at the business layer (e.g. file not found, invalid parameter, API error).

if (response.status === "completed") {
  const inner = typeof response.result === "string"
    ? JSON.parse(response.result)
    : response.result;
  if (inner.success) {
    // use inner data
  } else {
    // handle inner.error (e.g. ENOENT, EPERM, validation message)
  }
}

Empirically-Verified Behaviors

These behaviors were validated via live tool calls (see ASYNC-TOOLS-REFERENCE.md for the full test log):

  • Validation runs at execution time, not queue time. Invalid parameters are queued successfully and only fail when the task actually runs.
  • Conflicting stop conditions: when both _stopAfter and _duration are set, whichever limit triggers first wins. Tested with _stopAfter: 100 + _duration: 0.1 (6 s) — task stopped at 3 iterations.
  • Parallel concurrency: 19+ simultaneous async calls in a single function-calls block all queue and execute cleanly. No rate limit observed.
  • Mixed sync + async in the same function-calls block works — sync results return inline, async return execution IDs.
  • Periodic results are batched. A periodic task delivers no output until the full schedule completes. Use separate async calls (one per assistant message) if streaming is required.
  • _estimatedMinutes is purely cosmetic. Tasks run as fast as they can regardless of the estimate.
  • Minimum interval tested: 1 second (with ~10–20 ms drift per iteration). Sub-second intervals untested.
  • Decimal _duration values work (e.g. 0.1 = 6 seconds).
  • Error envelope: business-logic failures (ENOENT, EPERM, validation errors) surface inside result.error while the outer status remains "completed". The system never crashes or hangs on bad input.

Anti-Patterns

Do not use async for dependent tasks. If Task B references Task A's output, Task A must be sync.

Do not build a fake sleep / echo / timer tool to schedule something later. Attach _executeAsync, _interval, _stopAfter: 1, _delayFirst: true directly to the real tool you want to run.

Do not start a periodic task without _stopAfter or _duration. It will run until cancelled via POST /cancel/:executionId.


Get Queue Status

GET /status

  • Authentication: Required
  • Description: Get async tool queue statistics
  • Response:
{
  "success": true,
  "stats": {
    "pending": 0,
    "running": 2,
    "completed": 15,
    "failed": 1
  }
}

Get Executions by Conversation

GET /executions/:conversationId

  • Authentication: Required
  • Parameters:
    • conversationId (path): Conversation ID
  • Description: Get all async tool executions for a conversation
  • Response:
{
  "success": true,
  "executions": [
    {
      "executionId": "exec-id",
      "toolName": "tool-name",
      "status": "running|completed|failed|cancelled",
      "startedAt": "2024-01-01T00:00:00Z",
      "completedAt": null
    }
  ]
}

Get Running Executions

GET /executions/:conversationId/running

  • Authentication: Required
  • Parameters:
    • conversationId (path): Conversation ID
  • Description: Get only running async tool executions for a conversation
  • Response:
{
  "success": true,
  "executions": []
}

Get Execution Details

GET /execution/:executionId

  • Authentication: Required
  • Parameters:
    • executionId (path): Execution ID
  • Description: Get details of a specific async tool execution
  • Response:
{
  "success": true,
  "execution": {
    "executionId": "exec-id",
    "toolName": "tool-name",
    "status": "completed",
    "result": {}
  }
}
  • Error (404): Execution not found

Cancel Execution

POST /cancel/:executionId

  • Authentication: Required
  • Parameters:
    • executionId (path): Execution ID
  • Description: Cancel a running async tool execution
  • Response:
{
  "success": true,
  "message": "Async tool execution cancelled successfully"
}

Cancel All Executions for Conversation

POST /cancel-all/:conversationId

  • Authentication: Required
  • Parameters:
    • conversationId (path): Conversation ID
  • Description: Cancel all running async tools for a conversation (global stop)
  • Response:
{
  "success": true,
  "cancelled": 3
}

Group Routes

Base path: /api/groups

Groups organize content outputs (artifacts, generated assets) into a hierarchical tree. They support nesting via parent_id, custom sort order, and color-coding. All mutations broadcast realtime events (GROUP_CREATED, GROUP_UPDATED, GROUP_DELETED, CONTENT_UPDATED) to connected clients of the same user.

List Groups

GET /

  • Authentication: Required
  • Description: Retrieve all groups belonging to the authenticated user
  • Response:
{
  "groups": [
    {
      "id": "group-id",
      "user_id": "user-id",
      "name": "Group Name",
      "description": "Optional description",
      "color": "#6366f1",
      "sort_order": 0,
      "parent_id": null,
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-01-01T00:00:00Z"
    }
  ]
}

Create Group

POST /

  • Authentication: Required
  • Body:
{
  "name": "Group Name",
  "description": "Optional description",
  "color": "#6366f1",
  "sort_order": 0,
  "parent_id": "optional-parent-group-id"
}
  • Description: Create a new group. name is required; all other fields are optional. color defaults to #6366f1, sort_order to 0, parent_id to null (top-level).
  • Response: 201 Created with the full group object

Update Group

PUT /:id

  • Authentication: Required
  • Parameters:
    • id (path): Group ID
  • Body: Any subset of name, description, color, sort_order, parent_id
  • Description: Update fields on a group
  • Response: Updated group object, or 404 if not found

Delete Group

DELETE /:id

  • Authentication: Required
  • Parameters:
    • id (path): Group ID
  • Query Parameters:
    • mode (string, optional): move (default) reparents direct children to this group's parent (or root); delete lets the ON DELETE CASCADE remove child groups along with this one.
  • Description: Delete a group. Use mode=move to preserve children, mode=delete to remove the entire subtree.
  • Response:
{
  "message": "Group deleted"
}

Reorder Groups

PATCH /reorder

  • Authentication: Required
  • Body:
{
  "orders": [
    { "id": "group-id-1", "sort_order": 0 },
    { "id": "group-id-2", "sort_order": 1 }
  ]
}
  • Description: Update sort_order for multiple groups in a single call
  • Response:
{ "success": true }

Move Content Output to Group

PATCH /move/:outputId

  • Authentication: Required
  • Parameters:
    • outputId (path): Content output ID
  • Body:
{
  "group_id": "target-group-id-or-null"
}
  • Description: Move a single content output into a group. Pass group_id: null to ungroup (move to root).
  • Response:
{ "success": true }

Bulk Move Content Outputs

PATCH /bulk-move

  • Authentication: Required
  • Body:
{
  "output_ids": ["output-id-1", "output-id-2"],
  "group_id": "target-group-id-or-null"
}
  • Description: Move multiple content outputs into a group in a single call
  • Response:
{ "success": true }