{"openapi":"3.1.0","info":{"title":"Fablesh API","version":"2.0.0","description":"Catalog, playlists, podcasts, uploads, distribution and the Creator Hub. Public reads are cached for 30 seconds; writes require authentication."},"servers":[{"url":"https://api.fablesh.com","description":"Production API"}],"tags":[{"name":"Health"},{"name":"Catalog"},{"name":"Playback"},{"name":"Stations"},{"name":"Podcasts"},{"name":"Media"},{"name":"Creator Hub"},{"name":"Uploads"},{"name":"Artwork"},{"name":"Account"},{"name":"Operations"}],"paths":{"/health":{"get":{"tags":["Health"],"summary":"Health","description":"API, storage and database status.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/artists":{"get":{"tags":["Catalog"],"summary":"List artists","description":"All catalog artists. Filter with ?q=. Slim rows without track arrays.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/artists/{slug}":{"get":{"tags":["Catalog"],"summary":"Get artist","description":"One artist with bio, image, Spotify link and all releases.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"slug identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/releases":{"get":{"tags":["Catalog"],"summary":"List releases","description":"All releases. Filter with ?type=single|ep|album, ?artist={slug}, ?q=.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/releases/{id}":{"get":{"tags":["Catalog"],"summary":"Get release","description":"One release: tracks, artwork, UPC, label, release date, Spotify link.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"patch":{"tags":["Catalog"],"summary":"Update release","description":"Title, type, label, release date, UPC, genres, artwork URL.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}},"delete":{"tags":["Catalog"],"summary":"Delete release","description":"Removes a release and its tracks. Storage files are kept.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/tracks":{"get":{"tags":["Catalog"],"summary":"List tracks","description":"All tracks. Filter with ?artist=, ?release=, ?q= (matches ISRC too).","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/tracks/{id}":{"get":{"tags":["Catalog"],"summary":"Get track","description":"One track: audio URL, ISRC, duration, play count, Spotify link.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"patch":{"tags":["Catalog"],"summary":"Update track","description":"Title, track number, ISRC, duration.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}},"delete":{"tags":["Catalog"],"summary":"Delete track","description":"Removes a track; empty releases are removed too.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/summary":{"get":{"tags":["Catalog"],"summary":"Catalog summary","description":"Everything in one payload: { artists, releases, tracks }. Aliases: /api/public/catalog/summary, /api/v1/catalog/summary.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/artist-names":{"get":{"tags":["Catalog"],"summary":"Artist names","description":"Lightweight autocomplete list: name, slug, trackCount.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/playlists/auto":{"get":{"tags":["Catalog"],"summary":"Auto playlists","description":"Algorithmic playlists that grow by themselves: most played, radio favourites, fresh drops, hidden gems, genre mixes, daily mix.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/catalog/playlists/auto/{id}":{"get":{"tags":["Catalog"],"summary":"Get auto playlist","description":"One auto playlist with its current tracks.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/tracks/{id}/play":{"post":{"tags":["Playback"],"summary":"Count a play","description":"Increments the play counter. Called automatically by the web player.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/radio/stations":{"get":{"tags":["Stations"],"summary":"List all stations","description":"Both broadcast stations and the algorithmic playlists, marked kind: live | playlist.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/radio/live":{"get":{"tags":["Stations"],"summary":"Live now playing","description":"Now playing, listeners and recent history for Radio Uppsala and Radio Roslagen. Normalised, so no AzuraCast call needed.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/radio/stations/{id}":{"get":{"tags":["Stations"],"summary":"Get station queue","description":"The station's tracks, spread so no artist repeats back to back. Order is stable for a day. ?limit= up to 200.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/podcasts":{"get":{"tags":["Podcasts"],"summary":"List podcasts","description":"All public shows.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"post":{"tags":["Podcasts"],"summary":"Create podcast","description":"Creates a show.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/podcasts/{id}":{"get":{"tags":["Podcasts"],"summary":"Get podcast","description":"One show with metadata and artwork.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/podcasts/{id}/episodes":{"get":{"tags":["Podcasts"],"summary":"List episodes","description":"Episodes for a show.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"post":{"tags":["Podcasts"],"summary":"Create episode","description":"Adds an episode. Audio via …/audio/upload-url, then …/process and …/publish.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/rss/{slug}.xml":{"get":{"tags":["Podcasts"],"summary":"Podcast RSS","description":"Public RSS feed per show, ready for podcast apps.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"slug identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/assets":{"get":{"tags":["Media"],"summary":"List assets","description":"Uploaded media assets with metadata and processing status.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}}},"/api/assets/{id}":{"get":{"tags":["Media"],"summary":"Get asset","description":"One asset including variants and metadata.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"delete":{"tags":["Media"],"summary":"Delete asset","description":"Removes the record — audio files stay in storage.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/feed":{"get":{"tags":["Creator Hub"],"summary":"Creator feed","description":"The Creator Hub feed. Public read; posting requires sign-in.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"post":{"tags":["Creator Hub"],"summary":"Publish post","description":"Publishes a feed post. Like via /{id}/like, reply via /{id}/comments.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/collaborators":{"get":{"tags":["Creator Hub"],"summary":"Collaborator directory","description":"Published collaborator profiles.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"post":{"tags":["Creator Hub"],"summary":"Publish collaborator profile","description":"Creates or updates your directory profile.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/marketplace":{"get":{"tags":["Creator Hub"],"summary":"Marketplace","description":"Services offered by creators.","responses":{"200":{"description":"Success"},"404":{"description":"Not found"}}},"post":{"tags":["Creator Hub"],"summary":"Publish service","description":"Puts a service up for sale.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/assets/direct-upload-url":{"post":{"tags":["Uploads"],"summary":"Direct upload URL","description":"Presigned PUT URL for direct-to-R2 audio upload.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/assets/complete-direct-upload":{"post":{"tags":["Uploads"],"summary":"Complete direct upload","description":"Registers a finished upload and queues processing.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/import":{"post":{"tags":["Uploads"],"summary":"Import from URL","description":"Imports audio from a source URL.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/images/upload":{"post":{"tags":["Uploads"],"summary":"Upload image","description":"Multipart image upload (artwork, artist photos) to R2.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/files/upload-url":{"post":{"tags":["Uploads"],"summary":"Project file upload URL","description":"Presigned PUT URL for Creator Hub project files (audio, artwork, PDF…).","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/artists/{id}":{"patch":{"tags":["Catalog"],"summary":"Update artist","description":"Name and image.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/dedupe":{"post":{"tags":["Catalog"],"summary":"Deduplicate catalog","description":"Removes uploaded duplicates of catalog tracks. Dry run by default; { \"apply\": true } deletes.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/tracks/{id}/attach-audio":{"post":{"tags":["Catalog"],"summary":"Attach audio","description":"Attaches an uploaded audio file to a catalog track.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/artwork/auto":{"post":{"tags":["Artwork"],"summary":"Generate or fetch cover art","description":"Finds a cover on Deezer, reuses release art, or generates one with AI. Returns 202 and applies it when done.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/assets/{id}/metadata":{"patch":{"tags":["Media"],"summary":"Update asset metadata","description":"Title, artist, release, ISRC, artwork URL and more.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/assets/{id}/transcode":{"post":{"tags":["Media"],"summary":"Transcode asset","description":"Queues processing for uploaded media.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/assets/{id}/publish/{destination}":{"post":{"tags":["Media"],"summary":"Publish asset","description":"Queues distribution, e.g. destination radio-uppsala.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"},{"name":"destination","in":"path","required":true,"schema":{"type":"string"},"description":"destination identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/releases/{id}/distribute":{"post":{"tags":["Creator Hub"],"summary":"Distribute release","description":"Queues radio distribution for a release's tracks.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"id identifier"}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/podcasts/import/rss":{"post":{"tags":["Podcasts"],"summary":"Import podcast from RSS","description":"Imports an existing show — episodes and artwork included; optional ongoing sync.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/podcasts/artwork/backfill":{"post":{"tags":["Podcasts"],"summary":"Backfill episode artwork","description":"Re-reads the source feeds and pulls in per-episode images that were missing.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/podcasts/artwork/consolidate":{"post":{"tags":["Podcasts"],"summary":"Consolidate podcast artwork","description":"Moves show and episode covers into the image bucket. ?apply=true to write.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/podcasts/audio/purge-hosted":{"post":{"tags":["Podcasts"],"summary":"Purge hosted podcast audio","description":"Removes audio we used to host; episodes link to the publisher's own file. ?apply=true to write.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/artwork/mirror":{"post":{"tags":["Artwork"],"summary":"Mirror artwork into our bucket","description":"Pulls album, single, release, artist and track art hosted elsewhere into fablesh-images. Runs automatically every 30 min; ?apply=true forces a full pass.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/catalog/artwork/migrate-legacy":{"post":{"tags":["Artwork"],"summary":"Migrate legacy artwork","description":"Fetches artwork still hosted on the retired Lovable storage. ?apply=true to write.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/projects":{"get":{"tags":["Creator Hub"],"summary":"List projects","description":"Your Creator Hub projects. Create with POST; update/delete on /{id}.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/notifications":{"get":{"tags":["Creator Hub"],"summary":"Notifications","description":"Radio plays, finished renders, requests. Mark read via POST …/read.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/radio/plays":{"get":{"tags":["Creator Hub"],"summary":"Radio play statistics","description":"Totals, per station, top tracks and recent plays of your catalog.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/requests":{"post":{"tags":["Creator Hub"],"summary":"Send request","description":"Sends an inquiry to another creator. GET lists sent and received.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/hub/studio/events":{"get":{"tags":["Creator Hub"],"summary":"Studio live events","description":"Job feed over Server-Sent Events. Token as ?token= query parameter.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/me/export":{"get":{"tags":["Account"],"summary":"Export my data","description":"GDPR export of your account, roles and playlists as JSON.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/me":{"delete":{"tags":["Account"],"summary":"Delete my account","description":"Permanently removes your account, playlists and roles.","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}},"/api/audit":{"get":{"tags":["Operations"],"summary":"Audit log","description":"Trail of all write operations (who, what, when, outcome).","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid credentials"},"404":{"description":"Not found"}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Fablesh sign-in token; admin endpoints require the admin role."},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Server-to-server alternative for admin endpoints."}}}}