{"openapi":"3.0.3","info":{"title":"Novacast Podcast Data API","version":"1.0.0","description":"Search and read the Novacast podcast catalogue. Authenticate with `Authorization: Bearer <key>`. Limits per key: a per-minute rate over a 60 second sliding window (X-RateLimit-Minute-Limit; when exceeded, 429 'Per-minute rate limit exceeded' with a fixed Retry-After of 60 plus the daily and credit headers) and a daily quota that resets at 00:00 UTC (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset as a Unix timestamp); X-RateLimit-Minute-Remaining counts down within the current minute. Every request, including a rejected one, counts toward the per-minute limit; a rejected (400) request is refunded on the daily quota and on credits. Credits: one credit per served request, refunded on a 400; X-Credits-Remaining on every response, X-Credits-Reset (Unix timestamp of the next monthly allowance) when the key has one; at zero credits the API answers 429 with a `topUp` link in the body; buy credits with /billing/bundles and /billing/checkout, which never spend credits. Sandbox keys get a small weekly credit allowance (reset Monday 00:00 UTC, no carry-over), smaller search pages (20 x 3) and a lower per-minute rate; a key's first bundle purchase makes it a standard key (50 x 20 pages), and the remaining sandbox allowance is replaced by the purchased credits (25 left + a 5,000 bundle = 5,000). An unknown key and a revoked key both get the same 403 by design. itunesId is present where the catalogue has it. Developers sign up at /app/auth/developer and manage keys and credits at /app/profile/api. Fields: pciScore is the podcast quality score from 0 to 100; listenersLow/listenersHigh is the estimated audience range; userRatingValue/userRatingCount are null when the show has no ratings. Host contact details are never returned; free-text descriptions are the feed's own text and may contain addresses the publisher put there. Similar podcasts may be empty for shows that have no similarity embedding yet (about half the catalogue)."},"servers":[{"url":"https://novacast.ai/api/v1"}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer"}},"schemas":{"Podcast":{"type":"object","description":"A podcast as returned by search and similar. Host contact details are never included.","properties":{"uuid":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true,"description":"Feed description, may contain HTML"},"imageUrl":{"type":"string","nullable":true},"authorName":{"type":"string","nullable":true},"language":{"type":"string","nullable":true},"datePublished":{"type":"string","nullable":true},"websiteUrl":{"type":"string","nullable":true},"genres":{"type":"array","items":{"type":"string"},"description":"Genre codes such as PODCASTSERIES_TRUE_CRIME"},"pciScore":{"type":"number","nullable":true,"description":"Podcast quality score, 0 to 100"},"listenersLow":{"type":"integer","nullable":true,"description":"Estimated audience range, null when unknown"},"listenersHigh":{"type":"integer","nullable":true},"userRatingValue":{"type":"number","nullable":true,"description":"null when the show has no ratings"},"userRatingCount":{"type":"integer","nullable":true},"itunesId":{"type":"integer","nullable":true,"description":"Present where the catalogue has it, null otherwise"},"hosts":{"type":"array","items":{"$ref":"#/components/schemas/Host"},"description":"Search only; hosts who claimed the show on Novacast (public profiles), no contact details. Empty when unclaimed; authorName is the feed's own author"},"topPodcast":{"type":"integer","description":"Search only; position in the top-podcasts ranking, 0 when unranked"},"hasGuests":{"type":"boolean","nullable":true,"description":"Search only"},"ageDemographics":{"type":"string","nullable":true,"description":"Search only; audience age band such as 35-44"},"genderRatio":{"type":"string","nullable":true,"description":"Search only; such as mostly-male"},"incomeLevel":{"type":"string","nullable":true,"description":"Search only; such as medium-high"},"socialLinks":{"type":"object","description":"Search only; platform links, currently null","properties":{"spotify":{"type":"string","nullable":true},"applePodcasts":{"type":"string","nullable":true},"youtube":{"type":"string","nullable":true},"googlePodcasts":{"type":"string","nullable":true}}},"similarity":{"type":"number","description":"Similar only; cosine similarity between 0 and 1, higher is closer; results are sorted highest first"}}},"Host":{"type":"object","properties":{"hostName":{"type":"string"},"username":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true}}},"PodcastDetail":{"type":"object","description":"A podcast as returned by the detail route. Host contact details are never included.","properties":{"hosts":{"type":"array","items":{"$ref":"#/components/schemas/Host"},"description":"Hosts who claimed the show on Novacast (public profiles), no contact details; empty when unclaimed"},"uuid":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true,"description":"Feed description, may contain HTML"},"imageUrl":{"type":"string","nullable":true},"authorName":{"type":"string","nullable":true},"rssOwnerName":{"type":"string","nullable":true},"language":{"type":"string","nullable":true},"itunesId":{"type":"integer","nullable":true,"description":"Present where the catalogue has it, null otherwise"},"datePublished":{"type":"string","nullable":true},"websiteUrl":{"type":"string","nullable":true},"genres":{"type":"array","items":{"type":"string"}},"pciScore":{"type":"number","nullable":true,"description":"Podcast quality score, 0 to 100; the same value search returns"},"listeners":{"type":"object","nullable":true,"properties":{"low":{"type":"integer","nullable":true},"high":{"type":"integer","nullable":true},"estimated":{"type":"integer","nullable":true}}},"rating":{"type":"object","description":"Both values are null when the show has no ratings","properties":{"userRatingValue":{"type":"number","nullable":true},"userRatingCount":{"type":"integer","nullable":true}}},"demographics":{"type":"object","nullable":true,"properties":{"hasGuests":{"type":"boolean","nullable":true},"ageDemographic":{"type":"string","nullable":true},"genderRatio":{"type":"string","nullable":true},"incomeLevel":{"type":"string","nullable":true}}},"evaluation":{"type":"object","nullable":true,"properties":{"totalEpisodes":{"type":"integer","nullable":true},"lastEpisodeDate":{"type":"string","nullable":true,"description":"Publication date of the newest episode"}}},"episodes":{"type":"array","items":{"$ref":"#/components/schemas/Episode"},"description":"Latest episodes, newest first; omitted when episodes=0"}}},"Episode":{"type":"object","properties":{"uuid":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"datePublished":{"type":"string","nullable":true},"imageUrl":{"type":"string","nullable":true},"audioUrl":{"type":"string","nullable":true},"duration":{"type":"integer","nullable":true,"description":"seconds"},"episodeNumber":{"type":"integer","nullable":true},"seasonNumber":{"type":"integer","nullable":true},"websiteUrl":{"type":"string","nullable":true}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"object","nullable":true}}}}},"security":[{"apiKey":[]}],"paths":{"/podcasts/search":{"get":{"summary":"Semantic podcast search","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","maxLength":200}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"maximum":20,"default":1},"description":"At most 20 pages per query term"},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},{"name":"language","in":"query","schema":{"type":"string","pattern":"^[A-Za-z]{2,40}$"},"description":"Language name as stored in the catalogue, any case: english, spanish, german, french, chinese, portuguese, italian, japanese and others. Filters within the semantic matches."},{"name":"mode","in":"query","schema":{"type":"string","enum":["hybrid","semantic"]},"description":"Relevance engine. hybrid fuses the semantic match with a keyword match and a show-name match, so a query that is a podcast's name returns that podcast first; semantic is the vector match alone. When absent the server default applies (meta.mode reports which ran). With another sort it picks the relevant matches that sort orders; meta.total always counts semantic matches."},{"name":"sort","in":"query","schema":{"type":"string","enum":["relevance","new","recent-publication","pci","rating"],"default":"relevance"},"description":"Order of the matched podcasts: relevance (closest match first), new (date published), recent-publication (latest episode), pci (quality score), rating (user rating). Non-relevance sorts order the 50 most relevant matches (the relevance ranking of the chosen mode), so their results stay on topic and pages end after 50 results; meta.total still counts every match. A podcast missing the sort value (no rating, no date) comes last."}],"responses":{"200":{"description":"Ranked podcasts with audience, quality and demographic signals; host names only","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Podcast"}},"meta":{"type":"object","properties":{"total":{"type":"integer","nullable":true},"page":{"type":"integer"},"pageSize":{"type":"integer"},"sort":{"type":"string"},"mode":{"type":"string","description":"hybrid or semantic: the relevance engine that ranked (or, for another sort, picked) the results"},"language":{"type":"string","nullable":true,"description":"The applied language filter, upper-cased"},"isLastPage":{"type":"boolean","nullable":true},"requestMs":{"type":"integer"}}}}}}}},"400":{"description":"Invalid query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing key"},"403":{"description":"Invalid or revoked key (the same answer for both, by design)"},"429":{"description":"Per-minute rate limit, daily quota or credits exceeded"}}}},"/podcasts/{uuid}":{"get":{"summary":"Podcast detail","parameters":[{"name":"uuid","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"episodes","in":"query","schema":{"type":"integer","minimum":0,"maximum":100,"default":10}}],"responses":{"200":{"description":"Podcast with genres, listeners, demographics, rating, evaluation and latest episodes","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PodcastDetail"}}}}}},"400":{"description":"Invalid uuid or query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/podcasts/{uuid}/episodes":{"get":{"summary":"Episodes, newest first","parameters":[{"name":"uuid","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Episode page, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Episode"}},"meta":{"type":"object","properties":{"page":{"type":"integer"},"pageSize":{"type":"integer"},"isLastPage":{"type":"boolean"}}}}}}}}}}},"/billing/bundles":{"get":{"summary":"Credit bundles for sale (free call)","description":"Does not spend quota or credits. Prices are set in Stripe; amounts are in the smallest currency unit.","responses":{"200":{"description":"Bundles, cheapest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"credits":{"type":"integer"},"amount":{"type":"integer","description":"cents"},"currency":{"type":"string"}}}}}}}}}}}},"/billing/checkout":{"post":{"summary":"Stripe Checkout link for a bundle (free call)","description":"Open the returned url in a browser; credits land on the key when the payment completes (X-Credits-Remaining rises on the next call). A sandbox key becomes a standard key on its first purchase.","parameters":[{"name":"bundle","in":"query","required":true,"schema":{"type":"string"},"description":"A bundle id from /billing/bundles"}],"responses":{"200":{"description":"Checkout link","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string"},"expiresAt":{"type":"integer","description":"Unix timestamp"},"bundle":{"type":"object"}}}}}}}},"400":{"description":"Unknown bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/podcasts/{uuid}/similar":{"get":{"summary":"Similar podcasts by embedding","parameters":[{"name":"uuid","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":20,"default":4}}],"responses":{"200":{"description":"Similar podcasts; data is empty when the source show has no similarity embedding","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Podcast"}},"meta":{"type":"object","properties":{"limit":{"type":"integer"},"podcastUuid":{"type":"string","format":"uuid"}}}}}}}}}}}}}