Studio API
Auto-generated from the OpenAPI spec. Run
node docs-site/scripts/fetch-openapi.jsto regenerate.
GET /api/v1/studio/branding
Get Studio Branding
Responses
- 200 — Successful Response
GET /api/v1/studio/error-reporting
Get Error Reporting
The studio's error-monitoring consent, plus whether toggling it would
have any effect on this install (available). Manager-only.
Responses
- 200 — Successful Response
PUT /api/v1/studio/error-reporting
Update Error Reporting
Opt in to / out of error monitoring for this install. Takes effect immediately (no restart): enabling initialises the SDK, disabling closes it. Manager-only.
Request body
| Field | Required | Type | Description |
|---|---|---|---|
enabled | Yes | boolean |
Responses
- 200 — Successful Response
- 422 — Validation Error
GET /api/v1/studio
Get Studio Settings
Responses
- 200 — Successful Response
PUT /api/v1/studio
Update Studio Settings
Request body
| Field | Required | Type | Description |
|---|---|---|---|
studio_name | No | string | |
address | No | string | |
country | No | string | |
tax_id | No | string | |
default_vat_rate | No | number | |
timezone | No | string | |
cancellation_hours | No | integer | |
cancellation_deducts_credit | No | boolean | |
late_cancel_fee | No | number | |
no_show_fee | No | number | |
checkin_open_minutes_before | No | integer | |
checkin_close_minutes_after | No | integer | |
waitlist_confirm_minutes | No | integer | |
guest_bookings_enabled | No | boolean | |
self_service_purchases_enabled | No | boolean | |
reminder_hours_before | No | integer | |
calendar_start_hour | No | integer | |
calendar_end_hour | No | integer | |
primary_color | No | string | |
secondary_color | No | string | |
tunnel_url | No | string |
Responses
- 200 — Successful Response
- 422 — Validation Error
GET /api/v1/studio/status
Get Studio Status
Responses
- 200 — Successful Response
POST /api/v1/studio/backup
Trigger Backup
Take an on-demand SQLite snapshot of the studio database into
app_data_dir()/backups/ — the same artifact the nightly backup job
produces (app/tasks/nightly_backup.py).
Called by the desktop auto-updater before it applies an update, and
available as a manual "back up now" action in the Settings UI. Manager-only.
Prunes the backups directory to the newest 30 files and records
StudioSettings.last_backup_at.
Responses
- 200 — Successful Response
POST /api/v1/studio/reset
Reset Studio
Wipe all studio data and return to the setup wizard (PRODUCT_SPEC.md §3.4 "Danger Zone", TECHNICAL_SPEC.md §6.2).
Full-access manager only — gated by require_manager (checks
user.role.is_full_access, the same underlying flag is_full_access_staff
reads for the other "superpower" gates in this codebase, e.g. retroactive
bookings in bookings.py/appointments.py) — never any other staff role.
require_manager is used directly rather than is_full_access_staff here
because this endpoint is manager-only from the door (unlike those mixed
staff/client endpoints, which authenticate broadly and only use
is_full_access_staff as an in-body conditional); every other
manager-only endpoint in this router (PUT /studio, POST /studio/backup,
POST /studio/ai) uses the same require_manager dependency.
Re-authentication friction, both required:
current_passwordmust match the caller's ownpassword_hash(not any other manager's, if this codebase ever supports more than one full-access account) — 401 AUTH_INVALID_CREDENTIALS on mismatch, withburn_password_check()on the no-match path for timing consistency with login (SECURITY_GUIDELINES.md §1.3).confirm_studio_namemust exactly matchStudioSettings.studio_name— 400 STUDIO_NAME_MISMATCH otherwise (including when no StudioSettings row exists at all, which can't be confirmed against anything).
On success: takes an on-demand backup (app/backup.py's perform_backup
— the same function/artifact the nightly job and POST /studio/backup
use) BEFORE wiping anything. If the backup itself fails, the reset is
aborted with 500 BACKUP_FAILED and nothing is wiped — a mistaken reset
must stay recoverable, so this never silently wipes without a fresh
backup to fall back on. The wipe itself (app/services/ studio_reset_service.wipe_studio_data) deletes every table's rows in
FK-safe order (never DROP, never touches alembic_version) including
studio_settings itself, so needs_setup (see GET /studio/branding)
evaluates true again on the next load — exactly the fresh-install state.
db.commit() happens here, once, covering both the backup's own
last_backup_at write and the wipe — perform_backup commits
internally (see its docstring) and wipe_studio_data never does (per
backend/CLAUDE.md's transaction-semantics rule for app/services/).
Request body
| Field | Required | Type | Description |
|---|---|---|---|
current_password | Yes | string | |
confirm_studio_name | Yes | string |
Responses
- 200 — Successful Response
- 422 — Validation Error
AI Assistant configuration
The AI Assistant runs only on the studio's own AI provider key (a key saved with
POST /studio/ai below, or an LLM_API_KEY in the backend's environment) or on a local keyless
model (Ollama, via an ollama/ or ollama_chat/ LLM_MODEL). There is no hosted fallback: no
prompt or studio data passes through infrastructure run by Agon's developer. With none of these
configured, POST /agent/act and POST /support/chat answer in basic
mode (mode: "basic"): a keyword documentation search with no LLM and no network call.
All /studio/ai* endpoints are manager-only (require_manager) — that's a role check, not
a license check. Since the license-enforcement global dependency (2026-10-13) added an
app-wide 402 LICENSE_INACTIVE gate on every non-allowlisted route, these endpoints are
also subject to that gate like virtually all other routes: once a studio's subscription
lapses past its 72h grace period, /studio/ai* requests are blocked until the license is
renewed, the same as any other non-exempt endpoint. See License enforcement
below for the full allowlist and the two /studio/license endpoints a manager can always
reach to fix this. User guide:
How to set up the AI Assistant.
GET /api/v1/studio/ai/providers
Get Ai Providers
List the providers a manager can bring their own key for. Never includes
litellm_model — that's an internal implementation detail (see
app/services/ai_providers.py's module docstring).
Responses
- 200 — Successful Response
GET /api/v1/studio/ai
Get Ai Status
AI status for the desktop Settings page and the assistant's basic/AI mode.
configured / provider — unchanged: whether a BYO key is stored, and for which
provider. ai_configured / ai_provider / ai_source — the effective state
from resolve_ai_config (BYO key, env LLM_API_KEY or local Ollama): when
ai_configured is false, the assistant runs in basic mode. Never returns a key.
Responses
- 200 — Successful Response
POST /api/v1/studio/ai
Configure Ai
Manager only. Saves the studio's own AI provider key, which switches the assistant from basic
mode to AI mode (see the configuration note above). Validates the
submitted key with a live test call to the chosen provider before persisting anything; the key is then
stored encrypted at rest (Fernet, app/services/crypto.py) and is never returned by
any response.
Request body
| Field | Required | Type | Description |
|---|---|---|---|
provider | Yes | string | One of the id values from GET /studio/ai/providers (e.g. "gemini", "groq"). |
api_key | Yes | string | The provider's API key. Write-only — never echoed back. |
Responses
- 200 —
{"success": true} - 400
AI_KEY_INVALID— The provider rejected the key on the live validation call. - 403 — Caller is not a manager.
- 422
AI_INVALID_PROVIDER—provideris not one of the registered provider ids. Checked before any live call is made. - 422 — Validation Error (missing
providerorapi_key).
The custom error codes above aren't captured by fetch-openapi.js's regeneration
(FastAPI's OpenAPI spec doesn't include raise_api_error responses) — added by hand
from app/routers/studio.py's configure_ai/get_ai_status/get_ai_providers and
app/services/ai_providers.py's provider registry. Keep them if this page is
regenerated.
DELETE /api/v1/studio/ai
Remove Ai Key
Remove the studio's stored BYO AI-provider key, so the studio stops sending data to that provider — immediately, without a restart.
Clears both columns POST /studio/ai writes (gemini_api_key_encrypted,
ai_provider) and the in-memory runtime key, but only when that runtime key came
from the DB (see clear_runtime_byo_key): a key from the environment (.env)
or a local Ollama model is operator configuration, not the studio's stored key,
and stays active. Idempotent — 200 with no key stored. Returns the same shape as
GET /studio/ai, so ai_configured reflects what (if anything) is still active.
Never returns or logs the key.
Responses
- 200 — Successful Response
License enforcement
Since the license-enforcement global dependency (2026-10-13) was wired in, a studio whose
subscription has lapsed past its 72-hour grace period gets a 402 LICENSE_INACTIVE on
every endpoint except: /health, /docs, /redoc, /openapi.json, everything under
/api/v1/auth, the data-export endpoints (CSV/report/backup/PDF downloads), and the two
/studio/license endpoints below. This is the same app-wide gate noted on the /studio/ai*
endpoints above — see that note for how it also applies to
AI-configuration requests. A studio that has never configured a license key at all (the default
for every pre-existing self-hosted install) is never gated — only saving a key via
PUT /studio/license opts a studio into enforcement.
GET /api/v1/studio/license
Get License Status
This studio's license state. Manager-only (require_manager). Never returns the license key
itself — only whether one is configured, the outcome of the last background check, and (only
while in the 72-hour grace window) when it expires. Always reachable, even while a studio is
blocked, so a manager can see why and paste in a fresh key.
Responses
- 200 — Successful Response
{
"configured": true,
"status": "problem",
"last_checked_at": "2026-10-13T09:15:00Z",
"grace_until": "2026-10-16T09:15:00Z",
"current_amount_cents": 4000,
"next_renewal_at": "2026-11-13T09:15:00Z"
}
| Field | Type | Description |
|---|---|---|
configured | boolean | Whether a license key has ever been saved for this studio. |
status | string | "active", "problem", or "unchecked" — mirrors the last background check's outcome. Meaningless (always "unchecked") when configured is false. |
last_checked_at | string | null | ISO timestamp of the last check, or null if none has run yet. |
grace_until | string | null | ISO timestamp the 72-hour grace period ends. Non-null only while status is "problem" and the window hasn't expired; null once it has expired (or once status is "active" again). |
current_amount_cents | integer | null | The studio's current recurring contribution, in cents (Agon is pay-what-you-want). null if configured is false or the amount isn't known yet. |
next_renewal_at | string | null | ISO timestamp of the subscription's next Stripe billing date. null if configured is false or no upcoming renewal is scheduled. |
- 403 — Caller is not a manager.
PUT /api/v1/studio/license
Update License
Set or replace this studio's license key — the opaque string received at checkout on the
Agon-web pricing/checkout flow — pasted in by the manager in Settings → License. Manager-only
(require_manager). No format validation beyond non-blank; real verification happens
server-side against the license-verify API.
Runs an immediate check right after saving so the manager gets live feedback instead of waiting
for the next periodic run (every 8 hours). A failed or unreachable check is never fatal to this
request — the key is saved either way, and the 72-hour grace clock (if the check comes back
anything other than "active") starts from this moment. Never logs the key.
Request body
| Field | Required | Type | Description |
|---|---|---|---|
license_key | Yes | string | The license key received at checkout. Must not be blank. |
Responses
- 200 — Successful Response. Same shape as
GET /api/v1/studio/licenseabove — reflects the result of the immediate check that ran right after saving. Never returns the key itself. - 403 — Caller is not a manager.
- 422 — Validation Error (
license_keymissing or blank).
POST /api/v1/studio/feedback
Submit Feedback
Responses
- 201 — Successful Response
- 422 — Validation Error