# YotoShelf — Full Documentation > YotoShelf is a self-hosted tool for creating, publishing, and sharing Yoto MYO (Make Your Own) audio cards. > Built with Go and Astro/Svelte; provides AI cover art generation, an icon studio, audio transcoding, and > direct publish-to-Yoto. MIT licensed. Source: https://gitlab.com/yotoshelf/yotoshelf --- # Section: Self-Host --- # Self-Host Run YotoShelf on your own hardware or a VPS. A single container image, an SQLite database, and a Yoto API token are all you need. YotoShelf ships as a single OCI container image from `registry.gitlab.com/yotoshelf/yotoshelf`. It embeds its own static file server and uses SQLite — there are no external databases or caches to run alongside it. ## What you need - Docker (or Podman) with Compose support - A Yoto account with at least one Make Your Own card - Persistent storage for the database and card library (a Docker volume or a bind mount) - Two random secrets: `YOTOSHELF_SESSION_SECRET` and `YOTOSHELF_ENCRYPTION_KEY` (each at least 32 bytes) ## Sections in this guide - [Quick Start](/self-host/quick-start) — container run command, required environment variables, first login - [Build from Source](/self-host/build-from-source) — clone, toolchain setup, the `just dev` loop - [Configuration](/self-host/configuration) — full environment variable reference with defaults - [Backup & Restore](/self-host/backup-restore) — built-in CLI subcommands, cron scheduling, restore validation - [Hardware Notes](/self-host/hardware-notes) — Yoto device constraints: cover dimensions, display pixels, audio pipeline ## Runtime image The published image is built from the repository `Containerfile` on every push to `main`. It is a three-stage build: Node (frontend) → Go (binary) → Alpine (runtime + ffmpeg). The final layer is minimal — Alpine plus ffmpeg plus the single statically-compiled binary. The image is tagged `latest` and by commit SHA. Pin to a SHA for production; pull `latest` to track the main branch. --- # Quick Start Run YotoShelf from the registry image in under five minutes. ## Pull and run The simplest way to start is a single `docker run` command. Replace the placeholder secrets with random strings of at least 32 characters each. ```sh docker run -d \ --name yotoshelf \ -p 8080:8080 \ -v yotoshelf-db:/data \ -e YOTOSHELF_DB_PATH=/data/yotoshelf.db \ -e YOTOSHELF_LIBRARY_PATH=/data/library \ -e YOTOSHELF_SESSION_SECRET= \ -e YOTOSHELF_ENCRYPTION_KEY= \ registry.gitlab.com/yotoshelf/yotoshelf:latest ``` Then open http://localhost:8080 and create your first admin account. ## Required environment variables The following variables have no defaults and must be set before the server will start in production: | Variable | Purpose | |---|---| | `YOTOSHELF_SESSION_SECRET` | Signs session cookies. At least 32 bytes. Rotate to invalidate all sessions. | | `YOTOSHELF_ENCRYPTION_KEY` | Encrypts Yoto OAuth tokens at rest. At least 32 bytes. Changing this invalidates stored tokens. | See [Configuration](/self-host/configuration) for the full variable reference, including database path, library path, SMTP, OIDC, and observability settings. ## Volumes and data YotoShelf writes two kinds of data: - **Database** — a single SQLite file at `YOTOSHELF_DB_PATH` (default: `yotoshelf.db` in the working directory). Back this up regularly. - **Library** — audio files, cover images, and generated assets at `YOTOSHELF_LIBRARY_PATH` (default: `library/`). Can be large; put it on the storage you intend to keep. Both paths should be on a persistent volume. A single volume mount works fine — set both variables to paths inside it. ## Creating an admin user If local auth is enabled (the default), the server exposes a `create-admin` subcommand. Run it once before first login: ```sh docker exec yotoshelf /yotoshelf create-admin \ --email admin@example.com \ --password \ --db-path /data/yotoshelf.db ``` Alternatively, configure OIDC and set `YOTOSHELF_LOCAL_AUTH_ENABLED=false`; the first OIDC user to log in is automatically promoted to admin. See [Configuration](/self-host/configuration). ## Reverse proxy YotoShelf listens on `:8080` (configurable via `YOTOSHELF_LISTEN_ADDR`). Place it behind a reverse proxy (Caddy, nginx, Traefik) and terminate TLS there. Set `YOTOSHELF_TRUST_PROXY=true` so audit logs record real client IPs from `X-Forwarded-For`. Set `YOTOSHELF_PUBLIC_URL` to the externally visible URL (e.g. `https://yotoshelf.example.com`) so OAuth callbacks and share links use the correct base. ## Verify the server is up ```sh curl http://localhost:8080/healthz # → 200 OK curl http://localhost:8080/readyz # → 200 OK (checks DB + OIDC + SMTP) ``` --- # Build from Source Clone the repository, install the toolchain with mise, and run the development loop. ## Prerequisites - [mise](https://mise.jdx.dev/) (version manager — installs Go, Node, sqlc, golangci-lint from `.mise.toml`) - Git (with submodule support) - Docker or Podman (for the container build) ## Clone ```sh git clone --recurse-submodules https://gitlab.com/yotoshelf/yotoshelf.git cd yotoshelf ``` The `--recurse-submodules` flag is required — the `frontend/design` submodule provides design tokens, shared Svelte components, and brand assets. ## Install tools ```sh mise install ``` This reads `.mise.toml` and installs the pinned versions of Go, Node, sqlc, golangci-lint, and just. Everything is project-local — no system-level installs required. ## Development loop ```sh just dev ``` This starts the Go backend on `:8080` and the Astro frontend on `:4300` with hot reload. The frontend proxies `/api/**` to the backend. ## Common just recipes ```sh just check # run all checks: lint, test, build just generate # run sqlc + openapi-typescript code generation just seed # seed the database with fixture data for development just test # run unit tests just e2e # run Playwright E2E tests (requires both servers up) ``` ## Container build ```sh docker build -t yotoshelf:local . ``` The Containerfile is a three-stage build: Node (frontend) → Go (binary) → Alpine (runtime + ffmpeg). The binary embeds the compiled frontend via `//go:embed all:frontend/dist`. --- # Configuration YotoShelf is configured entirely via environment variables. All variables are prefixed `YOTOSHELF_`. CLI flags take precedence over environment variables; environment variables take precedence over defaults. ## Core | Variable | Default | Purpose | |---|---|---| | `YOTOSHELF_LISTEN_ADDR` | `:8080` | TCP address the server listens on | | `YOTOSHELF_PUBLIC_URL` | (none) | Externally visible URL. Required for OAuth callbacks and share links. | | `YOTOSHELF_DB_PATH` | `yotoshelf.db` | Path to the SQLite database file | | `YOTOSHELF_LIBRARY_PATH` | `library` | Directory for audio files, covers, and generated assets | | `YOTOSHELF_SESSION_SECRET` | (required) | HMAC secret for session cookies. At least 32 bytes. | | `YOTOSHELF_ENCRYPTION_KEY` | (required) | AES key for Yoto tokens at rest. At least 32 bytes. | | `YOTOSHELF_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` | | `YOTOSHELF_LOG_FORMAT` | `json` | Log format: `json` or `text` | | `YOTOSHELF_TRUST_PROXY` | `false` | Trust `X-Forwarded-For` headers (enable behind a reverse proxy) | ## Auth | Variable | Default | Purpose | |---|---|---| | `YOTOSHELF_LOCAL_AUTH_ENABLED` | `true` | Enable local password authentication | | `YOTOSHELF_OIDC_ENABLED` | `false` | Enable OIDC login | | `YOTOSHELF_OIDC_ISSUER` | (none) | OIDC issuer URL | | `YOTOSHELF_OIDC_CLIENT_ID` | (none) | OIDC client ID | | `YOTOSHELF_OIDC_CLIENT_SECRET` | (none) | OIDC client secret | | `YOTOSHELF_SESSION_MAX_AGE` | `604800` | Session cookie lifetime in seconds (default: 7 days) | ## SMTP (optional) | Variable | Default | Purpose | |---|---|---| | `YOTOSHELF_SMTP_HOST` | (none) | SMTP hostname | | `YOTOSHELF_SMTP_PORT` | `587` | SMTP port | | `YOTOSHELF_SMTP_USER` | (none) | SMTP username | | `YOTOSHELF_SMTP_PASS` | (none) | SMTP password | | `YOTOSHELF_SMTP_FROM` | (none) | From address for outbound email | SMTP is optional. Without it, password reset and email verification are disabled. ## Observability | Variable | Default | Purpose | |---|---|---| | `YOTOSHELF_METRICS_TOKEN` | (none) | Bearer token required for `GET /metrics`. If unset, metrics are disabled. | ## Yoto client | Variable | Default | Purpose | |---|---|---| | `YOTOSHELF_YOTO_CLIENT_ID` | (built-in) | OAuth2 client ID for Yoto API. Override only for testing. | --- # Hardware Notes Yoto device specifications relevant to card making: cover image dimensions, LED display format, audio pipeline, device types, and label printing templates. ## Cover images Yoto displays card cover images on the device. Required specifications: - **Format:** JPEG or PNG - **Dimensions:** 1500×1500 px (square) - **File size:** Under 2 MB recommended - **Colour space:** sRGB YotoShelf generates cover images at 1500×1500 and converts to JPEG before upload. ## LED display (Mini) The Yoto Mini has a 16×8 pixel LED matrix display. Per-track chapter icons are displayed here. - **Format:** PNG - **Dimensions:** 16×16 px (scaled to 16×8 on device) - **Colour:** Single colour (monochrome); any colour is displayed as lit/unlit YotoShelf's icon studio targets 16×16 icons. The Ideogram AI generator is configured for this constraint. ## Audio pipeline Yoto expects AAC audio. YotoShelf transcodes any uploaded format to AAC via ffmpeg. - **Format:** AAC (MPEG-4 audio) - **Bitrate:** 64 kbps (matches Yoto's own conversion) - **Sample rate:** 44.1 kHz - **Channels:** Stereo (or mono for speech content) - **Container:** M4A Track duration is extracted from ffprobe output and stored alongside the track. ## Device types | Device | Display | LED matrix | Notes | |---|---|---|---| | Yoto Player (3rd gen) | Cover art on screen | No | Large form factor | | Yoto Mini | No screen | 16×8 LED | Portable | | Yoto Player Mini (2024) | Cover art on screen | No | Compact | ## Label printing YotoShelf generates printable card labels in PDF format. Labels are designed for: - **Avery L7160** (21 labels per A4 sheet, 63.5×38.1 mm each) - **Avery 5160** (30 labels per letter sheet, 2⅝×1 inch each) Labels include the card cover image, title, and a QR code linking to the collection share page. --- # Backup & Restore YotoShelf provides built-in CLI subcommands for backup and restore. No external tools required. ## What to back up YotoShelf stores all state in two locations: - **Database** (`YOTOSHELF_DB_PATH`) — all user data, cards, collections, tokens, jobs. This is the critical backup. - **Library** (`YOTOSHELF_LIBRARY_PATH`) — audio files, cover images, generated assets. Large but regenerable from Yoto for the audio; covers may be irreplaceable. ## Backup command ```sh docker exec yotoshelf /yotoshelf backup \ --db-path /data/yotoshelf.db \ --output /backups/yotoshelf-$(date +%Y%m%d).db ``` The backup command uses SQLite's online backup API — it is safe to run against a live database without locking the server. ## Restore command ```sh # Stop the server first docker stop yotoshelf # Restore docker run --rm \ -v yotoshelf-db:/data \ -v /backups:/backups \ registry.gitlab.com/yotoshelf/yotoshelf:latest \ /yotoshelf restore \ --db-path /data/yotoshelf.db \ --input /backups/yotoshelf-20260101.db # Restart docker start yotoshelf ``` ## Cron scheduling Example cron entry for daily backups at 02:00, keeping 30 days of history: ```sh 0 2 * * * docker exec yotoshelf /yotoshelf backup --db-path /data/yotoshelf.db --output /backups/yotoshelf-$(date +\%Y\%m\%d).db && find /backups -name 'yotoshelf-*.db' -mtime +30 -delete ``` ## Restore validation After restoring, verify the database is intact: ```sh curl http://localhost:8080/readyz ``` A `200 OK` response confirms the database is reachable and migrations are current. # Section: Contribute --- # Contribute Set up a local development environment and submit your first merge request. YotoShelf is built to welcome AI-assisted contributors. YotoShelf is open source (MIT) and actively maintained. Contributions are welcome — bug fixes, new features, documentation improvements, and test coverage all count. ## Before you start Read [AGENTS.md](https://gitlab.com/yotoshelf/yotoshelf/-/blob/main/AGENTS.md) in the repository root before making any changes. It documents the required workflow, architecture constraints, and the validation checklist every merge request must pass. The site you are reading is a rendering — the repository file is the source of truth. AI-assisted contributors should fetch [/llms-full.txt](https://yotoshelf.dev/llms-full.txt) for a complete machine-readable rendition of this documentation. ## Sections in this guide - [How It Works](/contribute/how-it-works) — the AGENTS.md contract summarised: stop conditions, blast radius limits, the one-thing-at-a-time rule - [Architecture](/contribute/architecture) — Go + huma + SQLite/sqlc/goose; Astro 6/Svelte 5 frontend; go-yoto client; design system submodule - [Dev Loop](/contribute/dev-loop) — clone, mise, just recipes, fixture seeding, E2E - [CI](/contribute/ci) — the yotoshelf/ci catalog components, what each gate checks, the renovate preset - [Conventions](/contribute/conventions) — file-size limits, structural conformity, conventional commits, i18n ## Quick orientation ```sh git clone --recurse-submodules https://gitlab.com/yotoshelf/yotoshelf.git cd yotoshelf mise install # pins Go, Node, sqlc, golangci-lint from .mise.toml just dev # backend :8080 + frontend :4300, hot reload ``` After every change, run `just check`. If it passes locally, it passes in CI. --- # How It Works The AGENTS.md contract: stop conditions, blast-radius limits, and the one-thing-at-a-time rule. AGENTS.md is the primary instruction set for anyone (human or AI) making changes to YotoShelf. It exists because the codebase has several interconnected frameworks that must be kept in sync — changing one without updating the others causes subtle build or runtime failures. ## The one-thing-at-a-time rule Each merge request does exactly one thing. A MR that adds a new API endpoint should not also refactor unrelated code. This is enforced by the MR size gate in CI (`scripts/check-file-size.sh`). ## Stop conditions AGENTS.md lists explicit stop conditions — situations where an automated contributor must stop and request human review rather than proceeding: - Any change to the database schema (`internal/db/migrations/`) - Any change to the auth middleware or session handling - Any change to the Yoto API client (`go-yoto` module) - Any new dependency not already in `go.mod` or `package.json` - Any change to the container build (`Containerfile`, `Dockerfile`) - Any change to CI configuration (`.gitlab-ci.yml`, `ci/`) ## Blast radius limits Changes are scoped to minimise risk: - **API changes:** One new operation per MR. New operations must not modify existing handler signatures. - **Frontend changes:** One component or page per MR. Design token changes require a separate MR. - **Database changes:** One migration file per MR. Migrations must be additive (no column drops, no renames). ## Validation checklist Every MR must pass `just check` locally before submission. This runs: 1. `go vet` and `golangci-lint` 2. `go test ./...` 3. `sqlc vet` (schema/query consistency) 4. `npm run build` (frontend build) 5. File size gate (no file over 500 lines of prose) --- # Architecture Go huma API, SQLite/sqlc/goose, Astro 6/Svelte 5 frontend, go-yoto client, and the design system. Four frameworks enforce correctness at compile time. Understanding them before making changes is essential — they interact, and bypassing one usually breaks another. ## The four frameworks | Layer | Tool | Source of truth | Generated output | |---|---|---|---| | API | huma v2 | Go I/O structs in `internal/api/` | OpenAPI spec | | Database | sqlc | SQL in `internal/db/queries/*.sql` | Go in `internal/db/gen/` | | Migrations | goose | SQL in `internal/db/migrations/` | Schema with rollback | | Config | koanf | `internal/config/config.go` | Validated config struct | | Frontend types | openapi-typescript | huma's generated OpenAPI spec | `frontend/src/lib/api/types.ts` | | Frontend client | openapi-fetch | Generated types | `frontend/src/lib/api/client.ts` | ## Repository layout | Directory | Purpose | |---|---| | `cmd/yotoshelf/` | CLI entrypoints: `serve`, `create-admin`, `seed`, `migrate` | | `internal/api/` | huma API operations (HTTP layer). One file per domain. | | `internal/db/` | SQLite, goose migrations, sqlc queries, and generated code | | `internal/config/` | koanf configuration struct and loader | | `internal/` | Domain packages: `auth`, `card`, `icons`, `jobs`, `labels`, and others | | `frontend/` | Astro 6 + Svelte 5 frontend; embedded into the binary via `go:embed` | | `frontend/design` | Git submodule — this site's design tokens, components, and brand assets | | `e2e/` | Playwright end-to-end tests | | `scripts/` | CI helper scripts (file size check, i18n check) | | `docs/hardware.md` | Canonical Yoto hardware specs | ## Tech stack | Layer | Technologies | |---|---| | Backend runtime | Go 1.25, CGO-free (`modernc.org/sqlite`) | | HTTP | huma v2 + chi v5 | | Database | SQLite with sqlc (type-safe queries) and goose (migrations) | | Auth | argon2id password hashing, gorilla/sessions, OIDC (PKCE S256) | | Frontend build | Astro 6, Svelte 5 (runes — not stores), Tailwind v4 | | UI components | shadcn-svelte | | Icons | Phosphor duotone only | | i18n | Paraglide JS — all user-facing strings in `frontend/messages/en.json` | | Image generation | Ideogram (recommended AI provider) | ## go-yoto sibling module The Yoto API client lives in a separate module at [gitlab.com/yotoshelf/go-yoto](https://gitlab.com/yotoshelf/go-yoto). It is a standalone library — not a subdirectory of this repository. Changes to Yoto API integration belong there; changes to how YotoShelf calls it belong in `internal/`. ## Design submodule `frontend/design` is a git submodule pointing at [yotoshelf/yotoshelf.dev](https://gitlab.com/yotoshelf/yotoshelf.dev) — this documentation site, which is also the canonical home of the design system. It provides design tokens, shared Svelte components, fonts, and brand assets. The submodule must be initialised before building the frontend: ```sh git submodule update --init ``` ## New feature workflow Every new feature follows this sequence: 1. Write the SQL query in `internal/db/queries/{domain}.sql` 2. Run `just generate` — sqlc emits type-safe Go into `internal/db/gen/` 3. Write the huma operation in `internal/api/{domain}.go` 4. Run the server — the OpenAPI spec updates automatically 5. Run `just generate` again — openapi-typescript updates `frontend/src/lib/api/types.ts` 6. Use the shared typed client in Svelte: `import { api } from '$lib/api/client'` 7. Build the UI component in Svelte 5 with typed props --- # Dev Loop Clone, install tools with mise, run just recipes, seed fixtures, and run E2E tests. ## First-time setup ```sh git clone --recurse-submodules https://gitlab.com/yotoshelf/yotoshelf.git cd yotoshelf mise install # installs Go, Node, sqlc, golangci-lint, just just dev # starts Go :8080 + Astro :4300 ``` ## just recipes ```sh just dev # Go backend + Astro frontend, hot reload just check # lint + test + build (must pass before MR) just generate # sqlc + openapi-typescript code generation just seed # populate the DB with fixture data just test # unit tests only just test-v # unit tests, verbose just e2e # Playwright end-to-end (both servers must be up) just build # production binary + embedded frontend just migrate # run pending DB migrations just migration NAME # create a new goose migration file ``` ## Fixture seeding `just seed` creates a local admin user, two collections, several sample cards, and linked Yoto account fixtures. The seed command is idempotent — running it multiple times is safe. To reset to a clean state: ```sh rm yotoshelf.db just migrate just seed ``` ## Code generation After any change to SQL queries or the huma API: ```sh just generate ``` This runs sqlc (type-safe query generation) and openapi-typescript (frontend type generation). Generated files are committed — never edit them manually. ## E2E tests Playwright E2E tests live in `e2e/`. They require both servers to be running: ```sh # Terminal 1 just dev # Terminal 2 just e2e ``` CI runs E2E against a fresh seeded database on every MR. ## Hot reload behaviour - **Go backend:** Uses `air` for live reloading. Changes to `.go` files trigger a rebuild and restart in ~1s. - **Astro frontend:** Vite HMR. Component and style changes are instant. Changes to `frontend/src/lib/api/types.ts` (generated) require a manual page refresh. --- # Conventions File-size limits, structural conformity, conventional commits, and i18n. ## File size limits No file in the repository may exceed 500 lines of prose (code, comments, and blank lines combined). This is enforced by `scripts/check-file-size.sh` in CI. Large files are a sign that a domain needs to be split. Exceptions (whitelisted in the script): - Generated files (`internal/db/gen/`, `frontend/src/lib/api/types.ts`) - Migration files - Test fixture files ## Structural conformity Before writing any new code, find an existing example of the same pattern and follow it exactly. The AGENTS.md lookup table maps each kind of change to its canonical example file. New patterns without precedent require human approval before implementation. ## Conventional commits All commits use [Conventional Commits](https://www.conventionalcommits.org/): ``` (): [optional body] ``` **Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci` **Scopes:** match the affected package or component, e.g. `cards`, `auth`, `frontend`, `db`, `ci` **Examples:** ``` feat(cards): add bulk-delete endpoint fix(auth): handle expired OIDC state parameter docs(contribute): add E2E setup instructions ``` ## i18n All user-facing strings in the frontend must be in `frontend/messages/en.json`. Hard-coded strings in Svelte components are rejected by the i18n lint check (`scripts/check-i18n.sh`). Access translated strings via: ```svelte

{m.card_title()}

``` Backend error messages are in English and do not go through the i18n system — they are developer-facing, not user-facing. ## Import ordering Go imports follow the standard three-group convention: 1. Standard library 2. External packages 3. Internal packages (`gitlab.com/yotoshelf/yotoshelf/internal/`) `goimports` (run by golangci-lint) enforces this automatically. --- # CI The yotoshelf/ci catalog components, what each gate checks, and the Renovate preset. ## Pipeline structure YotoShelf uses a GitLab CI pipeline with components from the [yotoshelf/ci](https://gitlab.com/yotoshelf/ci) catalog. The pipeline runs on every push to a merge request branch and on every push to `main`. ## Gates | Job | What it checks | |---|---| | `lint` | golangci-lint (Go), ESLint (frontend) | | `test` | `go test ./...` (unit tests) | | `build` | `go build` + `npm run build` | | `sqlc` | sqlc vet — queries match the schema | | `file-size` | No file exceeds 500 lines | | `i18n` | No hard-coded user-facing strings in Svelte | | `e2e` | Playwright end-to-end against a seeded database | | `container` | Container image builds successfully | All gates must pass before a MR can be merged. There is no manual override. ## Renovate Dependency updates are automated via Renovate. The `renovate.json` at the repository root extends the `yotoshelf/ci` preset, which: - Groups patch-level updates into a single weekly MR - Separates major version bumps for human review - Auto-merges patch updates that pass all gates - Excludes `go-yoto` from auto-merge (API-adjacent, always human-reviewed) ## Smoke tests A weekly scheduled pipeline runs the go-yoto integration tests against the real Yoto API to detect upstream drift. See [go-yoto Client](/reference/go-yoto) for details. # Section: Design --- # Design System Brand identity, design tokens, shared components, and logo assets for the YotoShelf ecosystem. YotoShelf is a power tool for parents and teachers who manage Yoto audio cards across multiple homes, classrooms, and families. The design language balances **operational density** with **emotional warmth**. ## Design direction Three references shaped the visual language: - **Linear** — precision, keyboard-first UX, crisp task states. Every click does one thing efficiently. - **Plex** — media library ownership, cover art grids. "My library, my rules." - **Are.na** — warm curation, personal collections. A digital bookshelf that reflects taste. ## Brand identity **Colors:** Yoto Orange `#F45436` as the primary accent, warm neutrals throughout. Never pure gray — everything leans warm. **Typography:** Fraunces (display serif) + DM Sans (body sans) + IBM Plex Mono. The serif/sans contrast says "curated library" — editorial warmth meets operational precision. ## What's in this section - [Colors](/design/colors) — brand, semantic, neutral, and sidebar palettes with interactive swatches - [Typography](/design/typography) — font families, type scale, weights, and specimens - [Spacing](/design/spacing) — 4px-base scale with visual bars - [Components](/design/components) — live demos of Button, Card, Badge, Input, Dialog - [Logo](/design/logo) — mark, hero banner, favicons, and usage guidelines - [Process](/design/process) — how the logo was created and how to iterate it - [Integration](/design/integration) — how to consume tokens and components in your project --- # Colors Brand, semantic, neutral, and sidebar palettes with interactive swatches. ## Primary — Yoto Orange The primary accent colour is Yoto Orange, derived from Yoto's own brand palette. | Token | Value | Usage | |---|---|---| | `--color-primary` | `hsl(14 84% 56%)` | Primary buttons, active states, links | | `--color-primary-dark` | `hsl(14 84% 46%)` | Hover state for primary elements | | `--color-primary-light` | `hsl(14 84% 66%)` | Subtle highlights on dark backgrounds | | `--color-primary-subtle` | `hsl(14 84% 96%)` | Tinted backgrounds, active sidebar items | ## Neutral — warm tones All neutrals lean warm (hue 20–25°). Never pure gray. | Token | Value | Usage | |---|---|---| | `--color-background` | `hsl(0 0% 100%)` | Page background (light mode) | | `--color-foreground` | `hsl(20 14.3% 4.1%)` | Primary text | | `--color-muted` | `hsl(60 4.8% 95.9%)` | Sidebar, table headers, code backgrounds | | `--color-muted-foreground` | `hsl(25 5.3% 44.7%)` | Secondary text, labels | | `--color-border` | `hsl(20 5.9% 90%)` | Dividers, input borders | | `--color-card` | `hsl(0 0% 100%)` | Card backgrounds | ## Semantic | Token | Usage | |---|---| | `--color-warning` | Warning callouts (amber) | | `--color-warning-foreground` | Text inside warning callouts | | `--color-warning-subtle` | Background of warning callouts | | `--color-sidebar` | Left sidebar background | ## Dark mode Dark mode tokens are defined in `global.css` under `[data-theme="dark"]`. The dark palette uses the same warm hue rotation — backgrounds are warm-dark, never blue-black. --- # Typography Font families, type scale, weights, and specimens. ## Font families | Token | Family | Usage | |---|---|---| | `--font-display` | Fraunces | Headings, brand wordmark | | `--font-body` | DM Sans | Body text, UI labels, buttons | | `--font-mono` | IBM Plex Mono | Code, badges, metadata labels | Fraunces is a display serif with optical size variation. It is used for headings only — never body text. DM Sans is a geometric sans-serif optimised for screen readability. IBM Plex Mono is IBM's open-source monospace, consistent with the technical/operational character of the product. ## Type scale | Token | Size | Usage | |---|---|---| | `--text-xs` | 0.75rem (12px) | Metadata labels, badges, captions | | `--text-sm` | 0.875rem (14px) | Body text, sidebar links | | `--text-base` | 1rem (16px) | Default body, buttons | | `--text-lg` | 1.125rem (18px) | Subheadings | | `--text-xl` | 1.25rem (20px) | Section headings (h2) | | `--text-2xl` | 1.5rem (24px) | Page headings (h1 in cards) | | `--text-4xl` | 2.25rem (36px) | DocsLayout h1 | ## Font weights | Token | Value | Usage | |---|---|---| | `--font-weight-normal` | 400 | Body text | | `--font-weight-medium` | 500 | Active nav links | | `--font-weight-semibold` | 600 | Labels, table headers | | `--font-weight-bold` | 700 | Headings | ## Line height | Token | Value | |---|---| | `--leading-tight` | 1.25 | | `--leading-snug` | 1.375 | | `--leading-relaxed` | 1.625 | --- # Spacing 4px-base scale: every spacing token is a multiple of 4px. | Token | Value | px | |---|---|---| | `--spacing-1` | 0.25rem | 4px | | `--spacing-1-5` | 0.375rem | 6px | | `--spacing-2` | 0.5rem | 8px | | `--spacing-3` | 0.75rem | 12px | | `--spacing-4` | 1rem | 16px | | `--spacing-5` | 1.25rem | 20px | | `--spacing-6` | 1.5rem | 24px | | `--spacing-8` | 2rem | 32px | | `--spacing-10` | 2.5rem | 40px | | `--spacing-12` | 3rem | 48px | | `--spacing-16` | 4rem | 64px | | `--spacing-20` | 5rem | 80px | ## Usage patterns - **Component padding:** `spacing-3` to `spacing-6` - **Section gaps:** `spacing-8` to `spacing-16` - **Page padding:** `spacing-6` horizontal, `spacing-8` to `spacing-16` vertical - **Inline gaps (icons, badges):** `spacing-1` to `spacing-2` Never use arbitrary pixel values — always use a token. If you need a value not in the scale, use the nearest token and note why. --- # Components The YotoShelf component library is built on [shadcn-svelte](https://next.shadcn-svelte.com/) with custom tokens applied. Components are available from the design system submodule at `frontend/design/`. ## Button Variants: `default` (orange primary), `secondary`, `outline`, `ghost`, `destructive`, `link`. Sizes: `default`, `sm`, `lg`, `icon`. All buttons use `--font-body` at `--text-sm` with `--font-weight-semibold`. ## Card Cards use `--color-card` background, `--color-border` border, and `--radius-xl` border radius. The hover state lifts the card with `--shadow-card-hover` and shifts the border to `--color-primary`. ## Badge Variants map to semantic colours: `default`, `secondary`, `destructive`, `outline`. Used for status labels, auth badges, and method indicators in the API reference. ## Input All form inputs use `--color-border` borders and `--color-background` fill. Focus state uses a `--color-primary` ring. Error state uses `--color-destructive`. ## Dialog Modal dialogs use a backdrop blur overlay. The dialog container uses `--color-background` and `--radius-xl`. Close button is in the top-right corner. ## Consuming components ```svelte draft ``` Components are re-exported from `frontend/src/lib/components/ui/`. Do not import directly from shadcn-svelte — always use the re-export path so token overrides apply. --- # Logo Mark, hero banner, favicons, and usage guidelines. ## The mark The YotoShelf mark is a pixel-art illustration of three cozy shelves connected by glowing network lines — a visual metaphor for "a shared library, connected". It is hand-crafted at small resolution and scaled up with `image-rendering: pixelated`. The mark is available in two variants: - `/logo/mark.svg` — compact mark, used in the topbar (24×24) - `/logo/hero.svg` — full hero illustration (520px wide), used on the landing page ## Favicon The favicon is derived from the mark at 32×32 and 16×16. Provided as: - `/favicon.svg` — SVG favicon (modern browsers) - `/favicon.ico` — ICO fallback ## Background requirement The mark is designed for dark backgrounds. The pixel art has a dark background baked in (`#1c1714`). When placed on a light background, wrap it in a container with `background: #1c1714` or use the `topbar-mark` CSS class which applies this. ## Usage guidelines - Do not recolor the mark. The orange accent is the only brand color in the pixel art. - Do not crop the mark. The surrounding pixels are part of the illustration. - Do not place the mark on a white background without the dark wrapper. - The wordmark "YotoShelf" uses Fraunces Bold. Do not use a different font. ## File locations All logo assets live in `/public/logo/` in this repository. The design system submodule (`frontend/design/`) contains the canonical source files. --- # Process How the YotoShelf logo was created and how to iterate it. ## Creation The mark was created in [Aseprite](https://www.aseprite.org/) at 32×32 pixels. The brief was: "three shelves of Yoto cards, connected by glowing network lines, warm and cozy, dark background, library feel." The final illustration went through three rounds of iteration: 1. Initial concept — shelves only, no network lines 2. Added network lines and warm glow effects 3. Refined card sizes and spacing for legibility at small sizes ## The constraint Pixel art at 32×32 forces every design decision to matter. There is no room for decorative elements that do not communicate something. The three shelves = card library; the network lines = self-hosted sharing; the warm glow = home/cozy. All three brand values in 1024 pixels. ## Iterating the mark If the mark needs to be updated: 1. Open `mark.aseprite` in Aseprite (source file in the design system repo) 2. Make changes at 32×32. Export as PNG. 3. Convert to SVG using a pixel-art-preserving tool (e.g. `png2svg` with no path simplification, or hand-trace in Inkscape) 4. Test at 24×24 (topbar) and 32×32 (hero). The mark must read clearly at both sizes. 5. Update `/public/logo/mark.svg` and regenerate the favicon. ## What not to change - The three-shelf structure — this is the core metaphor - The warm dark background — this defines the "cozy" brand feel - The orange accent — this is the Yoto brand tie-in Changes to the number of shelves or the removal of network lines would require a new design brief and full stakeholder alignment. --- # Integration How to consume YotoShelf design tokens and components in your project. The design system is published as a git submodule at `frontend/design/` within the YotoShelf repository. External projects (or a fork) can consume it the same way. ## Adding the submodule ```sh git submodule add https://gitlab.com/yotoshelf/yotoshelf.dev.git design git submodule update --init ``` ## CSS tokens Import the global token file in your root CSS or layout: ```css @import './design/src/styles/global.css'; ``` This provides all `--color-*`, `--font-*`, `--spacing-*`, `--radius-*`, and `--text-*` tokens. ## Svelte components The design system exposes shared Svelte components from `design/packages/components/`. These are pre-built interactive components (TocRibbon, Carousel, Callout) already used in this documentation site. ```svelte ``` ## Updating the submodule ```sh cd design git pull origin main cd .. git add design git commit -m "chore(design): update design system submodule" ``` Pin the submodule to a specific commit for stability in production. The design system follows semantic versioning — breaking changes will be in major releases. # Section: Reference --- # Reference Technical reference for YotoShelf — covering the HTTP API, the go-yoto client library, hardware constraints, upstream API findings, and the architectural decisions behind the project. ## Sections - [API (OpenAPI)](/reference/api) — Full HTTP endpoint reference generated from the application source via huma. Grouped by tag with method, path, summary, and auth requirement. Includes regeneration instructions. - [go-yoto Client](/reference/go-yoto) — Package overview, installation, authentication flows, full API coverage table (26 of 27 upstream endpoints), known API quirks, and the weekly live-validation smoke test workflow. - [Hardware Constraints](/reference/hardware) — Yoto device specifications relevant to card making: cover image dimensions, LED display format, audio pipeline, device types, and label printing templates. - [Yoto API Findings](/reference/yoto-api-findings) — Verified findings from direct exploration of the Yoto upstream API: endpoint behaviour, undocumented quirks (device status in config, single-use refresh tokens, userinfo scope), MYO card structure, and import feasibility. - [Architecture](/reference/architecture) — Runtime stack, auth architecture (dual OAuth2 flows, BFF pattern), data model, Go package layout, frontend architecture, and container build. - [Design Decisions](/reference/decisions) — Rationale behind key choices: local + OIDC auth, collections-centric model, SQLite as source of truth, cover vs label terminology, audio pipeline strategy, federation posture, and more. --- # Architecture Tech stack, auth architecture, data model, and key architectural rationale for YotoShelf. ## Runtime stack | Concern | Technology | Notes | |---|---|---| | HTTP server | Go + chi + huma | chi for routing; huma for OpenAPI generation and type-safe handlers | | Frontend | Astro + Svelte 5 | Astro static MPA; interactive islands use Svelte 5 | | Database | SQLite (modernc.org/sqlite) | Pure Go — no CGo, works in distroless | | Session management | gorilla/sessions CookieStore | HMAC-signed cookies; Yoto tokens stored in SQLite, not cookies | | Media processing | ffmpeg | Local audio transcoding for preview playback | | Container | Multi-stage OCI | Node (frontend) → Go (binary) → Alpine (runtime + ffmpeg) | ## Auth architecture YotoShelf has two separate auth concerns: 1. **App authentication** — who is logged in to YotoShelf. Supports both local user accounts (admin-created, argon2id passwords) and OIDC (optional, for self-hosters with an existing IdP). 2. **Yoto account linking** — OAuth2 PKCE flow to link a Yoto account, storing tokens encrypted in SQLite. The Go backend is a Backend-for-Frontend (BFF): it handles all auth and serves the static frontend. The frontend never handles tokens directly. Go middleware checks the session cookie and redirects to `/login` for unauthenticated requests. ### Dual OAuth2 flows | Concern | App login | Yoto account link | |---|---|---| | Protocol | Local password or OIDC (Auth Code) | OAuth2 Auth Code + PKCE | | Token storage | Session cookie (user ID only) | SQLite `yoto_accounts` table (encrypted at rest) | | Token lifetime | Session = configurable (default 7 days) | Access token = hours; refresh token = long-lived | | Refresh | Re-auth or OIDC refresh | Background refresh; Yoto tokens are single-use | ### Token refresh serialisation One `yoto.Client` instance per linked Yoto account, shared across all background workers via a `sync.RWMutex`-protected map. The client's internal mutex serialises refresh operations. Yoto refresh tokens are single-use — the new token is persisted atomically in a database transaction before the old one is discarded. ## Data model overview SQLite is the single source of truth. `card.yaml` is a derived export, written after every SQLite change. The web UI never reads directly from `card.yaml`; a filesystem watcher imports external edits as a power-user escape hatch. ### Key tables | Table | Purpose | |---|---| | `users` | Authenticated users. `auth_type`: `local` or `oidc`. Role: `admin` / `user` / `viewer`. | | `yoto_accounts` | Linked Yoto accounts per user. Tokens encrypted at rest. Multiple accounts per user supported. | | `collections` | Primary organising unit. Cards belong to collections. Each collection has member roles: owner, contributor, viewer. | | `cards` | MYO cards. Each card has a home collection; can be linked to others. Status: `draft` / `ready`. | | `card_tracks` | Audio tracks per card. Includes `metadata_embedded` boolean. | | `publish_records` | Per-card, per-Yoto-account publish state. Cascade-deletes with card. | | `share_links` | Time-limited label share links. HMAC token in path for aggressive rate limiting. | | `jobs` | SQLite-backed job queue. Survives restarts. Per-track retry with order preservation. | ### Collections model Collections are the primary organising concept. "All Cards" is a virtual UI filter, not a real collection. Every card must be created in an explicit collection. A card has one home collection (ownership/deletion anchor) and can be linked to additional collections. Removing from the home collection deletes the card; removing from a linked collection removes the link only. ## Go package layout | Package | Purpose | |---|---| | `cmd/` | Executable entry points: `serve`, `gen-openapi`, etc. | | `internal/api` | HTTP handlers, huma registration, middleware | | `internal/auth` | Session store, local auth, OIDC, rate limiter | | `internal/card` | Card domain logic, path manager, YAML export | | `internal/db` | SQLite open, sqlc-generated queries | | `internal/config` | Environment variable config (CLI flags > env vars > defaults) | | `internal/job` | SQLite-backed job queue and worker pool | ## Frontend architecture Astro generates a static multi-page application embedded into the Go binary at build time via `//go:embed all:frontend/dist`. The Go server serves static files with long-lived cache headers for hashed `/_astro/*` assets. Interactive components (card editor, collection browser, publish matrix) are Svelte 5 islands hydrated with `client:load`. The Tailwind + shadcn-svelte component library provides accessible UI primitives. ## Container build Three-stage OCI build: 1. **Node (frontend):** `npm ci && npm run build` → `frontend/dist/` 2. **Go (binary):** `CGO_ENABLED=0 go build` with embedded frontend 3. **Alpine (runtime):** binary + ffmpeg. Database on a mounted volume. ## Observability `GET /metrics` exposes Prometheus-format metrics behind token auth. Metrics cover card count, publish rates, storage, job queue depth, and errors. No anonymous telemetry or phone-home — adoption is tracked via container registry downloads only. Structured logging via Go `slog` (JSON format). --- # Design Decisions Rationale behind YotoShelf's key design choices. ## go-yoto: separate module The Yoto API client lives in a separate Go module at [gitlab.com/yotoshelf/go-yoto](https://gitlab.com/yotoshelf/go-yoto) rather than inside the YotoShelf monorepo. This makes it independently importable by other Go projects without pulling in YotoShelf's full dependency tree. During development, the main module references it via a `replace` directive. ## Local users + OIDC (not OIDC-only) YotoShelf supports both local user accounts (admin-created, argon2id passwords) and optional OIDC login. OIDC-only would require every self-hoster to run a separate identity provider — a significant barrier. Local accounts are the baseline; OIDC is configured when an existing IdP is available. Local users can be migrated to OIDC later via admin account association. ## Collections-centric design Collections are the primary organising concept — not a layer added on top of a flat card library. Every card must be created in a collection. Publishing is collection-aware. "All Cards" is a UI filter, not a real collection. This matches how people think about their content: "Family cards", "School", "Audiobooks". It also provides a natural access control boundary (collection-level roles) without needing per-card permissions. ## SQLite as single source of truth SQLite is the authoritative store for all UX operations. `card.yaml` is a derived export, not a primary input. This avoids sync conflicts between two authoritative sources and makes the web UI simpler. The filesystem watcher that imports external `card.yaml` edits is a power-user escape hatch, not a core workflow. Self-hosters overwhelmingly run a single node. SQLite is battle-tested, zero-infrastructure, and trivially backed up. The `modernc.org/sqlite` pure-Go driver removes the CGo dependency, which simplifies the container build and enables static linking. ## Cover vs label terminology YotoShelf uses "cover" for the 1500×1500 card face image (displayed in the app and on the Yoto device) and "label" for the printable sticker template. These are distinct concepts with different dimensions and use cases. The original Yoto app conflates them; YotoShelf does not. ## Audio pipeline ffmpeg handles transcoding locally rather than uploading the source file and letting Yoto transcode it. This gives YotoShelf control over quality settings, progress feedback, and error handling. Yoto's own transcoding pipeline has undocumented quality limits that can produce audible artifacts on long-form audio. ## Federation posture YotoShelf does not federate or sync between instances. Each instance is standalone. Sharing between instances (and with external users) is handled via share links — time-limited URLs for label PDFs and collection views. Full federation would require a discovery mechanism, trust model, and upgrade path that are out of scope for a self-hosted personal tool. ## BFF pattern (no token exposure to frontend) The Go backend acts as a Backend-for-Frontend: it owns all authentication state and the Yoto OAuth2 tokens. The Svelte frontend makes API calls through the Go server, which proxies to Yoto with the stored credentials. This means the frontend never sees a Yoto token — even if the frontend is compromised, the attacker cannot extract and reuse the Yoto credentials offline. --- # go-yoto Client [gitlab.com/yotoshelf/go-yoto](https://gitlab.com/yotoshelf/go-yoto) is a zero-dependency Go client for the Yoto API. It handles OAuth2 PKCE and device code authentication, content library access, device management, MYO card CRUD, and media uploads with SHA256 deduplication. YotoShelf uses it as its upstream API layer. ## Installation ```sh go get gitlab.com/yotoshelf/go-yoto ``` ## Quick start ```go import "gitlab.com/yotoshelf/go-yoto" client := yoto.NewClient("YOUR_CLIENT_ID", nil, nil) ``` ## Authentication The Yoto API uses Authorization Code + PKCE. Two flows are supported: ### Browser OAuth2 PKCE (web applications) ```go verifier, err := yoto.GenerateCodeVerifier() authURL := client.BuildAuthorizeURL(redirectURI, "random-state", verifier) // redirect user to authURL, receive code in callback tokens, err := client.ExchangeCode(ctx, code, redirectURI, verifier) ``` ### Device code flow (CLI / headless) ```go dcr, err := client.RequestDeviceCode(ctx) fmt.Printf("Visit %s and enter code: %s\n", dcr.VerificationURI, dcr.UserCode) tokens, err := client.PollForToken(ctx, dcr) ``` ### Token persistence Implement the `TokenStore` interface to persist tokens across restarts. A `MemoryTokenStore` is included for testing. Tokens are automatically refreshed 30 seconds before expiry. ## API coverage **26 of 27 upstream endpoints (96%)** are implemented. Coverage is verified weekly via live smoke tests. ### Devices | Upstream endpoint | Go method | Status | |---|---|---| | `GET /device-v2/devices/mine` | `ListDevices` | Working | | `GET /device-v2/{id}/config` | `GetDeviceInfo` | Working | | `PUT /device-v2/{id}/config` | `UpdateDeviceSettings` | Working | | `POST /device-v2/{id}/command/status` | `SendCommand` | Working | | `GET /device-v2/{id}/status` | `GetDeviceStatus` | Deprecated — returns 403 | ### Content | Upstream endpoint | Go method | Status | |---|---|---| | `GET /content/mine` | `ListMyCards` | Working | | `GET /content/{id}` | `GetCard` | Working | | `POST /content` | `CreateCard` / `UpdateCard` | Working | | `DELETE /content/{id}` | `DeleteCard` | Working | ## Known API behaviours - **Device status in config:** `GET /device-v2/{id}/status` returns 403. Device status is embedded in the config response. Use `GetDeviceInfo` instead. - **Single-use refresh tokens:** Yoto refresh tokens are single-use. The new token must be persisted before the old one is discarded. - **Account email via family endpoint:** `/userinfo` requires the `openid` scope (not in default scope set). Use `GetUserFamily()` to get the account email. ## Error handling ```go if errors.Is(err, yoto.ErrNotFound) {} // 404 if errors.Is(err, yoto.ErrUnauthorized) {} // 401 if errors.Is(err, yoto.ErrRateLimited) {} // 429 var apiErr *yoto.APIError if errors.As(err, &apiErr) { fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message) } ``` ## Live validation A weekly scheduled CI pipeline runs the full integration test suite against the real Yoto API to detect upstream drift. If a smoke run detects shape drift, the pipeline fails and the library is updated before the next YotoShelf release. --- # Hardware Constraints Yoto device specifications relevant to card making: cover image dimensions, LED display format, audio pipeline, device types, and label printing templates. ## Cover images | Specification | Value | |---|---| | Format | JPEG or PNG | | Dimensions | 1500×1500 px (square, 1:1) | | File size | Under 2 MB recommended | | Colour space | sRGB | YotoShelf generates cover images at 1500×1500 and converts to JPEG before upload. ## LED display (Mini) The Yoto Mini has a 16×8 pixel LED matrix display that shows per-track chapter icons. | Specification | Value | |---|---| | Format | PNG | | Canvas dimensions | 16×16 px (scaled to 16×8 on device) | | Colour | Monochrome (single colour; any colour displays as lit/unlit) | ## Audio pipeline Yoto expects AAC audio with these constraints: | Specification | Value | |---|---| | Format | AAC (MPEG-4 audio) | | Bitrate | 64 kbps | | Sample rate | 44.1 kHz | | Channels | Stereo (or mono for speech) | | Container | M4A | YotoShelf transcodes any uploaded format to this spec via ffmpeg. ## Device types | Device | Screen | LED matrix | |---|---|---| | Yoto Player (3rd gen) | Cover art display | No | | Yoto Mini | None | 16×8 LED | | Yoto Player Mini (2024) | Cover art display | No | ## Label printing YotoShelf generates printable card labels in PDF format: | Template | Sheet size | Labels per sheet | Label dimensions | |---|---|---|---| | Avery L7160 | A4 | 21 | 63.5×38.1 mm | | Avery 5160 | Letter | 30 | 2⅝×1 inch | Labels include the card cover image, title, and a QR code linking to the collection share page. --- # Yoto API Findings Verified findings from direct exploration of the Yoto upstream API. This is living documentation — it is updated when the weekly smoke test detects upstream changes. ## Endpoint overview The Yoto API is undocumented and unofficial. These findings come from network inspection, go-yoto's integration test harness, and the `yoto-explorer` tool. Base URL: `https://api.yotoplay.com` Authentication: OAuth2 Authorization Code + PKCE. Client ID is publicly available from the Yoto mobile apps. ## Undocumented quirks ### Device status embedded in config `GET /device-v2/{id}/status` returns HTTP 403. Device status — active card ID, battery level, charging state, sleep state — is embedded inside the device config response at `GET /device-v2/{id}/config`. ### Single-use refresh tokens Yoto refresh tokens are single-use. Each token exchange returns a new refresh token that replaces the previous one. If you discard the new refresh token (e.g., crash between receiving it and persisting it), you are locked out and must re-authenticate. YotoShelf persists the new token atomically in a database transaction before considering the exchange complete. ### userinfo scope not in default The OIDC `/userinfo` endpoint requires the `openid` scope, which is not in the default scope set returned during OAuth2. The standard approach of fetching the user email from `/userinfo` does not work without requesting this scope explicitly. Workaround: call `GET /user/family` instead. The primary account email is at `family.Members[0].Email`. ## MYO card structure MYO (Make Your Own) cards appear in `GET /content/mine` alongside purchased and streaming cards. Distinguish them: | Type | `config.OnlineOnly` | `editSettings.RssUrl` | Track type | |---|---|---|---| | MYO / purchased | `false` or absent | absent | `"audio"` | | Streaming / podcast | `true` | present | `"stream"` | Track audio is served as signed S3 URLs (AAC format). Request with `GetCardPlayable` to get valid signed URLs. ## Import feasibility Importing existing MYO cards from a Yoto account into YotoShelf is feasible: - Metadata (title, cover URL, track list, chapter icons) is available from the content API. - Audio files are served as signed S3 URLs and can be downloaded. - Cover images are served as CDN URLs and can be downloaded. - Chapter icons (display icons) are served as CDN URLs. YotoShelf's import feature (`POST /api/import`) uses this flow. ## Pagination Content list endpoints (`/content/mine`) return all cards in a single response — there is no server-side pagination. For large libraries this can be a slow response. Client-side pagination is applied by YotoShelf. ## Rate limits Rate limits are not documented. In practice, the API handles several requests per second without issue. The `go-yoto` client includes exponential backoff on `429` responses. --- # API Reference HTTP API reference for YotoShelf — 153 endpoints across 14 groups. Generated from the OpenAPI spec produced by huma. Source: https://yotoshelf.dev/openapi.yaml ## Authentication Auth model (derived — not declared in spec): - `public` — no session required (login, OAuth callbacks, health checks) - `session` — active session cookie required - `admin` — session required with admin role, or token auth (metrics) ## Groups - **admin**: User management, audit log, job control, and system operations. Requires admin role. - **auth**: Login, logout, OIDC flows, password reset, and email verification. - **cards**: Create, edit, publish, and delete MYO audio cards. - **collections**: Organise cards into shared collections with role-based access. - **devices**: Linked Yoto player devices. - **icons**: AI-generated card icon management and regeneration. - **import**: Import cards from a linked Yoto account. - **jobs**: Background job queue status. - **labels**: Printable card label generation and sharing. - **publish**: Publish cards to linked Yoto accounts. - **recording**: Audio recording and track management. - **settings**: LLM provider and application settings. Requires admin role. - **tts**: Text-to-speech audio generation. - **yoto**: Yoto account linking and OAuth callbacks. ## Admin User management, audit log, job control, and system operations. Requires admin role. ### `GET /admin/audit-log` [admin] View audit log **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `user_id` | query | `string` | | | | `action` | query | `string` | | | | `limit` | query | `integer` | | | | `offset` | query | `integer` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `GET /admin/deletion-requests` [admin] List pending deletion requests **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /admin/deletion-requests/{id}/approve` [admin] Approve a deletion request **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (ApproveDeletionInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `reassignments` | `object` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/deletion-requests/{id}/reject` [admin] Reject a deletion request **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /admin/health` [admin] Admin comprehensive health report **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | AdminHealthOutputBody | ### `GET /admin/jobs` [admin] List all jobs with optional status/type filters **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `status` | query | `string` | | | | `type` | query | `string` | | | | `limit` | query | `integer` | | | | `offset` | query | `integer` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `DELETE /admin/jobs/{id}` [admin] Delete a job by ID **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /admin/orphaned-collections` [admin] List orphaned collections **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /admin/orphaned-collections/{id}/assign` [admin] Assign an orphaned collection to a user **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (AssignOrphanedInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `user_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/rotate-key` [admin] Rotate the encryption key for stored tokens **Request body (RotateKeyInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `old_key` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | MessageOutputBody | ### `POST /admin/seed-examples` [admin] Seed example cards for first-run experience **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | SeedExampleOutputBody | ### `GET /admin/seed-status` [admin] Check whether example content has been seeded **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | SeedStatusOutputBody | ### `GET /admin/users` [admin] List all users **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /admin/users` [admin] Create a new user **Request body (AdminCreateUserInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `display_name` | `string` | | | | `email` | `string (email)` | yes | | | `password` | `string` | yes | | | `role` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | AdminUserOutputBody | ### `DELETE /admin/users/{id}` [admin] Directly delete a user **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (AdminDeleteUserInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `reassignments` | `object` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /admin/users/{id}` [admin] Update user role or display name **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (AdminUpdateUserInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `display_name` | `string` | yes | | | `role` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | AdminUserOutputBody | ### `POST /admin/users/{id}/deactivate` [admin] Deactivate a user account **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/users/{id}/reactivate` [admin] Reactivate a deactivated user **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/users/{id}/resend-verification` [admin] Resend email verification to a user **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | MessageOutputBody | ### `POST /admin/users/{id}/reset-password` [admin] Admin-initiated password reset **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | AdminResetPasswordOutputBody | ### `POST /admin/users/{id}/revoke-sessions` [admin] Revoke all sessions for a user **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/users/{id}/unlock` [admin] Unlock a locked user account **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/users/{id}/verify-email` [admin] Manually verify a user's email **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /health` [public] Public health check **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | HealthOutputBody | ### `POST /library/rescan` [session] Trigger a full library rescan **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | MessageOutputBody | ### `GET /metrics` [admin] Basic system metrics **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | MetricsOutputBody | ### `GET /ready` [public] Readiness check — pings database **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | ReadyOutputBody | ## Auth Login, logout, OIDC flows, password reset, and email verification. ### `POST /auth/delete-request` [session] Request account deletion **Request body (DeletionRequestInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `reason` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | DeletionRequestOutputBody | ### `POST /auth/forgot-password` [public] Request a password reset email **Request body (ForgotPasswordInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `email` | `string (email)` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | ForgotPasswordOutputBody | ### `POST /auth/login` [public] Login with email and password **Request body (LoginInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `email` | `string (email)` | yes | User email | | `password` | `string` | yes | User password | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | UserOut | ### `POST /auth/logout` [public] Logout and clear session **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /auth/me` [session] Get current authenticated user **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | UserOut | ### `POST /auth/oidc/backchannel-logout` [public] OIDC backchannel logout (provider-initiated) **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `GET /auth/oidc/callback` [public] OIDC callback (public) **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /auth/oidc/login` [public] Initiate OIDC login flow **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /auth/oidc/unlink` [session] Unlink OIDC SSO from account **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /auth/password` [session] Change password (requires current password) **Request body (ChangePasswordInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `current_password` | `string` | yes | | | `new_password` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | ChangePasswordOutputBody | ### `PATCH /auth/profile` [session] Update display name **Request body (UpdateProfileInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `display_name` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | UpdateProfileOutputBody | ### `POST /auth/reset-password` [public] Reset password using a token **Request body (ResetPasswordInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `new_password` | `string` | yes | | | `token` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | ResetPasswordOutputBody | ### `POST /auth/sessions/revoke` [session] Revoke all sessions (log out everywhere) **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /auth/verify-email` [public] Verify email address using a token **Request body (VerifyEmailInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `token` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | VerifyEmailOutputBody | ## Cards Create, edit, publish, and delete MYO audio cards. ### `GET /cards` [session] List all cards across all collections **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `DELETE /cards/{slug}` [session] Delete a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /cards/{slug}` [session] Get a card by slug with its tracks **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | CardDetailOut | ### `PUT /cards/{slug}` [session] Update a card's title and description **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (UpdateCardInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `author` | `string` | | | | `description` | `string` | yes | | | `title` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /cards/{slug}/cover` [session] Serve card cover image **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `side` | query | `string` | | Cover side | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /cards/{slug}/cover` [session] Upload card cover image (multipart) **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `side` | query | `string` | | Cover side | **Request body:** _No structured fields (see spec for raw schema)._ **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/cover/accept-preview` [session] Promote a generated cover preview to the final cover **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (AcceptCoverPreviewRequest):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `side` | `string` | | Cover side | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GenerateCoverOutputBody | ### `POST /cards/{slug}/cover/generate` [session] Generate a preview cover image (does not commit) **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (GenerateCoverRequest):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `prompt` | `string` | yes | Description of the desired cover image | | `side` | `string` | | Cover side | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GenerateCoverOutputBody | ### `GET /cards/{slug}/cover/preview` [session] Serve the pending cover preview **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `side` | query | `string` | | Cover side | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `PUT /cards/{slug}/home` [session] Move a card to a different home collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (MoveCardHomeInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `collection_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/merge` [session] Merge multiple tracks into one **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (MergeTracksInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `title` | `string` | | | | `track_slugs` | `array | null` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | JobIDOutputBody | ### `GET /cards/{slug}/playback` [session] Get playback configuration for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | PlaybackConfigOut | ### `PUT /cards/{slug}/playback` [session] Update playback configuration for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (UpdatePlaybackConfigInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `autoadvance` | `string` | yes | | | `overlay_timeout` | `integer (int64)` | yes | | | `resume_timeout` | `integer (int64)` | yes | | | `shuffle` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /cards/{slug}/slug` [session] Rename a card's slug **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (RenameSlugInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `slug` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /cards/{slug}/sources` [session] List source material tracks for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `DELETE /cards/{slug}/sources/{track_slug}` [session] Purge a source material track (deletes file and DB record) **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/tracks` [session] Upload an audio track (multipart) **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body:** _No structured fields (see spec for raw schema)._ **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | TrackOut | ### `DELETE /cards/{slug}/tracks/{track_slug}` [session] Delete a track from a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /cards/{slug}/tracks/{track_slug}` [session] Get a track by slug **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | TrackDetailOut | ### `GET /cards/{slug}/tracks/{track_slug}/audio` [session] Serve track audio file with Range support **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `PUT /cards/{slug}/tracks/{track_slug}/description` [session] Update a track's description **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (UpdateTrackDescriptionInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `description` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/tracks/{track_slug}/fade` [session] Apply fade in/out to a track **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (FadeTrackInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `fade_in_ms` | `integer (int64)` | yes | | | `fade_out_ms` | `integer (int64)` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | JobIDOutputBody | ### `PUT /cards/{slug}/tracks/{track_slug}/icon-prompt` [session] Update a track's icon prompt **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (UpdateTrackIconPromptInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `icon_prompt` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /cards/{slug}/tracks/{track_slug}/slug` [session] Rename a track's slug **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (RenameTrackSlugInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `slug` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/tracks/{track_slug}/split` [session] Split a track at marker points **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (SplitTrackInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `split_points` | `array | null` | yes | | | `titles` | `array | null` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | JobIDOutputBody | ### `PUT /cards/{slug}/tracks/{track_slug}/title` [session] Update a track's title and auto-update slug **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (UpdateTrackTitleInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `title` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | UpdateTrackTitleOutputBody | ### `POST /cards/{slug}/tracks/{track_slug}/trim` [session] Trim a track to a selection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `track_slug` | path | `string` | yes | | **Request body (TrimTrackInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `end` | `number (double)` | yes | | | `start` | `number (double)` | yes | | | `title` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | JobIDOutputBody | ### `PUT /cards/{slug}/tracks/reorder` [session] Reorder tracks on a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (ReorderTracksInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `positions` | `array | null` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /cards/{slug}/tracks/slug-available` [session] Check if a track slug is available within a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `slug` | query | `string` | yes | | | `track_id` | query | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | TrackSlugAvailableOutputBody | ### `POST /cards/{slug}/tracks/stream` [session] Add a streaming track to a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (AddStreamTrackInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `stream_url` | `string` | yes | | | `title` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | TrackOut | ### `POST /cards/{slug}/tracks/tts` [session] Generate a TTS audio track from text **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (TTSGenerateInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `instructions` | `string` | | | | `position` | `integer (int64)` | | | | `text` | `string` | yes | | | `title` | `string` | | | | `voice` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | TrackOut | ### `GET /cards/slug-available` [session] Check if a card slug is available **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | query | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | SlugAvailableOutputBody | ### `GET /collections/{collectionID}/cards` [session] List cards in a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `collectionID` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /collections/{collectionID}/cards` [session] Create a new card in a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `collectionID` | path | `string` | yes | | **Request body (CreateCardInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `author` | `string` | | | | `description` | `string` | yes | | | `slug` | `string` | | | | `title` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | CardOut | ## Collections Organise cards into shared collections with role-based access. ### `GET /collections` [session] List collections for the authenticated user **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /collections` [session] Create a new collection **Request body (CreateCollectionInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `description` | `string` | yes | | | `name` | `string` | yes | | | `slug` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | CollectionOut | ### `DELETE /collections/{id}` [session] Delete a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | | `cascade` | query | `boolean` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /collections/{id}` [session] Get a collection by ID **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GetCollectionOutputBody | ### `PUT /collections/{id}` [session] Update a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (UpdateCollectionInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `description` | `string` | yes | | | `name` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /collections/{id}/background` [session] Serve collection background image **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /collections/{id}/background` [session] Upload collection background image (multipart) **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body:** _No structured fields (see spec for raw schema)._ **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `DELETE /collections/{id}/cards/{cardId}/link` [session] Unlink a card from a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | | `cardId` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /collections/{id}/cards/link` [session] Link a card to a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (LinkCardInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `card_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /collections/{id}/members` [session] List members of a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /collections/{id}/members` [session] Add a member to a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (AddMemberInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `role` | `string` | yes | | | `user_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `DELETE /collections/{id}/members/{userId}` [session] Remove a member from a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | | `userId` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /collections/{id}/members/{userId}` [session] Update a member's role in a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | | `userId` | path | `string` | yes | | **Request body (UpdateMemberInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `role` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /collections/{id}/presets` [session] List generation presets for a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /collections/{id}/presets` [session] Create a generation preset **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (CreatePresetInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `duration_min` | `integer (int64)` | yes | | | `emoji` | `string` | yes | | | `name` | `string` | yes | | | `prompt_template` | `string` | yes | | | `sort_order` | `integer (int64)` | yes | | | `vocab_level` | `string` | yes | | | `voice_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | PresetOut | ### `DELETE /collections/{id}/presets/{preset_id}` [session] Delete a generation preset **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | | `preset_id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /collections/{id}/presets/{preset_id}` [session] Update a generation preset **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | | `preset_id` | path | `string` | yes | | **Request body (UpdatePresetInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `duration_min` | `integer (int64)` | yes | | | `emoji` | `string` | yes | | | `name` | `string` | yes | | | `prompt_template` | `string` | yes | | | `sort_order` | `integer (int64)` | yes | | | `vocab_level` | `string` | yes | | | `voice_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /collections/{id}/profile` [session] Get generation profile for a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | CollectionProfileOut | ### `PATCH /collections/{id}/profile` [session] Update generation profile for a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (UpdateCollectionProfileInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `avoid_themes` | `string` | yes | | | `child_age` | `integer | null (int64)` | yes | | | `child_name` | `string` | yes | | | `duration_min` | `integer (int64)` | yes | | | `friends` | `string` | yes | | | `interests` | `string` | yes | | | `notes` | `string` | yes | | | `pets` | `string` | yes | | | `prompt_template` | `string` | yes | | | `vocab_level` | `string` | yes | | | `voice_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /collections/{id}/set-default` [session] Set a collection as the default for Quick Record **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /collections/{id}/slug` [session] Rename a collection's slug **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (RenameCollectionSlugInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `slug` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /collections/{id}/stats` [session] Get stats for a collection **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | CollectionStatsOutputBody | ### `GET /collections/by-slug/{slug}` [session] Get a collection by slug with its cards **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GetCollectionBySlugOutputBody | ### `POST /collections/default` [session] Get or create the default collection for Quick Record **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | CollectionOut | ### `GET /collections/slug-available` [session] Check if a collection slug is available **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | query | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | SlugAvailableOutputBody | ## Devices Linked Yoto player devices. ### `GET /devices` [session] List all devices across all linked Yoto accounts **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | ListDevicesOutputBody | ### `GET /devices/{accountID}/{deviceID}/cache-status` [session] Get device cache status from Yoto **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `accountID` | path | `string` | yes | | | `deviceID` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | CacheStatusOut | ### `POST /devices/{accountID}/{deviceID}/command` [session] Send a command to a device **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `accountID` | path | `string` | yes | | | `deviceID` | path | `string` | yes | | **Request body (SendCommandInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `card_id` | `string` | | | | `command` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | MessageOutputBody | ### `GET /devices/{accountID}/{deviceID}/info` [session] Get full device info including status and settings **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `accountID` | path | `string` | yes | | | `deviceID` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | InfoSnapshot | ### `GET /devices/{accountID}/{deviceID}/published` [session] List cards published to this device's account **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `accountID` | path | `string` | yes | | | `deviceID` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | PublishedCardsOutputBody | ### `PUT /devices/{accountID}/{deviceID}/settings` [session] Update device settings **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `accountID` | path | `string` | yes | | | `deviceID` | path | `string` | yes | | **Request body (UpdateSettingsInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `clock_face` | `string` | | | | `day_brightness` | `string` | | | | `day_max_volume` | `string` | | | | `day_time` | `string` | | | | `night_brightness` | `string` | | | | `night_max_volume` | `string` | | | | `night_time` | `string` | | | | `shutdown_timeout_min` | `string` | | | | `timezone` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | MessageOutputBody | ### `PUT /devices/{accountID}/{deviceID}/shortcuts` [session] Update device shortcut button assignments **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `accountID` | path | `string` | yes | | | `deviceID` | path | `string` | yes | | **Request body (UpdateShortcutsInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `shortcuts` | `array | null` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | MessageOutputBody | ### `POST /seed` [session] Seed content to Yoto devices **Request body (SeedContentInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `account_id` | `string` | yes | | | `card_ids` | `array | null` | yes | | | `collection_id` | `string` | yes | | | `device_ids` | `array | null` | yes | | | `schedule_at` | `string | null` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | SeedJobOut | ## Icons AI-generated card icon management and regeneration. ### `GET /cards/{slug}/icons/{position}` [session] Serve 16×16 track icon PNG **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `GET /cards/{slug}/icons/{position}-full` [session] Serve full-resolution track illustration **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `GET /cards/{slug}/icons/{position}/preview/{index}` [session] Serve an icon preview PNG **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | | `index` | path | `integer` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /cards/{slug}/icons/{position}/regenerate` [session] Regenerate a single icon for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GenerateIconsOutputBody | ### `POST /cards/{slug}/icons/{position}/select` [session] Select an icon preview as the final icon **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | **Request body (SelectPreviewInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `index` | `integer (int64)` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/icons/{position}/variations` [session] Generate N icon variations for preview **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | **Request body (GenerateVariationsInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `count` | `integer (int64)` | yes | | | `prompt` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | VariationsOutputBody | ### `POST /cards/{slug}/icons/generate` [session] Batch generate icons for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GenerateIconsOutputBody | ### `POST /cards/{slug}/icons/sample` [session] Generate icon samples for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | GenerateIconsOutputBody | ### `POST /cards/{slug}/icons/theme` [session] Suggest an icon theme for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | SuggestThemeOutputBody | ### `PUT /cards/{slug}/icons/theme` [session] Approve an icon theme for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (ApproveThemeInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `colors` | `array | null` | yes | | | `palette_name` | `string` | yes | | | `style` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /cards/{slug}/tracks/{position}/icon` [session] Upload a custom icon for a track position **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | | `position` | path | `integer` | yes | | **Request body:** _No structured fields (see spec for raw schema)._ **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ## Import Import cards from a linked Yoto account. ### `POST /cards/import-from-yoto` [session] Import a MYO card from a linked Yoto account (async) **Request body (ImportFromYotoInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `account_id` | `string` | yes | | | `yoto_card_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | ImportFromYotoOutputBody | ### `GET /yoto-accounts/{id}/library` [session] List MYO cards from a linked Yoto account **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ## Jobs Background job queue status. ### `GET /jobs/{id}` [session] Get a single job by ID **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | JobOut | ## Labels Printable card label generation and sharing. ### `GET /labels/{filename}` [session] Download a generated label file **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `filename` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /labels/{id}/share` [session] Create a share link for a label **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | LabelShareOut | ### `POST /labels/generate` [session] Generate print-ready label PDF/SVG **Request body (GenerateLabelsRequest):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `card_slugs` | `array | null` | yes | | | `format` | `string` | yes | | | `page_size` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | GenerateLabelsOutputBody | ### `DELETE /shares` [session] Revoke all label share links for the authenticated user **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /shares` [session] List label share links for the authenticated user **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `DELETE /shares/{id}` [session] Revoke a label share link **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ## Publish Publish cards to linked Yoto accounts. ### `GET /cards/{slug}/publish-status` [session] Get publish status for a card **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `GET /jobs` [session] List background jobs **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /publish` [session] Publish cards to Yoto accounts **Request body (PublishCardsInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `account_ids` | `array | null` | yes | | | `card_slugs` | `array | null` | yes | | | `collection_id` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `202` | Accepted | | ### `POST /publish/verify` [session] Verify published cards against Yoto accounts **Request body (VerifyPublishInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `account_ids` | `array | null` | yes | | | `card_slugs` | `array | null` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | VerifyPublishOutputBody | ## Recording Audio recording and track management. ### `POST /cards/{slug}/tracks/record-start` [session] Start a recording session with ffmpeg stdin pipe **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `slug` | path | `string` | yes | | **Request body (RecordStartInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `title` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | RecordStartOutputBody | ### `POST /recording/{session_id}/abandon` [session] Abandon a recording session **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `session_id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /recording/{session_id}/chunk` [session] Write a chunk to the recording ffmpeg stdin pipe **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `session_id` | path | `string` | yes | | **Request body:** _No structured fields (see spec for raw schema)._ **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /recording/{session_id}/finish` [session] Finish recording: close pipe, transcode FLAC to M4A, create track **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `session_id` | path | `string` | yes | | **Request body (RecordFinishBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `title` | `string` | | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | RecordFinishOutputBody | ## Settings LLM provider and application settings. Requires admin role. ### `GET /admin/llm-providers` [admin] List system LLM providers **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /admin/llm-providers` [admin] Create a system LLM provider **Request body (CreateProviderInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `api_key` | `string` | yes | | | `budget_usd` | `number (double)` | yes | | | `enabled` | `boolean` | yes | | | `model` | `string` | yes | | | `monthly_budget_cents` | `integer (int64)` | | | | `priority` | `integer (int64)` | yes | | | `provider` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | LLMProviderOut | ### `DELETE /admin/llm-providers/{id}` [admin] Delete a system LLM provider **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /admin/llm-providers/{id}` [admin] Update a system LLM provider **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (UpdateProviderInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `api_key` | `string` | | | | `budget_usd` | `number (double)` | yes | | | `enabled` | `boolean` | yes | | | `model` | `string` | yes | | | `monthly_budget_cents` | `integer (int64)` | | | | `priority` | `integer (int64)` | yes | | | `provider` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /admin/llm-providers/test` [admin] Test an LLM provider API key **Request body (TestProviderInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `api_key` | `string` | yes | | | `provider` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | TestProviderOutputBody | ### `GET /settings/llm-providers` [session] List user LLM providers **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `POST /settings/llm-providers` [session] Create a user LLM provider **Request body (CreateProviderInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `api_key` | `string` | yes | | | `budget_usd` | `number (double)` | yes | | | `enabled` | `boolean` | yes | | | `model` | `string` | yes | | | `monthly_budget_cents` | `integer (int64)` | | | | `priority` | `integer (int64)` | yes | | | `provider` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `201` | Created | LLMProviderOut | ### `DELETE /settings/llm-providers/{id}` [session] Delete a user LLM provider **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `PUT /settings/llm-providers/{id}` [session] Update a user LLM provider **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Request body (UpdateProviderInputBody):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `api_key` | `string` | | | | `budget_usd` | `number (double)` | yes | | | `enabled` | `boolean` | yes | | | `model` | `string` | yes | | | `monthly_budget_cents` | `integer (int64)` | | | | `priority` | `integer (int64)` | yes | | | `provider` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `POST /settings/llm-providers/{id}/test` [session] Test a user LLM provider API key **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | TestProviderOutputBody | ### `GET /settings/llm-providers/cascade` [session] Get resolved LLM provider cascade for user **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ## Tts Text-to-speech audio generation. ### `POST /tts/preview` [session] Preview a TTS voice with a short text sample **Request body (PreviewTTSVoiceRequest):** | Field | Type | Req | Description | | --- | --- | --- | --- | | `instructions` | `string` | | | | `text` | `string` | yes | | | `voice` | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ### `GET /tts/voices` [session] List available TTS voices **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | | ## Yoto Yoto account linking and OAuth callbacks. ### `GET /yoto-accounts` [session] List linked Yoto accounts **Responses:** | Code | Description | Schema | | --- | --- | --- | | `200` | OK | ListYotoAccountsOutputBody | ### `DELETE /yoto-accounts/{id}` [session] Unlink a Yoto account **Parameters:** | Name | In | Type | Req | Description | | --- | --- | --- | --- | --- | | `id` | path | `string` | yes | | **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /yoto-accounts/callback` [public] Yoto OAuth callback (public) **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | | ### `GET /yoto-accounts/link` [session] Initiate Yoto OAuth link flow **Responses:** | Code | Description | Schema | | --- | --- | --- | | `204` | No Content | |