{"openapi":"3.1.0","info":{"title":"zPod API","version":"1.144.1","summary":"HTTP API for shows, episodes, media, publish, webhooks, and directory listen links.","description":"Bearer tokens from Studio Account → Developer / API Tokens. Create stays draft. Publish, schedule, and unpublish require the opt-in publish scope. Webhook endpoints require opt-in webhooks:write. Official MCP is hosted at https://zpod.cc/mcp (Bearer token) and also ships as the client-run @zpod/mcp stdio wrapper; both call this HTTP API. Canonical feed URLs stay on rss.podfora.com."},"servers":[{"url":"https://zpod.cc","description":"Production"}],"tags":[{"name":"Account"},{"name":"Shows"},{"name":"Custom Domains"},{"name":"Episodes"},{"name":"Media"},{"name":"Publish"},{"name":"Archive Episodes"},{"name":"Attach"},{"name":"Webhooks"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API token","description":"Authorization: Bearer zpod_live_…"}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"}}},"Show":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"},"feedId":{"type":"string"},"feedUrl":{"type":"string","description":"Canonical https://rss.podfora.com/{slug}-{feedId}, or the admin vanity path when set"},"description":{"type":"string"},"ownerEmail":{"type":"string","description":"Optional itunes:email. Empty omits the tag."},"copyright":{"type":"string","description":"Optional RSS copyright. Empty omits the tag."},"author":{"type":"string"},"language":{"type":"string"},"explicit":{"type":"boolean"},"medium":{"type":"string"},"timeZone":{"type":"string"},"value":{"allOf":[{"$ref":"#/components/schemas/ShowValue"}],"nullable":true,"description":"The show-level Value 4 Value block (`podcast:value`). Returned by GET and PATCH `/api/v1/shows/{showId}`, not by the show list. Null when the feed leaves it out."}}},"ShowValue":{"type":"object","required":["type","method","recipients"],"description":"Podcasting 2.0 `podcast:value` at show level. Episode-level value is not supported. The feed writes it only with at least one recipient.","properties":{"type":{"type":"string","description":"Short lowercase name. Default `lightning`."},"method":{"type":"string","description":"Short lowercase name such as `keysend`. Default `keysend`."},"suggested":{"type":"string","nullable":true,"description":"Optional decimal, such as `0.00000005000` (BTC for Lightning)."},"recipients":{"type":"array","maxItems":24,"items":{"type":"object","required":["name","type","address","split"],"properties":{"name":{"type":"string"},"type":{"type":"string","enum":["node","lnaddress"]},"address":{"type":"string","description":"66-character node public key for `node`; name@example.com for `lnaddress`."},"split":{"type":"integer","minimum":1,"description":"Shares, a whole number of 1 or more."},"customKey":{"type":"string","nullable":true},"customValue":{"type":"string","nullable":true},"fee":{"type":"boolean","description":"True for an app or host fee recipient."}}}}}},"Episode":{"type":"object","properties":{"id":{"type":"string"},"showId":{"type":"string"},"guid":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"},"description":{"type":"string"},"allowHtml":{"type":"boolean"},"status":{"type":"string","enum":["draft","scheduled","published","unlisted"]},"publishedAt":{"type":"string","nullable":true},"durationSeconds":{"type":"number","nullable":true},"seasonNumber":{"type":"integer","nullable":true},"episodeNumber":{"type":"integer","nullable":true},"episodeType":{"type":"string"},"explicit":{"type":"boolean"},"isPrivate":{"type":"boolean"},"enclosure":{"type":"object","nullable":true},"artworkUrl":{"type":"string","nullable":true},"chaptersUrl":{"type":"string","nullable":true},"soundbites":{"$ref":"#/components/schemas/Soundbites"},"transcript":{"type":"object"}}},"Soundbites":{"type":"array","maxItems":1,"description":"Podcasting 2.0 podcast:soundbite: a short clip apps can play as a preview. Seconds from the start of the audio. An episode holds one soundbite today. Send [] or null to clear. When the episode's length is known, the clip must start before the end and finish inside it (soundbite_out_of_range). The spec suggests 15 to 120 seconds; zPod does not enforce that.","items":{"type":"object","required":["startTime","duration"],"properties":{"startTime":{"type":"number","minimum":0},"duration":{"type":"number","exclusiveMinimum":0},"title":{"type":"string","maxLength":255,"description":"Optional. Apps fall back to the episode title."}}}}}},"security":[{"bearerAuth":[]}],"paths":{"/api/v1/me":{"get":{"tags":["Account"],"summary":"Who this token acts for: account, studios, scopes, and show filter","description":"Any valid token may call this. `studios` lists each studio the token reaches with its show count (and the shows, when the token has shows:read). `hint` explains in plain words why a token reaches no shows.","responses":{"200":{"description":"Account, token, studios, showCount, optional hint"},"401":{"description":"Auth"}}}},"/api/v1/shows":{"get":{"tags":["Shows"],"summary":"List shows","description":"When the token reaches no shows, the response carries a `hint` saying which account the token acts for and why the list is empty.","security":[{"bearerAuth":["shows:read"]}],"responses":{"200":{"description":"Show list"},"401":{"description":"Auth"},"403":{"description":"Scope"}}},"post":{"tags":["Shows"],"summary":"Create a show","description":"Same path as Studio's New Show: plan show limits, storage, and defaults apply. Body: `title` (required), optional `studioId` (required when the account belongs to more than one studio; see `/api/v1/me`), `description`, `author`, `language`, `timeZone`, `feedMode` (public, unlisted, paid), `category`, `subcategory`, `ownerEmail`, `copyright`. A token with a show filter cannot create shows. Supports `Idempotency-Key`.","security":[{"bearerAuth":["shows:write"]}],"responses":{"201":{"description":"Show"},"400":{"description":"title_required, studio_required, or an invalid field"},"402":{"description":"Plan show limit reached"},"403":{"description":"Scope, or show_filter"},"404":{"description":"studio_not_found"}}}},"/api/v1/shows/{showId}":{"get":{"tags":["Shows"],"summary":"Get a show (canonical feed URL and Value 4 Value block included)","security":[{"bearerAuth":["shows:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Show"},"404":{"description":"Not found"}}},"patch":{"tags":["Shows"],"summary":"Change show settings","description":"Only the fields sent change: `title`, `description`, `author`, `language`, `explicit`, `copyright`, `timeZone`, `downloadFilenameStrategy`, `transcriptGeneration` (`disabled`, `disabled_per_episode`, `enabled`, `enabled_per_episode`), `ownerEmail` (owner or admin role), `category` with `subcategory` and the secondary and tertiary slots, `value` (a ShowValue object that replaces the whole Value 4 Value block; null or an empty `recipients` list removes it from the feed). Needs an edit role on the show. Shows cannot be deleted through the API.","security":[{"bearerAuth":["shows:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Show"},"400":{"description":"Invalid field or nothing_to_update"},"403":{"description":"Scope or role"},"404":{"description":"Not found"}}}},"/api/v1/shows/{showId}/artwork/url":{"post":{"tags":["Shows"],"summary":"Set the show cover from a public URL","description":"Body `{ url }`. PNG or JPEG, at least 1400x1400. WebP and AVIF are refused with a reason. Same checks as Studio's Set Cover From URL.","security":[{"bearerAuth":["shows:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Saved, with width and height"},"400":{"description":"invalid_url or unsupported image"},"502":{"description":"Fetch failed"}}}},"/api/v1/shows/{showId}/custom-domain":{"get":{"tags":["Custom Domains"],"summary":"Custom domain status and the DNS record to add","security":[{"bearerAuth":["shows:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"customDomain, customDomainStatus, adminStatus, entitled, dns"},"404":{"description":"Not found"}}},"put":{"tags":["Custom Domains"],"summary":"Add or change the show's custom domain","description":"Body `{ hostname }`. Saves the hostname and tries to attach it; the reply has the CNAME record and a plain-words `diagnosis`. Needs the Custom Hostname add-on and a token from the studio owner, because each domain is billed to the studio's card; a show admin gets 403 `owner_required`. Removing a domain is not possible through the API: an empty hostname is refused and there is no DELETE.","security":[{"bearerAuth":["domains:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Saved"},"400":{"description":"hostname_required or invalid hostname"},"402":{"description":"hostname_add_on_required, or hostname_payment_failed when the card on file is declined"},"403":{"description":"Scope, role, or owner_required"},"409":{"description":"hostname_taken"},"503":{"description":"Attach failed (try Check Hostname later) or billing_unavailable"}}}},"/api/v1/shows/{showId}/custom-domain/check":{"post":{"tags":["Custom Domains"],"summary":"Check Hostname","description":"Re-checks DNS for the saved hostname and attaches it once it points at zPod.","security":[{"bearerAuth":["domains:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Status and diagnosis"},"400":{"description":"hostname_required"},"402":{"description":"hostname_add_on_required"}}}},"/api/v1/shows/{showId}/directory-links":{"get":{"tags":["Shows"],"summary":"List stored and derived Listen Links for a show","security":[{"bearerAuth":["shows:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Directory links"},"404":{"description":"Not found"}}}},"/api/v1/shows/{showId}/directory-links/parse":{"post":{"tags":["Shows"],"summary":"Parse a listing link without saving","security":[{"bearerAuth":["shows:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}}}},"responses":{"200":{"description":"Parsed directory"},"400":{"description":"Unrecognized link"}}}},"/api/v1/shows/{showId}/directory-links/find":{"post":{"tags":["Shows"],"summary":"Find Apple Podcasts and Podcast Index listings by feed URL. Does not save.","security":[{"bearerAuth":["shows:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Proposed listings"},"429":{"description":"Apple lookup rate limit"}}}},"/api/v1/shows/{showId}/directory-links/find/confirm":{"post":{"tags":["Shows"],"summary":"Save a Find My Listings proposal after confirm. Never invents an id.","security":[{"bearerAuth":["shows:read","episodes:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lookupId":{"type":"string"},"directory":{"type":"string"}}}}}},"responses":{"200":{"description":"Saved listing"},"409":{"description":"Confirm required"}}}},"/api/v1/shows/{showId}/directory-links/{directory}":{"put":{"tags":["Shows"],"summary":"Save a directory listing link","security":[{"bearerAuth":["shows:read","episodes:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}},{"name":"directory","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"externalId":{"type":"string"},"hidden":{"type":"boolean"},"status":{"type":"string","enum":["not_started","submitted","listed"]}}}}}},"responses":{"200":{"description":"Saved link"},"400":{"description":"Invalid link"}}}},"/api/v1/shows/{showId}/episodes":{"get":{"tags":["Episodes"],"summary":"List episodes","security":[{"bearerAuth":["episodes:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["draft","scheduled","published","unlisted"]}},{"name":"hasAudio","in":"query","description":"true: only episodes with playable media. false: only episodes without (archive episodes, drafts awaiting audio).","schema":{"type":"boolean"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"episodes (each with hasAudio), total, offset, nextOffset"}}},"post":{"tags":["Episodes"],"summary":"Create a draft episode (never auto-publishes)","security":[{"bearerAuth":["episodes:write"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"allowHtml":{"type":"boolean"},"seasonNumber":{"type":"integer"},"episodeNumber":{"type":"integer"},"explicit":{"type":"boolean"},"episodeType":{"type":"string"},"publishedAt":{"type":"string","description":"Schedule instant stored as metadata only (ISO with Z/offset, naive YYYY-MM-DDTHH:mm, or dd MMM YYYY HH:mm in the show timezone). No feed fanout until POST …/publish or …/schedule."},"soundbites":{"$ref":"#/components/schemas/Soundbites"}}}}}},"responses":{"201":{"description":"Draft episode"},"400":{"description":"soundbites_invalid or soundbite_out_of_range"}}}},"/api/v1/episodes/{episodeId}":{"get":{"tags":["Episodes"],"summary":"Get an episode","security":[{"bearerAuth":["episodes:read"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Episode"}}},"patch":{"tags":["Episodes"],"summary":"Update draft metadata (no publish / no fanout)","description":"Send only the fields to change: title, description, allowHtml, seasonNumber, episodeNumber, explicit, episodeType, isPrivate, publishedAt, transcriptGeneration, downloadFilename, soundbites. A soundbite is checked against the episode's stored length.","security":[{"bearerAuth":["episodes:write"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"soundbites":{"$ref":"#/components/schemas/Soundbites"}},"additionalProperties":true}}}},"responses":{"200":{"description":"Updated episode"},"400":{"description":"soundbites_invalid or soundbite_out_of_range"},"409":{"description":"Not a draft"}}}},"/api/v1/episodes/{episodeId}/media/presign":{"post":{"tags":["Media"],"summary":"Presign an audio PUT (use the returned URL; do not post the file through this API)","description":"Episode media must be MP3, or AAC in M4A or MP4 (the formats Apple Podcasts plays). The filename's extension (.mp3, .m4a or .mp4), else contentType, picks the format; commit then reads the file and refuses anything else. PUT with the returned headers: the Content-Type is signed.","security":[{"bearerAuth":["media:upload"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["byteSize"],"properties":{"filename":{"type":"string","description":"Ends in .mp3, .m4a or .mp4. Kept as the episode's original filename."},"contentType":{"type":"string"},"byteSize":{"type":"integer","description":"Exact file size in bytes. The upload URL is signed for this size and accepts no other."}}}}}},"responses":{"200":{"description":"uploadUrl, ticket, headers"},"400":{"description":"byte_size_required"},"402":{"description":"Past the studio's included storage or spend cap"},"413":{"description":"Larger than a single upload allows"},"415":{"description":"unsupported_media: not an .mp3, .m4a or .mp4 file"}}}},"/api/v1/episodes/{episodeId}/media/commit":{"post":{"tags":["Media"],"summary":"Bind a completed upload ticket. Leaves the episode as draft.","security":[{"bearerAuth":["media:upload"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["ticket"],"properties":{"ticket":{"type":"string"},"byteSize":{"type":"integer","description":"Ignored. The stored object's size is measured."},"durationSeconds":{"type":"number"}}}}}},"responses":{"200":{"description":"Episode still draft"},"415":{"description":"unsupported_media: the stored file is not MP3 or AAC (for example Apple Lossless); it is deleted"}}}},"/api/v1/episodes/{episodeId}/validate":{"post":{"tags":["Publish"],"summary":"Publish checklist with no side effects (same items as Studio)","security":[{"bearerAuth":["episodes:read"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok, checks, blockers"}}}},"/api/v1/episodes/{episodeId}/publish":{"post":{"tags":["Publish"],"summary":"Publish now (Studio Publish Now + feed fanout)","security":[{"bearerAuth":["publish"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Published episode"},"403":{"description":"Missing publish scope"}}}},"/api/v1/shows/{showId}/episodes/archive-candidates":{"get":{"tags":["Archive Episodes"],"summary":"Imported drafts without usable audio, with the reason for each","description":"For a backlog imported from a show that kept audio only for a recent window. eligible: the source had no enclosure, or the audio returned 404 or 410; these can be published as archive episodes. attention: other copy failures, which stay drafts to retry.","security":[{"bearerAuth":["episodes:read"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"eligible and attention lists (episodeId, guid, title, publishedAt, code, reason, sourceEnclosureUrl, importJobId)"}}}},"/api/v1/shows/{showId}/episodes/publish-archive":{"post":{"tags":["Archive Episodes"],"summary":"Publish imported drafts whose source audio is gone as archive episodes (bulk)","description":"Keeps each GUID and original publish date, clears the dead audio reference, and publishes. Archive episodes appear on the show's website and stay out of the RSS feed. Sends no WebSub, Podping or webhooks. Ids that are not eligible are returned in skipped with a reason.","security":[{"bearerAuth":["publish"]}],"parameters":[{"name":"showId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"episodeIds":{"type":"array","items":{"type":"string"},"maxItems":5000},"all":{"type":"boolean","description":"Every eligible draft in the show."}}}}}},"responses":{"200":{"description":"publishedCount, published, skipped"},"400":{"description":"No episodeIds and no all"}}}},"/api/v1/episodes/{episodeId}/publish-archive":{"post":{"tags":["Archive Episodes"],"summary":"Clear an imported draft's dead audio reference and publish it as an archive episode","security":[{"bearerAuth":["publish"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Archive episode"},"409":{"description":"Not an imported draft whose source audio is gone"}}}},"/api/v1/episodes/{episodeId}/schedule":{"post":{"tags":["Publish"],"summary":"Schedule in the show timezone (Studio Schedule + fanout when due / past)","security":[{"bearerAuth":["publish"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publishedAt"],"properties":{"publishedAt":{"type":"string","description":"ISO with Z/offset, naive YYYY-MM-DDTHH:mm, or dd MMM YYYY HH:mm, interpreted in Show.timeZone and stored UTC."}}}}}},"responses":{"200":{"description":"Scheduled or published if the time is already due"}}}},"/api/v1/episodes/{episodeId}/unpublish":{"post":{"tags":["Publish"],"summary":"Soft-hide to draft (files stay)","security":[{"bearerAuth":["publish"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Draft episode"}}}},"/api/v1/episodes/{episodeId}/artwork/presign":{"post":{"tags":["Attach"],"summary":"Presign episode artwork PUT (PNG/JPEG; oversized files are cropped and sized to 3000)","security":[{"bearerAuth":["media:upload"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"uploadUrl, ticket, headers"}}}},"/api/v1/episodes/{episodeId}/artwork/commit":{"post":{"tags":["Attach"],"summary":"Commit episode artwork after the presigned PUT","security":[{"bearerAuth":["media:upload"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Episode with artworkUrl"}}}},"/api/v1/episodes/{episodeId}/chapters":{"put":{"tags":["Attach"],"summary":"Replace Podcasting 2.0 chapters JSON (Episode.chaptersJson)","security":[{"bearerAuth":["episodes:write"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Episode with chaptersUrl"}}},"patch":{"tags":["Attach"],"summary":"Replace Podcasting 2.0 chapters JSON (same as PUT)","security":[{"bearerAuth":["episodes:write"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Episode with chaptersUrl"}}}},"/api/v1/episodes/{episodeId}/transcript-generation":{"put":{"tags":["Attach"],"summary":"Turn transcript generation on or off for one episode","description":"Body `{ \"generation\": \"enabled\" | \"disabled\" | \"show\" }`. Works on any status. Only shows set to `disabled_per_episode` or `enabled_per_episode` take an episode override; `show` clears it. Turning generation on queues a transcript when the episode already has audio and no transcript. `episode.transcript.generation` reports the override (null follows the show).","security":[{"bearerAuth":["episodes:write"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Episode, show setting, and whether a transcript was queued"},"400":{"description":"invalid_transcript_generation"},"409":{"description":"transcript_generation_locked: the show's setting applies to every episode"}}}},"/api/v1/episodes/{episodeId}/transcript/presign":{"post":{"tags":["Attach"],"summary":"Presign a WebVTT or plain-text transcript PUT","security":[{"bearerAuth":["media:upload"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"uploadUrl, ticket, headers"}}}},"/api/v1/episodes/{episodeId}/transcript/commit":{"post":{"tags":["Attach"],"summary":"Commit a transcript sidecar after the presigned PUT","security":[{"bearerAuth":["media:upload"]}],"parameters":[{"name":"episodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Episode with transcript URLs"}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhook endpoints (secret never returned)","security":[{"bearerAuth":["webhooks:write"]}],"responses":{"200":{"description":"Webhook list"},"403":{"description":"Missing webhooks:write"}}},"post":{"tags":["Webhooks"],"summary":"Create a webhook endpoint. Signing secret is shown once.","security":[{"bearerAuth":["webhooks:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","url"],"properties":{"name":{"type":"string"},"url":{"type":"string","description":"HTTPS callback URL"},"events":{"type":"array","items":{"type":"string","enum":["episode.published","episode.scheduled","episode.unpublished","media.ready"]},"description":"Default is all v1 events"},"showIds":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Webhook plus secret"}}}},"/api/v1/webhooks/{webhookId}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook endpoint (no secret)","security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook"},"404":{"description":"Not found"}}},"patch":{"tags":["Webhooks"],"summary":"Update name, URL, events, or show filter","security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated webhook"}}}},"/api/v1/webhooks/{webhookId}/revoke":{"post":{"tags":["Webhooks"],"summary":"Revoke a webhook endpoint","security":[{"bearerAuth":["webhooks:write"]}],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked webhook"}}}}},"x-zpod-scopes":["shows:read","episodes:read","episodes:write","media:upload","publish","webhooks:write","shows:write","domains:write"],"x-zpod-default-scopes":["shows:read","episodes:read","episodes:write","media:upload"],"x-zpod-webhook-events":["episode.published","episode.scheduled","episode.unpublished","media.ready"],"x-zpod-mcp":{"package":"@zpod/mcp","transport":"stdio","hosted":true,"url":"https://zpod.cc/mcp","hostedTransport":"streamable-http","oauth":{"protectedResourceMetadata":"https://zpod.cc/.well-known/oauth-protected-resource","authorizationServerMetadata":"https://zpod.cc/.well-known/oauth-authorization-server"},"note":"Hosted at https://zpod.cc/mcp (Streamable HTTP; OAuth 2.1 sign-in with PKCE, or Authorization: Bearer API token) and client-run as the @zpod/mcp stdio wrapper. Both wrap /api/v1 with the same token and scopes. Staff 404 tools are client-run only."},"x-zpod-out-of-scope":["import:write","sdks","connect","stats:read"]}