Passa al contenuto principale

Studio API

Auto-generated from the OpenAPI spec. Run node docs-site/scripts/fetch-openapi.js to 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

FieldRequiredTypeDescription
enabledYesboolean

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

FieldRequiredTypeDescription
studio_nameNostring
addressNostring
countryNostring
tax_idNostring
default_vat_rateNonumber
timezoneNostring
cancellation_hoursNointeger
cancellation_deducts_creditNoboolean
late_cancel_feeNonumber
no_show_feeNonumber
checkin_open_minutes_beforeNointeger
checkin_close_minutes_afterNointeger
waitlist_confirm_minutesNointeger
guest_bookings_enabledNoboolean
self_service_purchases_enabledNoboolean
reminder_hours_beforeNointeger
calendar_start_hourNointeger
calendar_end_hourNointeger
primary_colorNostring
secondary_colorNostring
tunnel_urlNostring

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_password must match the caller's own password_hash (not any other manager's, if this codebase ever supports more than one full-access account) — 401 AUTH_INVALID_CREDENTIALS on mismatch, with burn_password_check() on the no-match path for timing consistency with login (SECURITY_GUIDELINES.md §1.3).
  • confirm_studio_name must exactly match StudioSettings.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

FieldRequiredTypeDescription
current_passwordYesstring
confirm_studio_nameYesstring

Responses

  • 200 — Successful Response
  • 422 — Validation Error

AI Assistant configuration​

note

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

FieldRequiredTypeDescription
providerYesstringOne of the id values from GET /studio/ai/providers (e.g. "gemini", "groq").
api_keyYesstringThe 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 — provider is not one of the registered provider ids. Checked before any live call is made.
  • 422 — Validation Error (missing provider or api_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​

note

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"
}
FieldTypeDescription
configuredbooleanWhether a license key has ever been saved for this studio.
statusstring"active", "problem", or "unchecked" — mirrors the last background check's outcome. Meaningless (always "unchecked") when configured is false.
last_checked_atstring | nullISO timestamp of the last check, or null if none has run yet.
grace_untilstring | nullISO 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_centsinteger | nullThe 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_atstring | nullISO 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

FieldRequiredTypeDescription
license_keyYesstringThe license key received at checkout. Must not be blank.

Responses

  • 200 — Successful Response. Same shape as GET /api/v1/studio/license above — 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_key missing or blank).

POST /api/v1/studio/feedback​

Submit Feedback

Responses

  • 201 — Successful Response
  • 422 — Validation Error