{"components": {"schemas": {"Asker": {"description": "Who asked, as the space is allowed to show them.\n\nReflects consent rather than the stored row: a question can hold a name the\nasker did not agree to publish, and this reports what may be used.", "properties": {"display_name": {"description": "What a page would call them \u2014 a name if they consented to one being shown, otherwise the space's word for an unnamed asker.", "title": "Display Name", "type": "string"}, "is_identified": {"description": "Whether they are named at all. False means the display name is a placeholder, not a person.", "title": "Is Identified", "type": "boolean"}, "location": {"default": "", "description": "Only if they consented to it being shown.", "title": "Location", "type": "string"}}, "required": ["display_name", "is_identified"], "title": "Asker", "type": "object"}, "BrandingOut": {"description": "The space's resolved appearance \u2014 defaults with its overrides applied.\n\nResolved rather than raw, because the raw config is mostly absent for a\nspace that has changed nothing, and an agent reading it would conclude the\nspace has no colours rather than that it has the default ones.", "properties": {"colors": {"additionalProperties": true, "description": "Hex values by role: bg, surface, text, muted, border, accent, accentText, error.", "title": "Colors", "type": "object"}, "colors_dark": {"additionalProperties": true, "description": "The same roles for dark mode. Used when theme_mode is dark, or auto on a device that prefers it.", "title": "Colors Dark", "type": "object"}, "copy": {"additionalProperties": true, "description": "The words the widget uses \u2014 including anonymousName, what an unnamed asker is called.", "title": "Copy", "type": "object"}, "font": {"additionalProperties": true, "description": "family (the body typeface), displayFamily (headings, falling back to family when empty), url (a stylesheet to load) and scale (a multiplier on the base size).", "title": "Font", "type": "object"}, "radius": {"description": "Corner radius in px: 0, 8, 16 or 999.", "title": "Radius", "type": "integer"}, "show": {"additionalProperties": true, "description": "Per-surface switches. These can only ever withhold: turning askerName off hides every name, and turning it on cannot reveal one the asker withheld.", "title": "Show", "type": "object"}, "theme_mode": {"description": "light | dark | auto.", "title": "Theme Mode", "type": "string"}}, "required": ["theme_mode", "colors", "colors_dark", "radius", "font", "copy", "show"], "title": "BrandingOut", "type": "object"}, "BrandingPatch": {"additionalProperties": false, "description": "Every field optional; only what is sent is changed.\n\nValues are validated here, on the way in. The console does not share a\nvalidator to borrow \u2014 it does its own check on the branding form, and\n`tenancy/config.css_variables` re-checks at render and falls back to a\ndefault for anything that does not look like what it claims to be. That\nrender-time check is the real protection, and it is right for a page that\nmust not break; it is wrong for an API, which would answer 200 while\nstoring a value it will never render.\n\nThe colour rule is `tenancy.config._HEX`, the one the renderer uses, so\nwhat is accepted here is exactly what will survive rendering. Note it\nadmits three-digit hex, which the console's own form does not.", "properties": {"colors": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "default": null, "description": "Partial: merged over what is set.", "title": "Colors"}, "colors_dark": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "default": null, "description": "Partial: merged over what is set.", "title": "Colors Dark"}, "copy": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "default": null, "description": "Partial: merged.", "title": "Copy"}, "font": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "default": null, "description": "Partial: merged. Keys are family, displayFamily, url and scale \u2014 an unrecognised key is refused rather than stored, because a key nothing reads is a 200 that changes nothing.", "title": "Font"}, "radius": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "0, 8, 16 or 999.", "title": "Radius"}, "show": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "default": null, "description": "Partial: merged.", "title": "Show"}, "theme_mode": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "light | dark | auto.", "title": "Theme Mode"}}, "title": "BrandingPatch", "type": "object"}, "Counts": {"description": "Every question is in exactly one of these, and none of them overlap.\n\nAn earlier version counted `awaiting_answer` as \"has no answer row\", which\nquietly included spam and archived questions \u2014 so the number an agent was\ntold was waiting for a human was larger than the inbox a human would see.\nAnswered-but-unpublished fell into no bucket at all.", "properties": {"archived": {"description": "Set aside deliberately. Not waiting on anyone.", "title": "Archived", "type": "integer"}, "awaiting_answer": {"description": "Questions a human still has to answer. Matches the inbox count in the console. Excludes archived and spam.", "title": "Awaiting Answer", "type": "integer"}, "drafted": {"description": "Answered but not yet published \u2014 nothing public exists yet.", "title": "Drafted", "type": "integer"}, "published": {"description": "Answered and published as an episode.", "title": "Published", "type": "integer"}, "spam": {"description": "Marked spam. Never counts as waiting.", "title": "Spam", "type": "integer"}}, "required": ["awaiting_answer", "drafted", "published", "archived", "spam"], "title": "Counts", "type": "object"}, "EpisodeDetail": {"properties": {"answer_transcript": {"default": "", "description": "What was answered, verbatim. The content asset: this is the text that gets indexed and searched.", "title": "Answer Transcript", "type": "string"}, "asker": {"$ref": "#/components/schemas/Asker", "description": "Who asked, subject to their consent."}, "audio_url": {"default": "", "description": "The episode audio. A signed URL that expires \u2014 fetch it when you need it rather than storing it.", "title": "Audio Url", "type": "string"}, "duration_sec": {"description": "Runtime of the spliced episode.", "title": "Duration Sec", "type": "integer"}, "image_url": {"default": "", "description": "Generated key art, 1200x630. Also signed.", "title": "Image Url", "type": "string"}, "published_at": {"description": "When it went public. UTC, ISO 8601. Ordering is on this.", "format": "date-time", "title": "Published At", "type": "string"}, "question_transcript": {"default": "", "description": "What was asked, verbatim.", "title": "Question Transcript", "type": "string"}, "slug": {"description": "Stable once published. The URL segment of its page.", "title": "Slug", "type": "string"}, "summary": {"default": "", "description": "The written answer.", "title": "Summary", "type": "string"}, "teaser": {"default": "", "description": "One line, for a list row.", "title": "Teaser", "type": "string"}, "themes": {"description": "Theme slugs.", "items": {"type": "string"}, "title": "Themes", "type": "array"}, "title": {"description": "The episode's headline, as shown on its page.", "title": "Title", "type": "string"}, "url": {"description": "Absolute, on this space's canonical host \u2014 which is the custom domain once one is verified, so build links from this rather than assembling them.", "title": "Url", "type": "string"}}, "required": ["slug", "title", "published_at", "url", "duration_sec", "themes", "asker"], "title": "EpisodeDetail", "type": "object"}, "EpisodeOut": {"description": "A published answer. Only published ones appear \u2014 a draft has no public\nURL and nothing to link to.", "properties": {"asker": {"$ref": "#/components/schemas/Asker", "description": "Who asked, subject to their consent."}, "duration_sec": {"description": "Runtime of the spliced episode.", "title": "Duration Sec", "type": "integer"}, "published_at": {"description": "When it went public. UTC, ISO 8601. Ordering is on this.", "format": "date-time", "title": "Published At", "type": "string"}, "slug": {"description": "Stable once published. The URL segment of its page.", "title": "Slug", "type": "string"}, "teaser": {"default": "", "description": "One line, for a list row.", "title": "Teaser", "type": "string"}, "themes": {"description": "Theme slugs.", "items": {"type": "string"}, "title": "Themes", "type": "array"}, "title": {"description": "The episode's headline, as shown on its page.", "title": "Title", "type": "string"}, "url": {"description": "Absolute, on this space's canonical host \u2014 which is the custom domain once one is verified, so build links from this rather than assembling them.", "title": "Url", "type": "string"}}, "required": ["slug", "title", "published_at", "url", "duration_sec", "themes", "asker"], "title": "EpisodeOut", "type": "object"}, "EpisodeThemesOut": {"properties": {"themes": {"description": "Theme slugs now on this episode.", "items": {"type": "string"}, "title": "Themes", "type": "array"}}, "required": ["themes"], "title": "EpisodeThemesOut", "type": "object"}, "EpisodeThemesPatch": {"additionalProperties": false, "properties": {"themes": {"description": "Theme names or slugs, replacing whatever the episode has. Matched case-insensitively against existing themes and created when new, so filing under `retirement` lands on an existing `Retirement` rather than making a second one. Send the whole list, not a delta.", "items": {"type": "string"}, "title": "Themes", "type": "array"}}, "required": ["themes"], "title": "EpisodeThemesPatch", "type": "object"}, "EpisodesOut": {"properties": {"episodes": {"items": {"$ref": "#/components/schemas/EpisodeOut"}, "title": "Episodes", "type": "array"}, "limit": {"description": "How many were returned at most.", "title": "Limit", "type": "integer"}, "offset": {"description": "How many were skipped.", "title": "Offset", "type": "integer"}, "total": {"description": "Matching rows, ignoring limit and offset.", "title": "Total", "type": "integer"}}, "required": ["total", "limit", "offset", "episodes"], "title": "EpisodesOut", "type": "object"}, "Error": {"properties": {"error": {"$ref": "#/components/schemas/ErrorBody"}}, "required": ["error"], "title": "ErrorOut", "type": "object"}, "ErrorBody": {"properties": {"code": {"description": "Stable, machine-readable. Branch on this.", "title": "Code", "type": "string"}, "message": {"description": "Human-readable. Do not parse.", "title": "Message", "type": "string"}}, "required": ["code", "message"], "title": "ErrorBody", "type": "object"}, "FeaturedOut": {"properties": {"featured": {"description": "Episode slugs, in the order they appear. First is first.", "items": {"type": "string"}, "title": "Featured", "type": "array"}}, "required": ["featured"], "title": "FeaturedOut", "type": "object"}, "FeaturedPatch": {"additionalProperties": false, "properties": {"featured": {"description": "Episode slugs, in the order wanted. Replaces the list entirely \u2014 send the whole order, not a delta. Every slug must be a published episode of this space.", "items": {"type": "string"}, "title": "Featured", "type": "array"}}, "required": ["featured"], "title": "FeaturedPatch", "type": "object"}, "Hosts": {"description": "Where a space's public pages actually live.\n\nTwo hostnames, and which is canonical moves the day a custom domain\nverifies. A caller building links must use `canonical`, not `platform` \u2014\nthat is the whole reason both are here rather than one.", "properties": {"base_url": {"description": "https:// + canonical. Build links from this.", "title": "Base Url", "type": "string"}, "canonical": {"description": "The hostname public URLs use. Equals platform until a custom domain is verified, then becomes that domain.", "title": "Canonical", "type": "string"}, "platform": {"description": "Always-available hostname, <slug>.openmic.dev. Never changes.", "title": "Platform", "type": "string"}}, "required": ["platform", "canonical", "base_url"], "title": "Hosts", "type": "object"}, "PublishRequest": {"additionalProperties": false, "properties": {"dry_run": {"default": false, "description": "Report what publishing would do \u2014 readiness and how many people would be emailed \u2014 and do none of it.", "title": "Dry Run", "type": "boolean"}, "notify": {"default": true, "description": "Send the emails. False publishes silently, which is the right choice for a backfill and the wrong one for a new episode.", "title": "Notify", "type": "boolean"}}, "title": "PublishRequest", "type": "object"}, "PublishResult": {"properties": {"already_published": {"description": "It was already live. Publishing again changes nothing and sends nothing \u2014 the original timestamp stands.", "title": "Already Published", "type": "boolean"}, "blocked_by": {"description": "Everything preventing publication, all at once rather than one per attempt. Empty means it is ready.", "items": {"type": "string"}, "title": "Blocked By", "type": "array"}, "dry_run": {"description": "Whether this was a rehearsal.", "title": "Dry Run", "type": "boolean"}, "published": {"description": "Whether the episode is now live. False on a dry run, and false when something blocks it.", "title": "Published", "type": "boolean"}, "url": {"default": "", "description": "Where it is published, once it is.", "title": "Url", "type": "string"}, "would_email": {"description": "How many people a real publish would mail: the asker, plus verified followers, minus the asker if they are both. Zero on a space that may not send mail yet.", "title": "Would Email", "type": "integer"}}, "required": ["published", "dry_run", "already_published", "blocked_by", "would_email"], "title": "PublishResult", "type": "object"}, "QuestionOut": {"properties": {"asker": {"$ref": "#/components/schemas/Asker", "description": "Who asked, subject to their consent."}, "consent_publish_audio": {"description": "Whether their voice may be used in an episode. Read-only everywhere: no interface can grant it on somebody's behalf.", "title": "Consent Publish Audio", "type": "boolean"}, "created_at": {"description": "When it arrived. UTC, ISO 8601.", "format": "date-time", "title": "Created At", "type": "string"}, "duration_sec": {"description": "Length of the recording. 0 for a typed question.", "title": "Duration Sec", "type": "integer"}, "id": {"description": "Stable identifier. Use it with /v1/questions/{id}.", "title": "Id", "type": "integer"}, "is_typed": {"description": "True when they typed rather than recorded. There is no audio to play and the transcript is what they wrote.", "title": "Is Typed", "type": "boolean"}, "status": {"description": "new | answered | published | archived | spam. `new` is the inbox \u2014 the ones waiting for a human.", "title": "Status", "type": "string"}, "transcript": {"description": "What they said or wrote. The thing worth reading to decide whether to answer \u2014 triage happens here, not by listening.", "title": "Transcript", "type": "string"}}, "required": ["id", "status", "created_at", "asker", "duration_sec", "is_typed", "transcript", "consent_publish_audio"], "title": "QuestionOut", "type": "object"}, "QuestionsOut": {"properties": {"limit": {"description": "How many were returned at most.", "title": "Limit", "type": "integer"}, "offset": {"description": "How many were skipped.", "title": "Offset", "type": "integer"}, "questions": {"items": {"$ref": "#/components/schemas/QuestionOut"}, "title": "Questions", "type": "array"}, "total": {"description": "Matching rows, ignoring limit and offset.", "title": "Total", "type": "integer"}}, "required": ["total", "limit", "offset", "questions"], "title": "QuestionsOut", "type": "object"}, "SpaceOut": {"description": "The space a key belongs to. The first call any integration makes.", "properties": {"counts": {"$ref": "#/components/schemas/Counts"}, "hosts": {"$ref": "#/components/schemas/Hosts"}, "is_live": {"description": "Whether the space is claimed and serving. A space that is not live renders nothing and accepts no questions.", "title": "Is Live", "type": "boolean"}, "name": {"description": "Display name, as shown to visitors.", "title": "Name", "type": "string"}, "scopes": {"description": "What the calling credential may do, lowest first: read, write, admin. Ranked, so `write` implies `read` and both are listed.", "items": {"type": "string"}, "title": "Scopes", "type": "array"}, "slug": {"description": "Unique, URL-safe identifier for this space.", "title": "Slug", "type": "string"}}, "required": ["slug", "name", "is_live", "hosts", "counts", "scopes"], "title": "SpaceOut", "type": "object"}, "ThemeOut": {"properties": {"description": {"default": "", "description": "The space's own one-line description, if it set one.", "title": "Description", "type": "string"}, "name": {"description": "What the space calls this theme.", "title": "Name", "type": "string"}, "published_count": {"description": "Published episodes carrying this theme.", "title": "Published Count", "type": "integer"}, "slug": {"description": "Stable identifier. Filter episodes with it.", "title": "Slug", "type": "string"}}, "required": ["slug", "name", "published_count"], "title": "ThemeOut", "type": "object"}, "ThemesOut": {"description": "Paged like every other list. It was the one that was not, while the spec\nand the docs page both said all of them are \u2014 a caller writing one paging\nloop against the documented contract would have found `total` missing.", "properties": {"limit": {"description": "How many were returned at most.", "title": "Limit", "type": "integer"}, "offset": {"description": "How many were skipped.", "title": "Offset", "type": "integer"}, "themes": {"items": {"$ref": "#/components/schemas/ThemeOut"}, "title": "Themes", "type": "array"}, "total": {"description": "Matching rows, ignoring limit and offset.", "title": "Total", "type": "integer"}}, "required": ["total", "limit", "offset", "themes"], "title": "ThemesOut", "type": "object"}}, "securitySchemes": {"bearerAuth": {"description": "A secret key from the console's Keys page, or an OAuth access token. Both arrive the same way; the key belongs to a space and the token belongs to a person.", "scheme": "bearer", "type": "http"}, "oauth2": {"description": "For a client acting on somebody's behalf. The person signs in, chooses one space, and the token carries no more than their membership allows. Clients register themselves at /o/register/ (RFC 7591); nothing has to be arranged in advance.", "flows": {"authorizationCode": {"authorizationUrl": "https://openmic.dev/o/authorize/", "refreshUrl": "https://openmic.dev/o/token/", "scopes": {"openmic:admin": "Everything the console restricts to an owner", "openmic:read": "Read questions, episodes, themes and branding", "openmic:write": "Answer, publish, and change branding and themes"}, "tokenUrl": "https://openmic.dev/o/token/"}}, "type": "oauth2"}}}, "info": {"description": "Everything the console can do, for a server or an agent.\n\n## Authentication\n\nTwo kinds of credential, both sent the same way:\n\n    Authorization: Bearer sk_...\n\nA **secret key**, created in the console under **Keys** \u2014 shown once, scoped\n(`read` < `write` < `admin`), revocable, and belonging to exactly one space\nwhich it can never reach beyond. Right for a server with nobody at a keyboard.\n\nOr an **OAuth access token**, for a client acting on somebody's behalf. Clients\nregister themselves at `/o/register/` (RFC 7591) and start from the\n`WWW-Authenticate` header on a `401`; the person signs in, chooses one space,\nand the token carries no more than their own membership allows. They can\nwithdraw it from the same **Keys** page.\n\nThe `pk_` key in your embed snippet is a different thing: it is public by\ndesign, it only tells a browser where to point the widget, and it does not\nauthenticate here.\n\n## Errors\n\nEvery error has the same shape, including 404 and 405:\n\n    {\"error\": {\"code\": \"invalid_credential\", \"message\": \"That key is not valid.\"}}\n\nBranch on `code`. `message` is for a human reading logs.\n\n## Paging\n\nLists take `limit` (max 100) and `offset`, and return `total` \u2014 the number of\nmatching rows, not the number on this page. Values out of range are clamped\nrather than refused, so a paging loop cannot be broken by asking for too much.\n", "title": "Open Mic API", "version": "1.0.0"}, "openapi": "3.1.0", "paths": {"/v1/branding": {"get": {"description": "Defaults with the space's overrides applied, not the raw config \u2014 which is mostly absent for a space that has changed nothing.", "operationId": "getBranding", "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BrandingOut"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "The space's resolved appearance", "tags": ["Open Mic"]}, "patch": {"description": "Only what is sent changes. Colours are validated on the way in, not at render.", "operationId": "updateBranding", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BrandingPatch"}}}, "required": false}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BrandingOut"}}}, "description": "Success"}, "400": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "A colour, mode or radius that is not one."}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks `write`, or the body tried to set consent."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "Change appearance", "tags": ["Open Mic"]}}, "/v1/episodes": {"get": {"description": "Drafts are not episodes: they have no public URL.", "operationId": "listEpisodes", "parameters": [{"description": "Theme slug.", "in": "query", "name": "theme", "required": false, "schema": {"type": "string"}}, {"description": "Max 100. Default 25.", "in": "query", "name": "limit", "required": false, "schema": {"type": "integer"}}, {"description": "Rows to skip.", "in": "query", "name": "offset", "required": false, "schema": {"type": "integer"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodesOut"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "Published episodes, newest first", "tags": ["Open Mic"]}}, "/v1/episodes/{slug}": {"get": {"operationId": "getEpisode", "parameters": [{"description": "The episode's slug.", "in": "path", "name": "slug", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodeDetail"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "One episode, with both transcripts", "tags": ["Open Mic"]}}, "/v1/episodes/{slug}/publish": {"post": {"description": "A real publish emails the asker and every verified follower. `dry_run` runs the same readiness check and counts the same recipients, and sends nothing.", "operationId": "publishEpisode", "parameters": [{"description": "The episode's slug.", "in": "path", "name": "slug", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishRequest"}}}, "required": false}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublishResult"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks `write`, or the body tried to set consent."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "Publish an episode, or rehearse it", "tags": ["Open Mic"]}}, "/v1/episodes/{slug}/themes": {"patch": {"description": "Replaces the episode's themes. Names are matched case-insensitively against existing themes and created when new, so the same theme does not appear twice under different spellings.", "operationId": "setEpisodeThemes", "parameters": [{"description": "The episode's slug.", "in": "path", "name": "slug", "required": true, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodeThemesPatch"}}}, "required": true}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodeThemesOut"}}}, "description": "Success"}, "400": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "More themes than the per-episode maximum."}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'write' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "File an episode under themes", "tags": ["Open Mic"]}}, "/v1/featured": {"get": {"operationId": "getFeatured", "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FeaturedOut"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "The featured episodes, in order", "tags": ["Open Mic"]}, "patch": {"description": "Replaces the list entirely \u2014 send the whole order, not a delta.", "operationId": "updateFeatured", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FeaturedPatch"}}}, "required": true}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FeaturedOut"}}}, "description": "Success"}, "400": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "A slug that is not a published episode of this space."}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'write' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "Set the featured list and its order", "tags": ["Open Mic"]}}, "/v1/questions": {"get": {"description": "The inbox. Filter to `status=new` for the ones waiting on a human.", "operationId": "listQuestions", "parameters": [{"description": "new | answered | published | archived | spam", "in": "query", "name": "status", "required": false, "schema": {"type": "string"}}, {"description": "Max 100. Default 25.", "in": "query", "name": "limit", "required": false, "schema": {"type": "integer"}}, {"description": "Rows to skip.", "in": "query", "name": "offset", "required": false, "schema": {"type": "integer"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionsOut"}}}, "description": "Success"}, "400": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "`status` is not one of the five."}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "Questions, newest first", "tags": ["Open Mic"]}}, "/v1/questions/{question_id}": {"get": {"operationId": "getQuestion", "parameters": [{"description": "The question's id.", "in": "path", "name": "question_id", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuestionOut"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "One question", "tags": ["Open Mic"]}}, "/v1/space": {"get": {"description": "The cheapest first call: confirms the credential works and says which hostname to build links from.", "operationId": "getSpace", "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpaceOut"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "The space this key belongs to", "tags": ["Open Mic"]}}, "/v1/themes": {"get": {"operationId": "listThemes", "parameters": [{"description": "Max 100. Default 25.", "in": "query", "name": "limit", "required": false, "schema": {"type": "integer"}}, {"description": "Rows to skip.", "in": "query", "name": "offset", "required": false, "schema": {"type": "integer"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThemesOut"}}}, "description": "Success"}, "401": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Missing, unknown or revoked key."}, "403": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "The key lacks the 'read' scope."}, "404": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "No such path, or no such row."}, "405": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}, "description": "Wrong method for this path."}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "summary": "Themes, in the space's own order", "tags": ["Open Mic"]}}}, "security": [{"bearerAuth": []}, {"oauth2": ["openmic:read"]}], "servers": [{"url": "https://openmic.dev"}]}