{"openapi": "3.1.0", "info": {"title": "Podzone API", "version": "1.0.0", "description": "The Podzone public API, on `https://api.podzone.fm`. Authenticate with an API key created in the Podzone app, sent as a bearer token: `Authorization: Bearer pzk_live_...`.\n\nThis is the reference: every endpoint, field and error. The guides at [podzone.fm/docs](https://podzone.fm/docs) explain the rest -- a [quickstart](https://podzone.fm/docs/quickstart), [team and account keys](https://podzone.fm/docs/authentication), [verifying webhooks](https://podzone.fm/docs/webhooks) and connecting an [AI assistant over MCP](https://podzone.fm/docs/mcp).\n\n## Rate limits\n\nRequests are limited per **account**, not per key or per user. Everyone on an account shares one budget, and so does every API key they hold -- a key does not get an allowance of its own.\n\n| Plan | Requests per minute | Requests per month | Transcript downloads per month |\n| --- | --- | --- | --- |\n| Free | 10 | 1,000 | 20 |\n| Starter | 60 | 10,000 | 1,000 |\n| Pro | 120 | 90,000 | 5,000 |\n\nA transcript download is also counted against the plan's monthly transcript downloads, which are shared with downloads made in the app.\n\nThe monthly allowances reset at **00:00 UTC** on the same day each month: the day your account subscribed to its plan, or the day it was created if it has never subscribed. In a month without that day -- the 31st in April, say -- they reset on the month's last day.\n\nEvery response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` for the per-minute window, and the `-Month` suffixed equivalents for the monthly one. `X-RateLimit-Reset` and `X-RateLimit-Reset-Month` are Unix timestamps in seconds; read the latter rather than working out your reset day.\n\nExceeding a limit returns `429` with a `Retry-After` header and an error body whose `scope` is `per_minute`, `per_month` or `transcript_downloads_per_month`. Branch on it: `per_minute` means slow down and retry; the monthly scopes mean retrying before the reset will fail too. Denied requests still count against your monthly request allowance; a refused download does not count as a download.\n\n## Pagination\n\nA route that returns a page at a time takes `limit` and `cursor`, and answers with `next_cursor`. Pass `next_cursor` back as `cursor`, with the other parameters unchanged, for the next page; `null` means there are no more. `limit` sets the page size, up to the maximum each route states. Cursors are opaque -- do not build or parse them -- and no list reports a total count.\n\nA cursor refused with `stale_cursor` was issued for a different query; one refused with `invalid_cursor` could not be read. Either way, start again from the first page.\n\n## Errors\n\nEvery error has the same shape. Branch on `code`; `message` is for people and may be reworded. Some codes carry more fields, described under each.\n\n```json\n{\"error\": {\"code\": \"episode_not_found\", \"message\": \"No such episode.\"}}\n```\n\n| Status | Code | Meaning |\n| --- | --- | --- |\n| 401 | `invalid_credentials` | The API key is missing, malformed or revoked. |\n| 403 | `plan_limit_reached` | The plan has no room for another one. `limit_key` and `limit` say which limit; only a plan with more room lifts it. |\n| 404 | `podcast_not_found` | No podcast has this id. |\n| 404 | `episode_not_found` | No episode has this id. |\n| 404 | `transcript_not_available` | The episode exists but has no transcript yet. |\n| 404 | `alert_not_found` | No alert has this id in this team. |\n| 404 | `alert_match_not_found` | No match has this id in this team. |\n| 404 | `webhook_not_found` | No webhook has this id in this team. |\n| 404 | `team_not_found` | No team this key can reach has this id. |\n| 422 | `invalid_input` | A parameter, header or body field was invalid. `fields` says what was wrong with each. |\n| 422 | `stale_cursor` | The cursor was issued for a different query. Start again from the first page. |\n| 422 | `invalid_cursor` | The cursor could not be read. Start again from the first page. |\n| 429 | `rate_limited` | A limit was reached. `scope` says which, `reset` when it lifts, and the `Retry-After` header how many seconds to wait. |\n| 503 | `search_unavailable` | Search is briefly unavailable. Retry after the `Retry-After` header's seconds. |\n| 503 | `storage_unavailable` | Transcripts are briefly unavailable. Retry after the `Retry-After` header's seconds. |\n\nEach route lists the errors it can return. Every route can also return `401` and `429`."}, "paths": {"/api/v1/me": {"get": {"operationId": "me", "summary": "The team and account this API key acts as", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/IdentityOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Returns the key, the team whose data it reaches, and the account whose quota it spends. Useful for verifying that a key is live and which team it acts as.\n\n`scope` is either `team` or `account`. A `team` key reaches one workspace and `team` names it. An `account` key reaches every team in the account, `team` is `null`, and each request that touches a team's data must name it in the `X-Podzone-Team` header \u2014 use `GET /v1/teams` to discover which ids it may send.", "tags": ["identity"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/teams": {"get": {"operationId": "list_teams", "summary": "The teams this API key can reach", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TeamListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Every team this key may act for.\n\nFor a `team`-scoped key this is exactly one row \u2014 the team on the credential. For an `account`-scoped key it is every team in the account, and the `id` values here are what the `X-Podzone-Team` header expects.\n\nThe shape is the same for both, so a client never has to know its own scope to parse the response.", "tags": ["identity"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/teams/{team_id}/members": {"get": {"operationId": "list_team_members", "summary": "Who is in a team this API key can reach", "parameters": [{"in": "path", "name": "team_id", "schema": {"format": "uuid", "title": "Team Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TeamMemberListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `team_not_found`: No team this key can reach has this id.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Everybody in the named team.\n\nThe `id` values here are what `recipient_user_ids` on an alert expects \u2014 this is how an integration discovers who it may notify.\n\nA team this key cannot reach answers `404`, whether it belongs to another account or is simply one a team-scoped key does not act for.\n\nEmail addresses are deliberately not published. An API key reaches a team's data, not its colleagues' contact details.", "tags": ["identity"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/podcasts/search": {"get": {"operationId": "search_podcasts", "summary": "Search podcasts", "parameters": [{"in": "query", "name": "q", "schema": {"description": "What to search for, at least three characters. Matches anywhere in a show's name. Bare words must all appear; `\"quoted phrases\"` match adjacent words, `|` means OR and a leading `-` excludes.", "maxLength": 200, "minLength": 3, "title": "Q", "type": "string"}, "required": true, "description": "What to search for, at least three characters. Matches anywhere in a show's name. Bare words must all appear; `\"quoted phrases\"` match adjacent words, `|` means OR and a leading `-` excludes."}, {"in": "query", "name": "fields", "schema": {"description": "Where to look: `title`, `description`, or both (the default). Repeat to give both.", "items": {"enum": ["title", "description"], "type": "string"}, "title": "Fields", "type": "array"}, "required": false, "description": "Where to look: `title`, `description`, or both (the default). Repeat to give both."}, {"in": "query", "name": "order_by", "schema": {"default": "best_match", "description": "`best_match`, or `last_episode_at` for the date of the show's newest episode.", "enum": ["best_match", "last_episode_at"], "title": "Order By", "type": "string"}, "required": false, "description": "`best_match`, or `last_episode_at` for the date of the show's newest episode."}, {"in": "query", "name": "order_dir", "schema": {"default": "desc", "description": "`desc` or `asc`. Ignored when `order_by` is `best_match`.", "enum": ["asc", "desc"], "title": "Order Dir", "type": "string"}, "required": false, "description": "`desc` or `asc`. Ignored when `order_by` is `best_match`."}, {"in": "query", "name": "limit", "schema": {"default": 25, "description": "How many results to return per page.", "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false, "description": "How many results to return per page."}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}, "required": false, "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PodcastSearchOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `stale_cursor`: The cursor was issued for a different query. Start again from the first page.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "503": {"description": "Temporarily unavailable. `error.code` is one of:\n\n- `search_unavailable`: Search is briefly unavailable. Retry after the `Retry-After` header's seconds.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}, "headers": {"Retry-After": {"description": "How many seconds to wait before retrying.", "schema": {"type": "integer"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Search the catalogue by podcast title, description, or both.\n\n`q` is one search term. It matches anywhere inside a show's name, so `tech` finds *Mostly Technical* and `radio` finds *Radiolab*, and it is full-text search over the show's description.\n\nBare words are combined with AND, so `true crime` requires both. Use `\"quoted phrases\"` for adjacent words, `|` to widen, and a leading `-` to exclude \u2014 for example `kubernetes | docker -beginner`. Malformed syntax is interpreted as best it can be rather than rejected.\n\n`fields` chooses where to look, and repeats: `?q=technical&fields=title&fields=description`. Omit it to search both. Terms must be at least three characters.\n\n**One term, searched across the fields you choose.** A single request cannot bind a different term to each field \u2014 `title=technical` *and* `description=kubernetes` together is not expressible. Narrow with `fields` and the query syntax instead.\n\nResults can be ordered with `order_by` \u2014 `best_match` (default) or `last_episode_at` \u2014 and `order_dir` (`asc`/`desc`). `best_match` always ranks most-relevant first and ignores `order_dir`.\n\nResults are paged with an opaque cursor. Pass the `next_cursor` from a response as `cursor` to get the next page; it is null on the last one. A cursor is tied to the search that issued it: change `q`, `fields` or the ordering and start again from the first page.", "tags": ["podcasts"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/podcasts/{podcast_id}": {"get": {"operationId": "get_podcast", "summary": "One podcast", "parameters": [{"in": "path", "name": "podcast_id", "schema": {"format": "uuid", "title": "Podcast Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PodcastDetailOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `podcast_not_found`: No podcast has this id.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Everything a search result carries, plus the show's categories and `available_episode_count`: how many of its episodes Podzone holds, which is exactly what `/podcasts/{podcast_id}/episodes` pages through.", "tags": ["podcasts"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/podcasts/{podcast_id}/episodes": {"get": {"operationId": "list_podcast_episodes", "summary": "One podcast's episodes", "parameters": [{"in": "path", "name": "podcast_id", "schema": {"format": "uuid", "title": "Podcast Id", "type": "string"}, "required": true}, {"in": "query", "name": "limit", "schema": {"default": 10, "description": "How many results to return per page.", "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false, "description": "How many results to return per page."}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}, "required": false, "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodeListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `podcast_not_found`: No podcast has this id.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "A podcast's episodes, most recently published first, with undated episodes last.\n\nPaged with an opaque cursor: pass the `next_cursor` from a response as `cursor` to get the next page, and stop when it is null. Cursors encode a position in this ordering and must not be constructed or parsed.\n\nPodzone follows a feed from the day it is admitted and does not fetch its back catalogue, so this lists the episodes we hold rather than everything the show has ever published.", "tags": ["podcasts"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/episodes/search": {"get": {"operationId": "search_episodes", "summary": "Search episodes", "parameters": [{"in": "query", "name": "q", "schema": {"description": "What to search for, at least three characters. Bare words must all appear; `\"quoted phrases\"` match adjacent words, `|` means OR and a leading `-` excludes.", "maxLength": 200, "minLength": 3, "title": "Q", "type": "string"}, "required": true, "description": "What to search for, at least three characters. Bare words must all appear; `\"quoted phrases\"` match adjacent words, `|` means OR and a leading `-` excludes."}, {"in": "query", "name": "scope", "schema": {"description": "`transcript` searches what was said; `metadata` searches episode titles and descriptions.", "enum": ["transcript", "metadata"], "title": "Scope", "type": "string"}, "required": true, "description": "`transcript` searches what was said; `metadata` searches episode titles and descriptions."}, {"in": "query", "name": "podcast_id", "schema": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "description": "Only search this podcast's episodes.", "title": "Podcast Id"}, "required": false, "description": "Only search this podcast's episodes."}, {"in": "query", "name": "since", "schema": {"anyOf": [{"format": "date", "type": "string"}, {"type": "null"}], "description": "First day to search, inclusive, as an ISO 8601 date: `YYYY-MM-DD` (UTC). Defaults to 30 days before `until`. An earlier date than 18 months ago is moved forward to 18 months before today.", "examples": ["2026-09-01"], "title": "Since"}, "required": false, "description": "First day to search, inclusive, as an ISO 8601 date: `YYYY-MM-DD` (UTC). Defaults to 30 days before `until`. An earlier date than 18 months ago is moved forward to 18 months before today.", "examples": ["2026-09-01"]}, {"in": "query", "name": "until", "schema": {"anyOf": [{"format": "date", "type": "string"}, {"type": "null"}], "description": "Last day to search, inclusive, as an ISO 8601 date: `YYYY-MM-DD` (UTC). Defaults to today.", "examples": ["2026-09-30"], "title": "Until"}, "required": false, "description": "Last day to search, inclusive, as an ISO 8601 date: `YYYY-MM-DD` (UTC). Defaults to today.", "examples": ["2026-09-30"]}, {"in": "query", "name": "order_by", "schema": {"default": "best_match", "description": "`best_match`, or `published_at` to see new mentions first.", "enum": ["best_match", "published_at"], "title": "Order By", "type": "string"}, "required": false, "description": "`best_match`, or `published_at` to see new mentions first."}, {"in": "query", "name": "order_dir", "schema": {"default": "desc", "description": "`desc` or `asc`. Ignored when `order_by` is `best_match`.", "enum": ["asc", "desc"], "title": "Order Dir", "type": "string"}, "required": false, "description": "`desc` or `asc`. Ignored when `order_by` is `best_match`."}, {"in": "query", "name": "limit", "schema": {"default": 25, "description": "How many results to return per page.", "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false, "description": "How many results to return per page."}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}, "required": false, "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodeSearchOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `stale_cursor`: The cursor was issued for a different query. Start again from the first page.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "503": {"description": "Temporarily unavailable. `error.code` is one of:\n\n- `search_unavailable`: Search is briefly unavailable. Retry after the `Retry-After` header's seconds.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}, "headers": {"Retry-After": {"description": "How many seconds to wait before retrying.", "schema": {"type": "integer"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Find episodes by what was said in them, or by their title and description.\n\n`scope` chooses which, and is required: `transcript` searches the spoken words, `metadata` searches titles and descriptions. **One scope per request.**\n\n`q` uses the same syntax as `/podcasts/search`. Bare words are combined with AND, `\"quoted phrases\"` match adjacent words, `|` is OR, and a leading `-` excludes \u2014 for example `\"brand safety\" -podcast`. Malformed syntax is interpreted as best it can be rather than rejected. Terms must be at least three characters.\n\n`podcast_id` narrows the search to one show.\n\n**Searches cover the last 30 days by default.** `since` and `until` are inclusive dates. `until` defaults to today, and `since` defaults to 30 days before `until`. Episodes are searchable for eighteen months: an earlier `since` is moved forward to that limit rather than refused. Every response reports the days it actually covered in `window`, with `window.earliest` as the limit, so an empty page can be told apart from an empty archive.\n\nThe eighteen months limit search, not what Podzone holds. An older episode is still listed by `/podcasts/{podcast_id}/episodes`, and its transcript can still be searched with `/episodes/{episode_id}/transcript/excerpts` and read with `/episodes/{episode_id}/transcript`, one episode at a time.\n\nResults can be ordered with `order_by`: `best_match` (default) or `published_at`, which is newest first unless `order_dir=asc`. `best_match` ignores `order_dir`. Use `published_at` to watch for new mentions.\n\nEach result carries up to two `excerpts`, each an object whose `text` is a short plain-text passage around the match, taken from the transcript or, for `metadata`, the description. They can be empty. They carry no position in the audio: ask `/episodes/{episode_id}/transcript/excerpts` with the same words for timed passages. The full transcript is not included; read it from `/episodes/{episode_id}/transcript`.\n\nResults are paged with an opaque cursor. Pass `next_cursor` back as `cursor`; it is null on the last page. A cursor is tied to the search that issued it: change `q`, `scope`, the dates or the ordering and start again from the first page. A default-window cursor also expires when the day changes (UTC), because the window has moved.", "tags": ["episodes"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/episodes/{episode_id}": {"get": {"operationId": "get_episode", "summary": "One episode", "parameters": [{"in": "path", "name": "episode_id", "schema": {"format": "uuid", "title": "Episode Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EpisodeDetailOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `episode_not_found`: No episode has this id.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "One episode, the show it belongs to, and a summary of its transcript.\n\n`transcript` is null when the episode has none \u2014 uncommon, but check it (or `has_transcript`) before reading it. `transcript.duration_seconds` is measured from the audio and is more reliable than `duration_seconds`, which is the publisher's claim.\n\nThe transcript itself is not included at any size. Find passages in it with `/episodes/{episode_id}/transcript/excerpts`, read it a page at a time from `/episodes/{episode_id}/transcript`, or download it whole from `/episodes/{episode_id}/transcript/download`.", "tags": ["episodes"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/episodes/{episode_id}/transcript/download": {"get": {"operationId": "download_episode_transcript", "summary": "Download one episode's transcript", "parameters": [{"in": "path", "name": "episode_id", "schema": {"format": "uuid", "title": "Episode Id", "type": "string"}, "required": true}, {"in": "query", "name": "format", "schema": {"default": "json", "enum": ["json", "txt", "srt", "vtt"], "title": "Format", "type": "string"}, "required": false}], "responses": {"200": {"description": "The transcript, as a file.", "content": {"application/json": {"schema": {"type": "object"}}, "text/plain": {"schema": {"type": "string"}}, "application/x-subrip": {"schema": {"type": "string"}}, "text/vtt": {"schema": {"type": "string"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `episode_not_found`: No episode has this id.\n- `transcript_not_available`: The episode exists but has no transcript yet.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "503": {"description": "Temporarily unavailable. `error.code` is one of:\n\n- `storage_unavailable`: Transcripts are briefly unavailable. Retry after the `Retry-After` header's seconds.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}, "headers": {"Retry-After": {"description": "How many seconds to wait before retrying.", "schema": {"type": "integer"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "The episode's transcript as a file, in one of four formats.\n\n| `format` | Content-Type | Contains |\n| --- | --- | --- |\n| `json` *(default)* | `application/json` | timed segments |\n| `txt` | `text/plain` | the transcript as paragraphs, no timestamps |\n| `srt` | `application/x-subrip` | SubRip subtitles |\n| `vtt` | `text/vtt` | WebVTT subtitles |\n\n`json` carries the transcript as timed `segments` and does not repeat it as a single flat string; for the plain words, ask for `txt`, or join the segment texts with a space for exactly that string. `txt` breaks the words into paragraphs at pauses, at most 30 seconds each, separated by a blank line; `json`, `srt` and `vtt` stay at one sentence per segment.\n\nThe body is the file itself \u2014 not an archive to unpack \u2014 and carries a `Content-Disposition` filename, so the response can be written straight to disk and opened. (Ordinary HTTP transport compression still applies, and every HTTP client handles that for you.)\n\nTimestamps are seconds from the start of the episode, so they deep-link into the publisher's audio directly.\n\nReturns `404` with code `transcript_not_available` when the episode exists but has no transcript, which is distinct from `episode_not_found`.\n\nEach download counts against your plan's monthly transcript downloads, shared with downloads made in the app, as well as being an API request. Once they are used, this returns `429` with code `rate_limited` and `scope` `transcript_downloads_per_month`, with `reset` and `Retry-After` saying when the allowance renews. A `404` costs no download.", "tags": ["episodes"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/episodes/{episode_id}/transcript/excerpts": {"get": {"operationId": "find_transcript_excerpts", "summary": "Find words in one episode's transcript", "parameters": [{"in": "path", "name": "episode_id", "schema": {"format": "uuid", "title": "Episode Id", "type": "string"}, "required": true}, {"in": "query", "name": "q", "schema": {"description": "Words or phrases to find, matched as written: whole words, ignoring case, with no search syntax. Repeat for up to five, variants included (`podcast`, `podcasts`); a passage is returned where any of them is said.", "items": {"maxLength": 200, "minLength": 3, "type": "string"}, "maxItems": 5, "minItems": 1, "title": "Q", "type": "array"}, "required": true, "description": "Words or phrases to find, matched as written: whole words, ignoring case, with no search syntax. Repeat for up to five, variants included (`podcast`, `podcasts`); a passage is returned where any of them is said."}, {"in": "query", "name": "context", "schema": {"default": 2, "description": "Sentences to include before and after each match.", "maximum": 5, "minimum": 0, "title": "Context", "type": "integer"}, "required": false, "description": "Sentences to include before and after each match."}, {"in": "query", "name": "limit", "schema": {"default": 5, "description": "The most passages to return. The earliest matches come first.", "maximum": 5, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false, "description": "The most passages to return. The earliest matches come first."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TranscriptExcerptsOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `episode_not_found`: No episode has this id.\n- `transcript_not_available`: The episode exists but has no transcript yet.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "503": {"description": "Temporarily unavailable. `error.code` is one of:\n\n- `storage_unavailable`: Transcripts are briefly unavailable. Retry after the `Retry-After` header's seconds.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}, "headers": {"Retry-After": {"description": "How many seconds to wait before retrying.", "schema": {"type": "integer"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "The passages of one episode's transcript where any of the words in `q` is said, each with its start and end time in seconds.\n\nUse it after a search to see where, and in what context, something was said, without reading the whole transcript. It works on any transcribed episode, including one published before search's eighteen-month window.\n\n**Terms match as written.** Each is matched as a whole word or phrase, ignoring case, with none of search's query syntax. Send the words themselves rather than a search's `q`, one term per word or phrase: `q=brand safety` here is the phrase, where search reads two words that need not be adjacent. Variants such as `podcast` and `podcasts` are different words, so send both if both count.\n\n**The earliest matches only.** At most `limit` passages come back, each `context` sentences either side of a match, and there is no next page. `total_hits` says how many times the words are said in all, so you can tell when there is more to read. To read the rest, use `/episodes/{episode_id}/transcript`.\n\nCounts as one API request and never as a transcript download. Returns `404` with code `transcript_not_available` when the episode has no transcript.", "tags": ["episodes"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/episodes/{episode_id}/transcript": {"get": {"operationId": "read_episode_transcript", "summary": "Read one episode's transcript", "parameters": [{"in": "path", "name": "episode_id", "schema": {"format": "uuid", "title": "Episode Id", "type": "string"}, "required": true}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it to start from the beginning, which counts as a transcript download. A cursor works only for the API key's account, and only until the monthly allowance resets.", "title": "Cursor"}, "required": false, "description": "The `next_cursor` from the previous page. Omit it to start from the beginning, which counts as a transcript download. A cursor works only for the API key's account, and only until the monthly allowance resets."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TranscriptPageOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `episode_not_found`: No episode has this id.\n- `transcript_not_available`: The episode exists but has no transcript yet.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `stale_cursor`: The cursor was issued for a different query. Start again from the first page.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "503": {"description": "Temporarily unavailable. `error.code` is one of:\n\n- `storage_unavailable`: Transcripts are briefly unavailable. Retry after the `Retry-After` header's seconds.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}, "headers": {"Retry-After": {"description": "How many seconds to wait before retrying.", "schema": {"type": "integer"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "The transcript as timed paragraphs, a page at a time: the same paragraphs as the `txt` download, each with its start and end time in seconds. It works on any transcribed episode, including one published before search's eighteen-month window.\n\n**The first page counts as one transcript download**, the same as `/episodes/{episode_id}/transcript/download`, and is refused with `429` and `scope` `transcript_downloads_per_month` once the month's are used. Following `next_cursor` to the later pages counts only as API requests.\n\nA cursor works only for the account that read the first page, and only until the monthly allowances reset. After that it is refused with `422` and code `stale_cursor`: read again from the first page.\n\nReturns `404` with code `transcript_not_available` when the episode has no transcript, which costs no download.", "tags": ["episodes"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/alerts": {"get": {"operationId": "list_alerts", "summary": "List alerts", "parameters": [{"in": "query", "name": "limit", "schema": {"default": 50, "description": "How many results to return per page.", "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false, "description": "How many results to return per page."}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}, "required": false, "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them."}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Every alert belonging to the team this API key acts for, newest first.\n\nPaged with an opaque `cursor`: pass the `next_cursor` from one response as `cursor` on the next request, and stop when it comes back `null`.", "tags": ["alerts"], "security": [{"APIKeyAuth": []}]}, "post": {"operationId": "create_alert", "summary": "Create an alert", "parameters": [{"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"description": "Not allowed on this plan. `error.code` is one of:\n\n- `plan_limit_reached`: The plan has no room for another one. `limit_key` and `limit` say which limit; only a plan with more room lifts it.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Creates an alert for the team this API key acts for.\n\n`keywords` is a list of up to 10 terms to watch for; a transcript matching any of them produces a match.\n\nOptionally supply `question` with `use_classifier: true` to have each keyword judged using intelligent matching. The question should be phrased to return a yes or no answer.\n\n**An alert emails nobody unless you say otherwise.** Pass `recipient_user_ids` with ids from `/teams/{team_id}/members` and `email_enabled: true` to have matches delivered by email; omit this and the alert will still record matches without emailing anyone.\n\n**Keywords too broad to run are refused** with a 422 naming `keywords`: before an alert is switched on, its keywords are counted against the last week of transcripts, and an alert that would match too many episodes a day is not created. A running alert found in too many episodes in one day is switched off, with `paused_reason: match_limit`. Matches are kept for 60 days.", "tags": ["alerts"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertCreateRequest"}}}, "required": true}, "security": [{"APIKeyAuth": []}]}}, "/api/v1/alerts/matches/{match_id}": {"get": {"operationId": "get_match", "summary": "Read one match", "parameters": [{"in": "path", "name": "match_id", "schema": {"format": "uuid", "title": "Match Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MatchOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `alert_match_not_found`: No match has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "One match, by id, from any of this team's alerts.", "tags": ["alerts"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/alerts/{alert_id}": {"get": {"operationId": "get_alert", "summary": "Read one alert", "parameters": [{"in": "path", "name": "alert_id", "schema": {"format": "uuid", "title": "Alert Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `alert_not_found`: No alert has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "One alert, by id, from any of this team's alerts.", "tags": ["alerts"], "security": [{"APIKeyAuth": []}]}, "patch": {"operationId": "update_alert", "summary": "Update an alert", "parameters": [{"in": "path", "name": "alert_id", "schema": {"format": "uuid", "title": "Alert Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"description": "Not allowed on this plan. `error.code` is one of:\n\n- `plan_limit_reached`: The plan has no room for another one. `limit_key` and `limit` say which limit; only a plan with more room lifts it.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not found. `error.code` is one of:\n\n- `alert_not_found`: No alert has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Partial update: send only the fields to change, and the rest are left alone.\n\nSend `is_active: false` to pause matching entirely, or `email_enabled: false` to keep recording matches while stopping the email digest. Sending `is_active: true`, or new `keywords` for a running alert, re-runs the breadth check a new alert passes, and clears any `paused_reason`.\n\n`keywords` and `recipient_user_ids` each replace the whole list rather than adding to it. An alert holds at most 10 keywords.", "tags": ["alerts"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertUpdateRequest"}}}, "required": true}, "security": [{"APIKeyAuth": []}]}, "delete": {"operationId": "delete_alert", "summary": "Delete an alert", "parameters": [{"in": "path", "name": "alert_id", "schema": {"format": "uuid", "title": "Alert Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"204": {"description": "No Content"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `alert_not_found`: No alert has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Deletes the alert and every match it has recorded.\n\nAlternatively to stop the alert, send `is_active: false` instead.", "tags": ["alerts"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/alerts/{alert_id}/matches": {"get": {"operationId": "list_alert_matches", "summary": "List an alert's matches", "parameters": [{"in": "path", "name": "alert_id", "schema": {"format": "uuid", "title": "Alert Id", "type": "string"}, "required": true}, {"in": "query", "name": "limit", "schema": {"default": 25, "description": "How many results to return per page.", "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false, "description": "How many results to return per page."}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}, "required": false, "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them."}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MatchListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `alert_not_found`: No alert has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Every episode this alert has matched, newest first.\n\nPaged with an opaque `cursor`: pass the `next_cursor` from one response as `cursor` on the next request, and stop when it comes back `null`.\n\n`first_offset_seconds` and `hit_offsets_seconds` are positions in the episode's audio, in seconds, suitable for deep-linking. `excerpt` is the surrounding transcript text, captured when the match was recorded.", "tags": ["alerts"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/webhooks": {"get": {"operationId": "list_webhooks", "summary": "List webhooks", "parameters": [{"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "The webhook of the team this API key acts for, in a list: a team has at most one today.", "tags": ["webhooks"], "security": [{"APIKeyAuth": []}]}, "post": {"operationId": "create_webhook", "summary": "Add a webhook", "parameters": [{"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookCreatedOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"description": "Not allowed on this plan. `error.code` is one of:\n\n- `plan_limit_reached`: The plan has no room for another one. `limit_key` and `limit` say which limit; only a plan with more room lifts it.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Adds the team's webhook. Podzone then POSTs each new match from the team's alerts to `url`, signed -- [the webhooks guide](https://podzone.fm/docs/webhooks) explains the payload and how to verify it.\n\nThe response carries the signing `secret`. It is not returned again: save it now, or rotate it later for a new one.\n\nA team can have one webhook; a second is refused with `403 plan_limit_reached`. A URL that is not https, or whose host does not resolve to a public address, is refused with a `422` naming `url`.", "tags": ["webhooks"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookCreateRequest"}}}, "required": true}, "security": [{"APIKeyAuth": []}]}}, "/api/v1/webhooks/{webhook_id}": {"get": {"operationId": "get_webhook", "summary": "Read a webhook", "parameters": [{"in": "path", "name": "webhook_id", "schema": {"format": "uuid", "title": "Webhook Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `webhook_not_found`: No webhook has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "tags": ["webhooks"], "security": [{"APIKeyAuth": []}]}, "patch": {"operationId": "update_webhook", "summary": "Change a webhook", "parameters": [{"in": "path", "name": "webhook_id", "schema": {"format": "uuid", "title": "Webhook Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"description": "Not allowed on this plan. `error.code` is one of:\n\n- `plan_limit_reached`: The plan has no room for another one. `limit_key` and `limit` say which limit; only a plan with more room lifts it.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "404": {"description": "Not found. `error.code` is one of:\n\n- `webhook_not_found`: No webhook has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Send `url` to change where matches go; deliveries still being retried go to the new URL.\n\nSend `is_active: false` to stop sending -- deliveries waiting to be retried are cancelled, and matches found while it is off are never sent. `is_active: true` switches it back on, including after Podzone switched it off for failing.", "tags": ["webhooks"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookUpdateRequest"}}}, "required": true}, "security": [{"APIKeyAuth": []}]}, "delete": {"operationId": "delete_webhook", "summary": "Delete a webhook", "parameters": [{"in": "path", "name": "webhook_id", "schema": {"format": "uuid", "title": "Webhook Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"204": {"description": "No Content"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `webhook_not_found`: No webhook has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Deletes the webhook and its delivery history.", "tags": ["webhooks"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/webhooks/{webhook_id}/rotate-secret": {"post": {"operationId": "rotate_webhook_secret", "summary": "Replace a webhook's signing secret", "parameters": [{"in": "path", "name": "webhook_id", "schema": {"format": "uuid", "title": "Webhook Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookSecretOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `webhook_not_found`: No webhook has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Returns a new signing secret. The old one stops working immediately: every delivery from now on, retries included, is signed with the new one.", "tags": ["webhooks"], "security": [{"APIKeyAuth": []}]}}, "/api/v1/webhooks/{webhook_id}/test": {"post": {"operationId": "test_webhook", "summary": "Send a test event", "parameters": [{"in": "path", "name": "webhook_id", "schema": {"format": "uuid", "title": "Webhook Id", "type": "string"}, "required": true}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"202": {"description": "Accepted", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookDeliveryOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `webhook_not_found`: No webhook has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "Queues a sample `alert.match.created` delivery with `\"test\": true`, signed with the webhook's secret, to its URL or to `url` instead. It is sent once and never retried; read its result from the deliveries list.\n\nA team may send 50 an hour. Past that, `429 rate_limited` with `scope: \"webhook_test_sends_per_hour\"` and a `Retry-After` header.", "tags": ["webhooks"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookTestRequest"}}}, "required": true}, "security": [{"APIKeyAuth": []}]}}, "/api/v1/webhooks/{webhook_id}/deliveries": {"get": {"operationId": "list_webhook_deliveries", "summary": "List a webhook's deliveries", "parameters": [{"in": "path", "name": "webhook_id", "schema": {"format": "uuid", "title": "Webhook Id", "type": "string"}, "required": true}, {"in": "query", "name": "limit", "schema": {"default": 50, "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false}, {"in": "query", "name": "cursor", "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Cursor"}, "required": false}, {"name": "X-Podzone-Team", "in": "header", "required": false, "schema": {"type": "string", "format": "uuid"}, "description": "The team id of the team to act for. **Required for an account-scoped key** on this route, which reaches every team and so cannot infer one; omitting it returns 422. Ignored by a team-scoped key, which only reaches one team."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookDeliveryListOut"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"description": "Not found. `error.code` is one of:\n\n- `webhook_not_found`: No webhook has this id in this team.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "422": {"description": "Invalid request. `error.code` is one of:\n\n- `invalid_input`: A parameter, header or body field was invalid. `fields` says what was wrong with each.\n- `invalid_cursor`: The cursor could not be read. Start again from the first page.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "429": {"$ref": "#/components/responses/RateLimited"}}, "description": "What was sent and how the webhook answered, newest first, kept for 30 days.\n\nPaged with an opaque `cursor`: pass the `next_cursor` from one response as `cursor` on the next request, and stop when it comes back `null`.", "tags": ["webhooks"], "security": [{"APIKeyAuth": []}]}}}, "components": {"schemas": {"AccountOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"title": "Name", "type": "string"}}, "required": ["id", "name"], "title": "AccountOut", "type": "object"}, "IdentityOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"title": "Name", "type": "string"}, "prefix": {"title": "Prefix", "type": "string"}, "scope": {"title": "Scope", "type": "string"}, "team": {"anyOf": [{"$ref": "#/components/schemas/TeamOut"}, {"type": "null"}]}, "account": {"$ref": "#/components/schemas/AccountOut"}}, "required": ["id", "name", "prefix", "scope", "team", "account"], "title": "IdentityOut", "type": "object"}, "TeamOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"title": "Name", "type": "string"}}, "required": ["id", "name"], "title": "TeamOut", "type": "object"}, "TeamListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/TeamOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results"], "title": "TeamListOut", "type": "object"}, "TeamMemberListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/TeamMemberOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results"], "title": "TeamMemberListOut", "type": "object"}, "TeamMemberOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"}}, "required": ["id", "full_name"], "title": "TeamMemberOut", "type": "object"}, "PodcastSearchQuery": {"properties": {"q": {"description": "What to search for, at least three characters. Matches anywhere in a show's name. Bare words must all appear; `\"quoted phrases\"` match adjacent words, `|` means OR and a leading `-` excludes.", "maxLength": 200, "minLength": 3, "title": "Q", "type": "string"}, "fields": {"description": "Where to look: `title`, `description`, or both (the default). Repeat to give both.", "items": {"enum": ["title", "description"], "type": "string"}, "title": "Fields", "type": "array"}, "order_by": {"default": "best_match", "description": "`best_match`, or `last_episode_at` for the date of the show's newest episode.", "enum": ["best_match", "last_episode_at"], "title": "Order By", "type": "string"}, "order_dir": {"default": "desc", "description": "`desc` or `asc`. Ignored when `order_by` is `best_match`.", "enum": ["asc", "desc"], "title": "Order Dir", "type": "string"}, "limit": {"default": 25, "description": "How many results to return per page.", "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}}, "required": ["q"], "title": "PodcastSearchQuery", "type": "object"}, "PodcastOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "description": {"title": "Description", "type": "string"}, "author": {"title": "Author", "type": "string"}, "image_url": {"title": "Image Url", "type": "string"}, "language": {"title": "Language", "type": "string"}, "explicit": {"title": "Explicit", "type": "boolean"}, "last_episode_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Last Episode At"}, "feed_url": {"title": "Feed Url", "type": "string"}, "itunes_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Itunes Id"}}, "required": ["id", "title", "description", "author", "image_url", "language", "explicit", "last_episode_at", "feed_url", "itunes_id"], "title": "PodcastOut", "type": "object"}, "PodcastSearchOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/PodcastOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results", "next_cursor"], "title": "PodcastSearchOut", "type": "object"}, "CategoryOut": {"properties": {"slug": {"title": "Slug", "type": "string"}, "name": {"title": "Name", "type": "string"}}, "required": ["slug", "name"], "title": "CategoryOut", "type": "object"}, "PodcastDetailOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "description": {"title": "Description", "type": "string"}, "author": {"title": "Author", "type": "string"}, "image_url": {"title": "Image Url", "type": "string"}, "language": {"title": "Language", "type": "string"}, "explicit": {"title": "Explicit", "type": "boolean"}, "last_episode_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Last Episode At"}, "feed_url": {"title": "Feed Url", "type": "string"}, "itunes_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Itunes Id"}, "available_episode_count": {"title": "Available Episode Count", "type": "integer"}, "categories": {"items": {"$ref": "#/components/schemas/CategoryOut"}, "title": "Categories", "type": "array"}}, "required": ["id", "title", "description", "author", "image_url", "language", "explicit", "last_episode_at", "feed_url", "itunes_id", "available_episode_count", "categories"], "title": "PodcastDetailOut", "type": "object"}, "EpisodeListQuery": {"properties": {"limit": {"default": 10, "description": "How many results to return per page.", "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}}, "title": "EpisodeListQuery", "type": "object"}, "EpisodeListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/EpisodeOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results", "next_cursor"], "title": "EpisodeListOut", "type": "object"}, "EpisodeOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "description": {"title": "Description", "type": "string"}, "published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Published At"}, "duration_seconds": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Duration Seconds"}, "episode_number": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Episode Number"}, "season_number": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Season Number"}, "audio_url": {"default": "", "title": "Audio Url", "type": "string"}, "has_transcript": {"title": "Has Transcript", "type": "boolean"}}, "required": ["id", "title", "description", "published_at", "duration_seconds", "episode_number", "season_number", "has_transcript"], "title": "EpisodeOut", "type": "object"}, "EpisodeSearchQuery": {"properties": {"q": {"description": "What to search for, at least three characters. Bare words must all appear; `\"quoted phrases\"` match adjacent words, `|` means OR and a leading `-` excludes.", "maxLength": 200, "minLength": 3, "title": "Q", "type": "string"}, "scope": {"description": "`transcript` searches what was said; `metadata` searches episode titles and descriptions.", "enum": ["transcript", "metadata"], "title": "Scope", "type": "string"}, "podcast_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "description": "Only search this podcast's episodes.", "title": "Podcast Id"}, "since": {"anyOf": [{"format": "date", "type": "string"}, {"type": "null"}], "description": "First day to search, inclusive, as an ISO 8601 date: `YYYY-MM-DD` (UTC). Defaults to 30 days before `until`. An earlier date than 18 months ago is moved forward to 18 months before today.", "examples": ["2026-09-01"], "title": "Since"}, "until": {"anyOf": [{"format": "date", "type": "string"}, {"type": "null"}], "description": "Last day to search, inclusive, as an ISO 8601 date: `YYYY-MM-DD` (UTC). Defaults to today.", "examples": ["2026-09-30"], "title": "Until"}, "order_by": {"default": "best_match", "description": "`best_match`, or `published_at` to see new mentions first.", "enum": ["best_match", "published_at"], "title": "Order By", "type": "string"}, "order_dir": {"default": "desc", "description": "`desc` or `asc`. Ignored when `order_by` is `best_match`.", "enum": ["asc", "desc"], "title": "Order Dir", "type": "string"}, "limit": {"default": 25, "description": "How many results to return per page.", "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}}, "required": ["q", "scope"], "title": "EpisodeSearchQuery", "type": "object"}, "EpisodePodcastOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "image_url": {"title": "Image Url", "type": "string"}}, "required": ["id", "title", "image_url"], "title": "EpisodePodcastOut", "type": "object"}, "EpisodeSearchExcerptOut": {"properties": {"text": {"description": "The passage, as plain text.", "title": "Text", "type": "string"}}, "required": ["text"], "title": "EpisodeSearchExcerptOut", "type": "object"}, "EpisodeSearchHitOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "description": {"title": "Description", "type": "string"}, "published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Published At"}, "duration_seconds": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Duration Seconds"}, "audio_url": {"default": "", "title": "Audio Url", "type": "string"}, "has_transcript": {"title": "Has Transcript", "type": "boolean"}, "podcast": {"$ref": "#/components/schemas/EpisodePodcastOut"}, "excerpts": {"items": {"$ref": "#/components/schemas/EpisodeSearchExcerptOut"}, "title": "Excerpts", "type": "array"}}, "required": ["id", "title", "description", "published_at", "duration_seconds", "has_transcript", "podcast", "excerpts"], "title": "EpisodeSearchHitOut", "type": "object"}, "EpisodeSearchOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/EpisodeSearchHitOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}, "window": {"$ref": "#/components/schemas/EpisodeSearchWindowOut"}}, "required": ["results", "next_cursor", "window"], "title": "EpisodeSearchOut", "type": "object"}, "EpisodeSearchWindowOut": {"properties": {"since": {"format": "date", "title": "Since", "type": "string"}, "until": {"format": "date", "title": "Until", "type": "string"}, "earliest": {"format": "date", "title": "Earliest", "type": "string"}}, "required": ["since", "until", "earliest"], "title": "EpisodeSearchWindowOut", "type": "object"}, "EpisodeDetailOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "description": {"title": "Description", "type": "string"}, "published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Published At"}, "duration_seconds": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Duration Seconds"}, "episode_number": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Episode Number"}, "season_number": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Season Number"}, "audio_url": {"default": "", "title": "Audio Url", "type": "string"}, "has_transcript": {"title": "Has Transcript", "type": "boolean"}, "podcast": {"$ref": "#/components/schemas/EpisodePodcastOut"}, "transcript": {"anyOf": [{"$ref": "#/components/schemas/TranscriptSummaryOut"}, {"type": "null"}]}}, "required": ["id", "title", "description", "published_at", "duration_seconds", "episode_number", "season_number", "has_transcript", "podcast"], "title": "EpisodeDetailOut", "type": "object"}, "TranscriptSummaryOut": {"properties": {"language": {"title": "Language", "type": "string"}, "duration_seconds": {"anyOf": [{"type": "number"}, {"type": "null"}], "title": "Duration Seconds"}, "word_count": {"title": "Word Count", "type": "integer"}}, "required": ["language", "duration_seconds", "word_count"], "title": "TranscriptSummaryOut", "type": "object"}, "TranscriptDownloadQuery": {"properties": {"format": {"default": "json", "enum": ["json", "txt", "srt", "vtt"], "title": "Format", "type": "string"}}, "title": "TranscriptDownloadQuery", "type": "object"}, "TranscriptExcerptQuery": {"properties": {"q": {"description": "Words or phrases to find, matched as written: whole words, ignoring case, with no search syntax. Repeat for up to five, variants included (`podcast`, `podcasts`); a passage is returned where any of them is said.", "items": {"maxLength": 200, "minLength": 3, "type": "string"}, "maxItems": 5, "minItems": 1, "title": "Q", "type": "array"}, "context": {"default": 2, "description": "Sentences to include before and after each match.", "maximum": 5, "minimum": 0, "title": "Context", "type": "integer"}, "limit": {"default": 5, "description": "The most passages to return. The earliest matches come first.", "maximum": 5, "minimum": 1, "title": "Limit", "type": "integer"}}, "required": ["q"], "title": "TranscriptExcerptQuery", "type": "object"}, "TranscriptExcerptOut": {"properties": {"start": {"description": "Seconds from the start of the episode.", "title": "Start", "type": "number"}, "end": {"description": "Seconds from the start of the episode.", "title": "End", "type": "number"}, "text": {"title": "Text", "type": "string"}}, "required": ["start", "end", "text"], "title": "TranscriptExcerptOut", "type": "object"}, "TranscriptExcerptsOut": {"properties": {"total_hits": {"description": "How many times the words are said in the episode, including matches not returned in `excerpts`.", "title": "Total Hits", "type": "integer"}, "excerpts": {"items": {"$ref": "#/components/schemas/TranscriptExcerptOut"}, "title": "Excerpts", "type": "array"}}, "required": ["total_hits", "excerpts"], "title": "TranscriptExcerptsOut", "type": "object"}, "TranscriptPageQuery": {"properties": {"cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it to start from the beginning, which counts as a transcript download. A cursor works only for the API key's account, and only until the monthly allowance resets.", "title": "Cursor"}}, "title": "TranscriptPageQuery", "type": "object"}, "TranscriptPageOut": {"properties": {"language": {"title": "Language", "type": "string"}, "duration_seconds": {"anyOf": [{"type": "number"}, {"type": "null"}], "title": "Duration Seconds"}, "paragraphs": {"items": {"$ref": "#/components/schemas/TranscriptParagraphOut"}, "title": "Paragraphs", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Pass as `cursor` for the next page. Null on the last page.", "title": "Next Cursor"}}, "required": ["language", "duration_seconds", "paragraphs", "next_cursor"], "title": "TranscriptPageOut", "type": "object"}, "TranscriptParagraphOut": {"properties": {"start": {"description": "Seconds from the start of the episode.", "title": "Start", "type": "number"}, "end": {"description": "Seconds from the start of the episode.", "title": "End", "type": "number"}, "text": {"title": "Text", "type": "string"}}, "required": ["start", "end", "text"], "title": "TranscriptParagraphOut", "type": "object"}, "AlertListQuery": {"properties": {"limit": {"default": 50, "description": "How many results to return per page.", "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer"}, "cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}}, "title": "AlertListQuery", "type": "object"}, "AlertAuthorOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "email": {"title": "Email", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"}}, "required": ["id", "email", "full_name"], "title": "AlertAuthorOut", "type": "object"}, "AlertListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/AlertOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results", "next_cursor"], "title": "AlertListOut", "type": "object"}, "AlertOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"title": "Name", "type": "string"}, "keywords": {"items": {"type": "string"}, "title": "Keywords", "type": "array"}, "question": {"title": "Question", "type": "string"}, "is_active": {"title": "Is Active", "type": "boolean"}, "use_classifier": {"title": "Use Classifier", "type": "boolean"}, "email_enabled": {"title": "Email Enabled", "type": "boolean"}, "paused_reason": {"anyOf": [{"enum": ["match_limit", "plan_limit"], "type": "string"}, {"type": "null"}], "title": "Paused Reason"}, "paused_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Paused At"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}, "updated_at": {"format": "date-time", "title": "Updated At", "type": "string"}, "team": {"$ref": "#/components/schemas/AlertTeamOut"}, "created_by": {"anyOf": [{"$ref": "#/components/schemas/AlertAuthorOut"}, {"type": "null"}]}, "recipients": {"items": {"$ref": "#/components/schemas/AlertRecipientOut"}, "title": "Recipients", "type": "array"}}, "required": ["id", "name", "keywords", "question", "is_active", "use_classifier", "email_enabled", "paused_reason", "paused_at", "created_at", "updated_at", "team", "created_by", "recipients"], "title": "AlertOut", "type": "object"}, "AlertRecipientOut": {"properties": {"user_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "User Id"}, "email": {"title": "Email", "type": "string"}, "full_name": {"title": "Full Name", "type": "string"}}, "required": ["user_id", "email", "full_name"], "title": "AlertRecipientOut", "type": "object"}, "AlertTeamOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"title": "Name", "type": "string"}}, "required": ["id", "name"], "title": "AlertTeamOut", "type": "object"}, "AlertCreateRequest": {"properties": {"name": {"description": "What to call the alert.", "maxLength": 200, "minLength": 1, "title": "Name", "type": "string"}, "keywords": {"description": "Words or phrases to watch for, at most 10. A transcript that says any of them, as a whole word and ignoring case, is a match.", "items": {"maxLength": 200, "minLength": 1, "type": "string"}, "title": "Keywords", "type": "array"}, "question": {"anyOf": [{"minLength": 10, "type": "string"}, {"const": "", "type": "string"}, {"type": "null"}], "description": "A yes-or-no question for intelligent matching to ask about each match, such as \"Is our product discussed in detail?\". Required when `use_classifier` is true. Send `\"\"` for none.", "title": "Question"}, "use_classifier": {"default": false, "description": "Keep only the matches for which the answer to `question` is yes. Off by default, because it also drops passing mentions.", "title": "Use Classifier", "type": "boolean"}, "is_active": {"default": true, "description": "Whether the alert looks for matches. False pauses it.", "title": "Is Active", "type": "boolean"}, "email_enabled": {"default": true, "description": "Whether recipients get the hourly email digest. False keeps recording matches without emailing.", "title": "Email Enabled", "type": "boolean"}, "recipient_user_ids": {"anyOf": [{"items": {"format": "uuid", "type": "string"}, "type": "array"}, {"type": "null"}], "description": "Who receives the email digest, as user ids from `GET /v1/teams/{team_id}/members`. Replaces the whole list. Empty or omitted on a new alert means nobody is emailed.", "title": "Recipient User Ids"}}, "required": ["name", "keywords"], "title": "AlertCreateRequest", "type": "object"}, "MatchEpisodeOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Published At"}, "duration_seconds": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Duration Seconds"}, "podcast": {"$ref": "#/components/schemas/MatchPodcastOut"}}, "required": ["id", "title", "published_at", "duration_seconds", "podcast"], "title": "MatchEpisodeOut", "type": "object"}, "MatchOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}, "keywords_matched": {"items": {"type": "string"}, "title": "Keywords Matched", "type": "array"}, "first_offset_seconds": {"anyOf": [{"type": "number"}, {"type": "null"}], "title": "First Offset Seconds"}, "hit_offsets_seconds": {"items": {"type": "number"}, "title": "Hit Offsets Seconds", "type": "array"}, "excerpt": {"title": "Excerpt", "type": "string"}, "alert_id": {"format": "uuid", "title": "Alert Id", "type": "string"}, "alert_name": {"title": "Alert Name", "type": "string"}, "episode": {"$ref": "#/components/schemas/MatchEpisodeOut"}}, "required": ["id", "created_at", "keywords_matched", "first_offset_seconds", "hit_offsets_seconds", "excerpt", "alert_id", "alert_name", "episode"], "title": "MatchOut", "type": "object"}, "MatchPodcastOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "title": {"title": "Title", "type": "string"}, "image_url": {"title": "Image Url", "type": "string"}}, "required": ["id", "title", "image_url"], "title": "MatchPodcastOut", "type": "object"}, "AlertUpdateRequest": {"properties": {"name": {"anyOf": [{"maxLength": 200, "minLength": 1, "type": "string"}, {"type": "null"}], "description": "What to call the alert.", "title": "Name"}, "keywords": {"anyOf": [{"items": {"maxLength": 200, "minLength": 1, "type": "string"}, "type": "array"}, {"type": "null"}], "description": "Words or phrases to watch for, at most 10. A transcript that says any of them, as a whole word and ignoring case, is a match.", "title": "Keywords"}, "question": {"anyOf": [{"minLength": 10, "type": "string"}, {"const": "", "type": "string"}, {"type": "null"}], "description": "A yes-or-no question for intelligent matching to ask about each match, such as \"Is our product discussed in detail?\". Required when `use_classifier` is true. Send `\"\"` for none.", "title": "Question"}, "use_classifier": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Keep only the matches for which the answer to `question` is yes. Off by default, because it also drops passing mentions.", "title": "Use Classifier"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether the alert looks for matches. False pauses it.", "title": "Is Active"}, "email_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether recipients get the hourly email digest. False keeps recording matches without emailing.", "title": "Email Enabled"}, "recipient_user_ids": {"anyOf": [{"items": {"format": "uuid", "type": "string"}, "type": "array"}, {"type": "null"}], "description": "Who receives the email digest, as user ids from `GET /v1/teams/{team_id}/members`. Replaces the whole list. Empty or omitted on a new alert means nobody is emailed.", "title": "Recipient User Ids"}}, "title": "AlertUpdateRequest", "type": "object"}, "MatchListQuery": {"properties": {"limit": {"default": 25, "description": "How many results to return per page.", "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer"}, "cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The `next_cursor` from the previous page. Omit it for the first page. Cursors are opaque: do not construct or parse them.", "title": "Cursor"}}, "title": "MatchListQuery", "type": "object"}, "MatchListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/MatchOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results", "next_cursor"], "title": "MatchListOut", "type": "object"}, "WebhookListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/WebhookOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Always null today: a team has at most one webhook.", "title": "Next Cursor"}}, "required": ["results", "next_cursor"], "title": "WebhookListOut", "type": "object"}, "WebhookOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "url": {"description": "Where this team's alert matches are POSTed.", "title": "Url", "type": "string"}, "is_active": {"description": "Whether matches are being sent to it.", "title": "Is Active", "type": "boolean"}, "disabled_reason": {"anyOf": [{"const": "failing", "type": "string"}, {"type": "null"}], "description": "Why Podzone switched it off, or null if it is on or was switched off by you. `failing`: a delivery failed every attempt for about a day and nothing was accepted since. Sending `is_active: true` switches it back on.", "title": "Disabled Reason"}, "disabled_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Disabled At"}, "last_success_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "When it last answered a delivery with a 2xx. Test sends are not counted.", "title": "Last Success At"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}}, "required": ["id", "url", "is_active", "disabled_reason", "disabled_at", "last_success_at", "created_at"], "title": "WebhookOut", "type": "object"}, "WebhookCreatedOut": {"properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "url": {"description": "Where this team's alert matches are POSTed.", "title": "Url", "type": "string"}, "is_active": {"description": "Whether matches are being sent to it.", "title": "Is Active", "type": "boolean"}, "disabled_reason": {"anyOf": [{"const": "failing", "type": "string"}, {"type": "null"}], "description": "Why Podzone switched it off, or null if it is on or was switched off by you. `failing`: a delivery failed every attempt for about a day and nothing was accepted since. Sending `is_active: true` switches it back on.", "title": "Disabled Reason"}, "disabled_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Disabled At"}, "last_success_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "When it last answered a delivery with a 2xx. Test sends are not counted.", "title": "Last Success At"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}, "secret": {"description": "The signing secret. Returned now, and again only by rotating it.", "title": "Secret", "type": "string"}}, "required": ["id", "url", "is_active", "disabled_reason", "disabled_at", "last_success_at", "created_at", "secret"], "title": "WebhookCreatedOut", "type": "object"}, "WebhookCreateRequest": {"properties": {"url": {"description": "An https URL on a public host.", "maxLength": 2000, "minLength": 1, "title": "Url", "type": "string"}}, "required": ["url"], "title": "WebhookCreateRequest", "type": "object"}, "WebhookUpdateRequest": {"properties": {"url": {"anyOf": [{"maxLength": 2000, "minLength": 1, "type": "string"}, {"type": "null"}], "title": "Url"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Active"}}, "title": "WebhookUpdateRequest", "type": "object"}, "WebhookSecretOut": {"properties": {"secret": {"description": "The new signing secret. The old one stops working now.", "title": "Secret", "type": "string"}}, "required": ["secret"], "title": "WebhookSecretOut", "type": "object"}, "WebhookDeliveryOut": {"properties": {"id": {"description": "The `webhook-id` header the delivery carried.", "format": "uuid", "title": "Id", "type": "string"}, "event_type": {"title": "Event Type", "type": "string"}, "is_test": {"title": "Is Test", "type": "boolean"}, "target_url": {"description": "Where a test send went when it was not the webhook's URL; empty otherwise.", "title": "Target Url", "type": "string"}, "status": {"enum": ["pending", "succeeded", "failed", "cancelled"], "title": "Status", "type": "string"}, "attempts": {"title": "Attempts", "type": "integer"}, "next_attempt_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "When the next attempt is due, while pending.", "title": "Next Attempt At"}, "last_attempt_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Last Attempt At"}, "last_status_code": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "The HTTP status of the last answer, or null if none came back.", "title": "Last Status Code"}, "last_error": {"description": "Why the last attempt failed, e.g. \"HTTP 500\" or \"Timed out.\"", "title": "Last Error", "type": "string"}, "delivered_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "title": "Delivered At"}, "created_at": {"format": "date-time", "title": "Created At", "type": "string"}}, "required": ["id", "event_type", "is_test", "target_url", "status", "attempts", "next_attempt_at", "last_attempt_at", "last_status_code", "last_error", "delivered_at", "created_at"], "title": "WebhookDeliveryOut", "type": "object"}, "WebhookTestRequest": {"properties": {"url": {"anyOf": [{"maxLength": 2000, "minLength": 1, "type": "string"}, {"type": "null"}], "description": "Send the test here instead of to the webhook's own URL.", "title": "Url"}}, "title": "WebhookTestRequest", "type": "object"}, "DeliveryListQuery": {"properties": {"limit": {"default": 50, "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer"}, "cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Cursor"}}, "title": "DeliveryListQuery", "type": "object"}, "WebhookDeliveryListOut": {"properties": {"results": {"items": {"$ref": "#/components/schemas/WebhookDeliveryOut"}, "title": "Results", "type": "array"}, "next_cursor": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next Cursor"}}, "required": ["results", "next_cursor"], "title": "WebhookDeliveryListOut", "type": "object"}, "AlertMatchCreatedEvent": {"properties": {"type": {"const": "alert.match.created", "title": "Type", "type": "string"}, "timestamp": {"description": "When the match was found.", "format": "date-time", "title": "Timestamp", "type": "string"}, "test": {"description": "True for a test event, whose `data` is a fixed example rather than a real match.", "title": "Test", "type": "boolean"}, "data": {"$ref": "#/components/schemas/MatchOut", "description": "Exactly what `GET /v1/alerts/matches/{match_id}` returns for this match."}}, "required": ["type", "timestamp", "test", "data"], "title": "AlertMatchCreatedEvent", "type": "object"}, "ErrorDetail": {"properties": {"code": {"description": "What went wrong, for code to branch on. Listed under Errors above.", "title": "Code", "type": "string"}, "message": {"description": "What went wrong, for people. May be reworded at any time.", "title": "Message", "type": "string"}, "fields": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "description": "On `invalid_input`: what was wrong with each parameter, header or field.", "title": "Fields", "type": "object"}, "scope": {"description": "On `rate_limited`: which limit was reached -- `per_minute`, `per_month`, `transcript_downloads_per_month` or `webhook_test_sends_per_hour`.", "title": "Scope", "type": "string"}, "limit": {"description": "On `rate_limited` and `plan_limit_reached`: the limit that was reached.", "title": "Limit", "type": "integer"}, "remaining": {"description": "On `rate_limited`: what is left of it, which is 0.", "title": "Remaining", "type": "integer"}, "reset": {"description": "On `rate_limited`: when the limit lifts, in Unix seconds.", "title": "Reset", "type": "integer"}, "limit_key": {"description": "On `plan_limit_reached`: which of the plan's limits it is.", "title": "Limit Key", "type": "string"}}, "required": ["code", "message"], "title": "ErrorDetail", "type": "object"}, "ErrorOut": {"properties": {"error": {"$ref": "#/components/schemas/ErrorDetail"}}, "required": ["error"], "title": "ErrorOut", "type": "object"}}, "securitySchemes": {"APIKeyAuth": {"type": "http", "description": "An API key from the Podzone app's API keys page, sent as `Authorization: Bearer pzk_live_...`. [Authentication](https://podzone.fm/docs/authentication) explains team and account keys.", "scheme": "bearer"}}, "responses": {"Unauthorized": {"description": "Not authenticated. `error.code` is one of:\n\n- `invalid_credentials`: The API key is missing, malformed or revoked.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}}, "RateLimited": {"description": "A limit was reached. `error.code` is one of:\n\n- `rate_limited`: A limit was reached. `scope` says which, `reset` when it lifts, and the `Retry-After` header how many seconds to wait.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorOut"}}}, "headers": {"Retry-After": {"description": "How many seconds to wait before retrying.", "schema": {"type": "integer"}}}}}}, "servers": [], "webhooks": {"alert.match.created": {"post": {"operationId": "alert_match_created", "tags": ["webhook events"], "summary": "An alert found a match", "description": "Sent to the team's webhook once for each new match. Verify `webhook-signature` before trusting it: [the webhooks guide](https://podzone.fm/docs/webhooks) shows how.", "parameters": [{"name": "webhook-id", "description": "The delivery's id, the same on every retry.", "in": "header", "required": true, "schema": {"type": "string"}}, {"name": "webhook-timestamp", "description": "When this attempt was sent, in Unix seconds.", "in": "header", "required": true, "schema": {"type": "string"}}, {"name": "webhook-signature", "description": "`v1,` and the base64 HMAC-SHA256 signature.", "in": "header", "required": true, "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertMatchCreatedEvent"}}}}, "responses": {"2XX": {"description": "Accepted. Any 2xx within 5 seconds; anything else is retried."}}, "x-podzone-delivery": {"response_timeout_seconds": 5, "retry_waits_seconds": [5, 25, 125, 625, 3125, 15625, 78125], "test_events_per_hour": 50}}}}, "tags": [{"name": "identity", "description": "The key you are calling with: its account and team, and the teams and members it can reach."}, {"name": "podcasts", "description": "The podcast catalogue: search it by title or description, read a podcast, and list its episodes."}, {"name": "episodes", "description": "Episodes and their transcripts: search what was said, read an episode, find words in its transcript, and read the transcript a page at a time or download it whole."}, {"name": "alerts", "description": "A team's keyword alerts, and the matches they find in new episodes."}, {"name": "webhooks", "description": "A team's webhook: the endpoint each new alert match is POSTed to, its signing secret, and its delivery log."}, {"name": "webhook events", "description": "What Podzone POSTs to a team's webhook. Verify each delivery's signature before trusting it."}]}