# הפודקאסט של מיכאל — API > Read-only HTTP + MCP surface for הפודקאסט של מיכאל. No write methods. Auth is optional (anonymous OAuth 2.1 + PKCE S256 available for clients that prefer a bearer token). ## Quickstart Three lines from zero to a real episode: ```bash curl https://podcast-a0k.pages.dev/status # 1. health check curl 'https://podcast-a0k.pages.dev/api/search?q=ai&limit=3' # 2. find an episode curl https://podcast-a0k.pages.dev/1.md # 3. read the transcript (replace 1) ``` ## Authentication **Auth is optional.** Every endpoint is public, read-only, and CORS-open. Two modes: - **Zero-auth (default).** Just call the endpoints — no header, no signup. - **Public OAuth 2.1 + PKCE S256.** For clients that prefer M2M / client_credentials flows or want a bearer token to log per-request quotas, anonymous tokens are issued without consent at `https://podcast-a0k.pages.dev/oauth/token`. Discovery: `https://podcast-a0k.pages.dev/.well-known/oauth-authorization-server` (RFC 8414) and `https://podcast-a0k.pages.dev/.well-known/oauth-protected-resource` (RFC 9728). ### M2M / agent auth walkthrough (optional) ```bash # 1. Generate a PKCE verifier + S256 challenge (any client lib does this) # 2. Anonymous client_credentials grant — no client secret, public client curl -X POST https://podcast-a0k.pages.dev/oauth/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials&client_id=public&scope=read:episodes search:episodes' # Response: { access_token, token_type: "Bearer", expires_in, scope } # 3. Use the token (or skip — anonymous calls also work) curl -H 'Authorization: Bearer ' 'https://podcast-a0k.pages.dev/api/search?q=ai' ``` Available scopes: - `read:episodes` — read episode metadata, audio URLs, transcript URLs. - `read:transcripts` — read full transcript text. - `search:episodes` — call /api/search and /ask. All scopes are granted automatically on anonymous client_credentials. ## SDK install There's no proprietary SDK. Use any HTTP client; for MCP, any MCP-compliant client works: ```bash # JavaScript / TypeScript npm install undici # or use built-in fetch # Python pip install httpx # or requests # MCP (Claude.ai, ChatGPT, Cursor) # Add this URL as a custom MCP connector — no install: # https://podcast-a0k.pages.dev/mcp (transport: streamable-http, auth: none) ``` ## Rate limits - **60 requests/minute per IP** across all API endpoints (`/api/*`, `/mcp`, `/.well-known/mcp`, `/ask`, `/status`). - Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds). - 429 responses carry `Retry-After` (seconds). Self-throttle on those headers. ## Errors Every error is a structured JSON envelope: ```json { "error": { "code": "episode_not_found", "message": "…", "hint": "…", "docs_url": "…" } } ``` Status codes: 400 (bad query/body), 402 (donation requested at /donate), 404 (no such episode), 405 (wrong method), 429 (rate-limited), 500 (server side). ## Endpoints ### Search `GET https://podcast-a0k.pages.dev/api/search?q=&limit=` Ranked full-text search over episode title + description + transcript. Response: `{ query, count, took_ms, results: [{ id, title, date, url, audio, transcript, score, snippet }] }`. ### Ask (NLWeb) `POST https://podcast-a0k.pages.dev/ask` — body: `{ "query": "...", "limit": 10 }` `GET https://podcast-a0k.pages.dev/ask?q=&limit=` — query-string variant Natural-language ask. Returns episodes ranked by transcript relevance, wrapped in NLWeb `_meta` envelope. Set `Accept: text/event-stream` (or `Prefer: streaming=true`) for SSE: events `start`, `result` (one per match), `complete`. ### Async (202 Accepted + polling) Long-running operations can opt into the async pattern. Three interchangeable entry points: **`POST https://podcast-a0k.pages.dev/jobs`** with body `{ kind: "ask"|"search", query, limit }` (conventional path), **`POST https://podcast-a0k.pages.dev/ask?async=1`** (or `?async=1` on `/api/search`), or set **`Prefer: respond-async`** on any of the above. All return **HTTP 202 Accepted** with `Location: /jobs/`, `Retry-After: 1`, and a JSON body `{ job_id, status: "pending", poll_url, retry_after_seconds }`. Poll `GET https://podcast-a0k.pages.dev/jobs/` until `status` flips from `"pending"` to `"completed"`; the final response carries the result under `.result`. ```bash # 1. Kick off the job — 202 Accepted curl -i -X POST 'https://podcast-a0k.pages.dev/ask?async=1' \ -H 'Content-Type: application/json' \ -d '{"query":"ai agents"}' # HTTP/1.1 202 Accepted # Location: https://podcast-a0k.pages.dev/jobs/ # Retry-After: 1 # { "job_id": "", "status": "pending", "poll_url": "https://podcast-a0k.pages.dev/jobs/", "retry_after_seconds": 1 } # 2. Poll the job — 200 OK curl https://podcast-a0k.pages.dev/jobs/ # { "job_id": "", "status": "completed", "result": { "query": "...", "results": [...] } } ``` Job ids are stateless: the id encodes the spec, so any client that holds an id can resume polling later from a different IP / device. ### Status `GET https://podcast-a0k.pages.dev/status` — health snapshot for circuit-breaker logic. Always 200 when reachable. Response includes show name, episode count, latest episode summary. ### MCP server (Streamable HTTP, JSON-RPC 2.0) `POST https://podcast-a0k.pages.dev/mcp` — tool calls `GET https://podcast-a0k.pages.dev/mcp` — manifest summary Methods: `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`. Tools: `search_episodes`, `get_episode`, `get_latest_episode`. Batch / bulk: the request body may be a JSON-RPC 2.0 batch — an array of up to 50 request objects run in one round-trip, answered by an array of responses in order. MCP discovery URLs (all return the same manifest): - https://podcast-a0k.pages.dev/.well-known/mcp - https://podcast-a0k.pages.dev/.well-known/mcp.json - https://podcast-a0k.pages.dev/.well-known/mcp-configuration - https://podcast-a0k.pages.dev/.well-known/mcp/server.json ### OpenAPI `GET https://podcast-a0k.pages.dev/.well-known/openapi.json` — OpenAPI 3.1 spec for the entire read surface. ## Webhooks Real-time event subscriptions. `GET https://podcast-a0k.pages.dev/webhooks` returns the event catalog + payload schema; `POST https://podcast-a0k.pages.dev/webhooks` registers a subscription. - **Events:** `episode.published`, `episode.updated`, `episode.deleted`. - **Register (JSON):** `POST /webhooks` with `{ url, events?, secret? }` → 201 + `Location: /webhooks/`. - **Register (WebSub):** `POST /webhooks` form `hub.mode=subscribe&hub.topic=https://podcast-a0k.pages.dev/rss.xml&hub.callback=` → 202. - **Delivery:** `POST` to your callback, body `{ id, type, created, data: { episode } }`. - **Signature:** `X-Webhook-Signature` = hex HMAC-SHA256 of the raw body keyed by your `secret`. - **Manage:** `GET /webhooks/` (inspect), `DELETE /webhooks/` (unsubscribe). ```bash curl -X POST https://podcast-a0k.pages.dev/webhooks \ -H 'Content-Type: application/json' \ -d '{"url":"https://your-app.example/hook","events":["episode.published"],"secret":"s3cret"}' ``` ## Agent mode Append `?mode=agent` to `/` or to any `/` for an agent briefing — capabilities, endpoint inventory, auth, webhooks, and either the latest episode (homepage) or the specific episode (episode page). It returns a readable **HTML** page by default (with the full briefing embedded as JSON), or pure **JSON** when you send `Accept: application/json`. ## Markdown view - `https://podcast-a0k.pages.dev/index.md` — homepage as markdown - `https://podcast-a0k.pages.dev/.md` — episode page as markdown - Or send `Accept: text/markdown` on any HTML page.