Prompt library API
A tenant prompt is a per-organization, admin-managed, versioned prompt template: a display name, an optional description, the prompt template body, an optional category, and a monotonic version counter. Every create and every edit snapshots the full body, so any past version can be restored (restore mints a new version whose body equals the chosen one — no history is destroyed). This is the organization-wide library managed by an authorized admin; it is separate from a user's personal prompts (the /chat-commands surface). In the UI it is surfaced as Workspace › Library › Prompts, where each row also offers a Use in chat action that inserts the prompt into a new chat.
Base URL: https://<your-gateway-host>/admin/v1
Required role for every endpoint: admin, tenant_admin, or ki_manager. A caller with any lower role is rejected 403; an unauthenticated caller 401. The client is never the authorization boundary — every call is re-gated and tenant-scoped server-side.
Four-eyes approval. If the organization has enabled config approval, a create/update/delete/restore by a Fachadmin (
tenant_admin/ki_manager) is not applied immediately — it returns202 { "status": "pending_approval", "approval_id": "…" }and is held until a second, different administrator approves it. See the Config approvals API. This is off by default; when off, these endpoints apply immediately as described below.
Tenant scope
Tenant scope is decided server-side; the client value never widens it.
| Caller | Scope |
|---|---|
admin |
Must target one tenant with ?tenant_id=<id>. |
tenant_admin / ki_manager |
Always their own tenant. A ?tenant_id naming another tenant is ignored (pinned to the caller's own). |
These routes are inherently per-tenant: an admin who omits ?tenant_id is rejected 400 { "error": "tenant_id required" }; a repeated ?tenant_id=a&tenant_id=b is 400 { "error": "tenant_id must be a single value" }; a non-admin whose account carries no tenant is refused 403. A prompt (or a prompt version) that belongs to another tenant reads as 404 not-found — the boundary never leaks another tenant's existence.
GET /tenant-prompts
List the tenant's live prompts (soft-deleted prompts are excluded), ordered by name. Returns a JSON array; description and category are null when unset. version may serialize as a string. Each row also carries owner_name — a display label for the prompt's author (COALESCE(user.name, user.email) resolved server-side from created_by), which is null when the author was erased (created_by is anonymized to NULL, never leaking another user) — and updated_at, surfaced for the library's Owner and Updated columns. owner_name is a display label only: the tenant fence stays on tenant_id, and an author is always same-tenant, so no cross-tenant identity can appear.
[
{ "id": "…", "name": "Complaint reply", "description": "Standard tone", "template": "Answer politely…", "category": "Support", "version": "2", "owner_name": "curator@acme.example", "created_at": 1753000000, "updated_at": 1753000100 }
]
GET /tenant-prompts?q=<query>
When q is a non-empty string, performs a server-side full-text search (MATCH … IN BOOLEAN MODE) over name + description + template, tenant-scoped. The query is escaped to a safe boolean expression: tokens shorter than 3 bytes are dropped and operator characters (+ - * " ( ) ~ < > @) can never reach the engine, so operator injection is impossible and an unbalanced quote can never break the query. An empty query or a query whose every token was dropped returns [] (no error, no unfiltered list). Search rows carry the same fields as the plain list except owner_name — the search query does not resolve the author label, so the library's Owner column is blank in search mode.
POST /tenant-prompts
Create a prompt at version 1 (and its v1 snapshot). Body (JSON object):
| Field | Accepted | Rejected → 400 |
|---|---|---|
name |
string, 1–255 characters, valid UTF-8 (required) | missing / empty / non-string / > 255 chars / malformed UTF-8 |
template |
string, 1–65535 bytes, valid UTF-8 (required) | missing / empty / non-string / > 65535 bytes / malformed UTF-8 |
description |
string ≤ 65535 bytes valid UTF-8, or null / "" → stored NULL (optional) |
non-string / > 65535 bytes / malformed UTF-8 |
category |
string ≤ 64 characters valid UTF-8 (trimmed), or null / "" / whitespace → stored NULL (optional) |
non-string / > 64 chars / malformed UTF-8 |
Success → 201 { "id": "<new-id>" }. version, created_by, created_at, updated_at are server-set and never accepted from the client.
PATCH /tenant-prompts/<id>
Update a prompt's mutable fields (mints a new version + snapshot). Only the fields present in the body are changed; each present field is validated exactly as in POST above. description / category present as null (or empty) clear the column to NULL; absent leaves it unchanged.
An empty body (no updatable field) is rejected 400 { "error": "no updatable fields" }. A prompt that does not exist in the caller's tenant (unknown id / another tenant / already soft-deleted) → 404 { "error": "prompt not found" }. Success → 200 { "ok": true }.
DELETE /tenant-prompts/<id>
Soft-delete a prompt (it stops resolving on every read path; its version history is retained until the organization is purged). Unknown / another tenant / already deleted → 404 { "error": "prompt not found" }. Success → 200 { "deleted": true }.
GET /tenant-prompts/<id>/versions
The prompt's version history, newest first, tenant-scoped through the parent prompt. Returns a JSON array of { "version", "name", "description", "template", "category", "created_at" }. A prompt not in the caller's tenant (or soft-deleted) → 404 (every live prompt has ≥ 1 version, so an empty history is never a valid response — it is reported as not-found).
GET /tenant-prompts/<id>/versions/<version>
One historical snapshot. version must be a positive integer (1 … 2147483647); a non-numeric / fractional / out-of-range value → 400 { "error": "invalid version" }. An unknown version, or a prompt not in the caller's tenant → 404 { "error": "version not found" }.
POST /tenant-prompts/<id>/versions/<version>/restore
Restore a chosen version: re-applies its snapshot through the single update path, minting a new version whose body equals the chosen one (no history is lost). A NULL snapshot description / category is faithfully restored back to NULL. version validation and not-found behaviour are as for the single-version read above. Success → 200 { "ok": true }.
Notes
- Full-gauntlet isolation. Cross-tenant access is impossible: every CRUD query is scoped
tenant_id = ?and every version query is scoped by a JOIN through the parent prompt (parent.tenant_id = ? AND parent.deleted_at IS NULL). Atenant_admin/ki_managercan never read, edit, restore, or delete another organization's prompt or version. - DSGVO / GDPR. When a user is erased, the
created_byauthor pointer on every prompt and version snapshot is anonymized (set toNULL) while the organization's library is preserved. When an organization is purged, its prompts and all version snapshots are removed entirely (foreign-key cascade), with the count recorded in the deletion protocol.