{"openapi":"3.1.0","info":{"title":"podkst.ai API","version":"1.0.0","description":"AI-powered multi-speaker podcast generation engine. Convert any text, URL, or topic into a professionally produced podcast with natural-sounding voices.\n\n## Quick Start\n\n### 1. Get an API Key\n\nCreate an API key at [podkst.ai](https://podkst.ai) → Dashboard → Settings → API Keys.\n\n### 2. Generate a Podcast\n\n```bash\ncurl -X POST https://api.podkst.ai/v1/generations \\\n  -H \"x-api-key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": \"https://example.com/article\",\n    \"language\": \"en-US\",\n    \"style\": \"conversational\",\n    \"speakers\": [\n      {\"voice\": \"Nina\", \"tone\": \"analytical\"},\n      {\"voice\": \"Kai\", \"tone\": \"enthusiastic\"}\n    ]\n  }'\n```\n\n### 3. Check Status\n\n```bash\ncurl https://api.podkst.ai/v1/generations/JOB_ID \\\n  -H \"x-api-key: YOUR_API_KEY\"\n```\n\nPoll until `status` is `\"succeeded\"`, then download the audio from `output.audio_url`.\n\n### 4. Use Webhooks (optional)\n\nAdd a `webhook` URL in the generation request to receive real-time notifications instead of polling:\n\n```json\n{\n  \"input\": \"Your content here\",\n  \"webhook\": \"https://yourapp.com/hooks/podkst\",\n  \"webhook_events_filter\": [\"completed\", \"failed\"]\n}\n```\n\n## Authentication\n\nPass your API key via the `x-api-key` header on every request. Supabase JWT tokens are also accepted via `Authorization: Bearer <token>` for first-party frontend use.\n\n## Rate Limits\n\nAPI requests are rate-limited per key. If you hit a 429 response, back off and retry with exponential delay.\n\n## Agent Discovery\n\n- **llms.txt**: `GET /llms.txt` — machine-readable API summary for LLMs\n- **OpenAPI**: `GET /openapi.json` — full spec for code generation\n- **MCP Server**: Available as an MCP tool server for Claude, Cursor, and other MCP clients"},"servers":[{"url":"https://api.podkst.ai","description":"Current"}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key for service-to-service and programmatic access"},"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Supabase JWT for user-facing operations (API key management)"}},"schemas":{"BillingProfile":{"type":"object","properties":{"full_name":{"type":"string"},"tax_id":{"type":"string"},"cellphone":{"type":"string"},"abacatepay_customer_id":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["full_name","tax_id","cellphone","abacatepay_customer_id","created_at","updated_at"]}},"parameters":{}},"paths":{"/v1/generations":{"post":{"security":[{"apiKey":[]}],"tags":["Generations"],"summary":"Create generation","description":"Creates a new podcast generation job. Returns immediately with status `queued`. Poll `urls.get` for status, or pass `Prefer: wait` to wait up to 60 s for the result inline.\n\n**Three modes:**\n**Modes:**\n- `mode: \"full\"` (default) — runs the LLM pipeline + TTS. Use `format` to choose dialogue (2 speakers) or monologue (single voice).\n- `mode: \"script_to_audio\"` — bypasses LLM pipeline, takes a pre-written `script` object and goes straight to TTS.\n\n**Formats (for mode=full):**\n- `format: \"dialogue\"` (default) — two-host conversation. Requires `input` and exactly 2 speakers.\n- `format: \"monologue\"` — single-voice narration. Requires `input` and top-level `voice` field. Do NOT send `speakers`, `style`, or `emotion_annotations`.\n\n**Field renames (90-day backward compat until 2026-07-19):**\n- `coverage` → `tier` (old name still accepted)\n- `mode: \"monologue\"` → `mode: \"full\", format: \"monologue\"` (old form still accepted)\n\n**Example (full dialogue):**\n```bash\ncurl -X POST /v1/generations \\\n  -H \"x-api-key: YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"input\": \"https://example.com/article\", \"tier\": \"regular\", \"language\": \"en-US\"}'\n```\n\n**Example (script_to_audio):**\n```bash\ncurl -X POST /v1/generations \\\n  -H \"x-api-key: YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mode\":\"script_to_audio\",\"script\":{\"title\":\"Ep 1\",\"text\":\"Nina: Welcome!\\nKai: Hello!\"},\"tts_quality\":\"pro\"}'\n```\n\n**Example (monologue):**\n```bash\ncurl -X POST /v1/generations \\\n  -H \"x-api-key: YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"format\":\"monologue\",\"input\":\"The history of jazz\",\"voice\":\"Nina\",\"tier\":\"regular\",\"language\":\"en-US\"}'\n```\n\n**Credit cost (ADR-011):**\nFormula: `ceil(duration_min × tier_rate × tts_mult) + research_fee`.\n- Tier rates: flash=2/min, regular=4/min, deep-dive=6/min. Pro voice = 2x multiplier.\n- When `target_duration` is set, it replaces the tier default duration (flash=5min, regular=10min, deep-dive=20min) for pricing.\n- Research fee: `3 x search_calls` (flash=1, regular=2, deep-dive=3).\n- Script-to-audio: `ceil(duration_min x 2 x tts_mult)`, min 1 credit. Daily ceiling: 500 credits.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["full","script_to_audio","monologue"],"default":"full","description":"Pipeline mode: \"full\" (default) runs the LLM pipeline + TTS. \"script_to_audio\" takes a pre-written script and goes straight to TTS. DEPRECATED value \"monologue\" is accepted — use mode=\"full\" with format=\"monologue\" instead."},"format":{"type":"string","enum":["dialogue","monologue"],"default":"dialogue","description":"Output format: \"dialogue\" (two-host conversation, default) or \"monologue\" (single-voice narration). For monologue, pass a single voice via the top-level `voice` field. Do NOT send `speakers`, `style`, or `emotion_annotations` with format=\"monologue\"."},"input":{"type":"string","maxLength":50000,"default":"","description":"Content to generate podcast from (text, URL, or topic). Required (min 10 chars) for mode=full, ignored for mode=script_to_audio."},"voice":{"type":"string","enum":["Nina","Luna","Mia","Aria","Jade","Zara","Vera","Clara","Nora","Lily","Alma","Eva","Iris","Rosa","Kai","Leo","Rio","Max","Enzo","Nico","Ravi","Omar","Dante","Hugo","Axel","Ivan","Luca","Teo","Marco","Soren"],"description":"Single voice display name for format=\"monologue\". Required when format is \"monologue\", ignored otherwise. Use `speakers[]` for dialogue format instead."},"script":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"Podcast episode title"},"description":{"type":"string","maxLength":1000,"default":"","description":"Short episode description"},"text":{"type":"string","minLength":10,"maxLength":20000,"description":"Full script text. Each line of dialogue starts with a speaker label followed by a colon (e.g. `Luna: Olá!` or `Narrator: ...`). Use any labels you want — the first N unique labels in order of first appearance are auto-detected and bound positionally to `speakers[0..N-1]`, so write `Luna:` lines if you want `speakers[0].voice` to speak them. The response echoes the binding as `input.speakers[i].resolved_label`. Used verbatim for TTS — no LLM processing. Capped at 20,000 chars (~22 min of spoken audio) — credit cost scales per 2,500 chars."}},"required":["title","text"],"description":"Pre-written script for mode=script_to_audio. Required when mode is \"script_to_audio\", ignored for mode \"full\"."},"tier":{"type":"string","enum":["flash","regular","deep-dive"],"description":"Content coverage tier: flash (key headlines), regular (main topics with analysis), deep-dive (comprehensive, all angles). Preferred over deprecated `coverage` field."},"coverage":{"type":"string","enum":["flash","regular","deep-dive"],"default":"regular","description":"DEPRECATED — use `tier` instead. Accepted for backward compatibility until 2026-07-19."},"language":{"type":"string","enum":["pt-BR","en-US"],"default":"pt-BR"},"style":{"type":"string","enum":["conversational","interview","narrative","educational"],"default":"conversational"},"speakers":{"type":"array","items":{"type":"object","properties":{"voice":{"type":"string","enum":["Nina","Luna","Mia","Aria","Jade","Zara","Vera","Clara","Nora","Lily","Alma","Eva","Iris","Rosa","Kai","Leo","Rio","Max","Enzo","Nico","Ravi","Omar","Dante","Hugo","Axel","Ivan","Luca","Teo","Marco","Soren"],"description":"Voice display name (e.g., Nina, Kai, Luna, Leo). Case-insensitive. See GET /v1/voices for full list."},"tone":{"type":"string","description":"Personality/tone (e.g., analytical, enthusiastic)"}},"required":["voice"]},"minItems":1,"maxItems":2,"default":[{"voice":"Nina","tone":"analytical"},{"voice":"Kai","tone":"enthusiastic"}]},"target_duration":{"type":"number","minimum":1,"maximum":30,"description":"Target audio duration in minutes (1–30). When set, overrides the tier-based default word count target. The script generator aims for `target_duration × 150` words. Also used for up-front credit pricing instead of the tier default."},"tts_quality":{"type":"string","enum":["standard","pro"],"default":"pro","description":"TTS tier: standard (Flash) or pro (Pro)"},"research":{"type":"boolean","default":false,"description":"Enable web research before script generation. Research depth follows the tier level. Adds a credit surcharge."},"emotion_annotations":{"type":"boolean","default":true,"description":"Enable emotion/pacing annotations in script"},"webhook":{"type":"string","format":"uri","description":"Webhook URL for completion notification (HTTPS required)"},"webhook_events_filter":{"type":"array","items":{"type":"string","enum":["started","progress","completed","failed"]}}}}}}},"responses":{"200":{"description":"Generation result (sync via Prefer: wait)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","succeeded","failed","canceled"]},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"input":{"type":"object","properties":{"content_preview":{"type":"string"},"tier":{"type":"string"},"coverage":{"type":"string"},"language":{"type":"string"},"style":{"type":"string"},"tts_quality":{"type":"string"},"research":{"type":"boolean"},"mode":{"type":"string","enum":["full","script_to_audio","monologue"]},"format":{"type":"string","enum":["dialogue","monologue"]},"speakers":{"type":"array","items":{"type":"object","properties":{"voice":{"type":"string"},"resolved_label":{"type":["string","null"]}},"required":["voice","resolved_label"]}}},"required":["content_preview","tier","coverage","language","style","tts_quality"]},"output":{"type":["object","null"],"properties":{"audio_url":{"type":["string","null"],"format":"uri"},"audio_format":{"type":["string","null"]},"duration_seconds":{"type":["number","null"]},"file_size_bytes":{"type":["number","null"]},"script":{"type":["object","null"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"text":{"type":"string"},"word_count":{"type":"number"}},"required":["title","description","text","word_count"]}},"required":["audio_url","audio_format","duration_seconds","file_size_bytes","script"]},"estimated_credits":{"type":["number","null"],"description":"Credit cost estimated before generation (based on tier defaults or target_duration). Informational — actual cost may differ."},"estimated_duration_minutes":{"type":["number","null"],"description":"Estimated audio duration in minutes (based on tier defaults or target_duration)."},"progress":{"type":["number","null"],"minimum":0,"maximum":100},"attempt":{"type":["integer","null"],"minimum":1},"max_attempts":{"type":["integer","null"],"minimum":1},"error":{"type":["object","null"],"properties":{"type":{"type":"string"},"message":{"type":"string"}},"required":["type","message"]},"urls":{"type":"object","properties":{"get":{"type":"string","format":"uri"},"cancel":{"type":"string","format":"uri"}},"required":["get","cancel"]}},"required":["id","status","created_at","started_at","completed_at","input","output","estimated_credits","estimated_duration_minutes","progress","attempt","max_attempts","error","urls"]}}}},"202":{"description":"Generation queued","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","succeeded","failed","canceled"]},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"input":{"type":"object","properties":{"content_preview":{"type":"string"},"tier":{"type":"string"},"coverage":{"type":"string"},"language":{"type":"string"},"style":{"type":"string"},"tts_quality":{"type":"string"},"research":{"type":"boolean"},"mode":{"type":"string","enum":["full","script_to_audio","monologue"]},"format":{"type":"string","enum":["dialogue","monologue"]},"speakers":{"type":"array","items":{"type":"object","properties":{"voice":{"type":"string"},"resolved_label":{"type":["string","null"]}},"required":["voice","resolved_label"]}}},"required":["content_preview","tier","coverage","language","style","tts_quality"]},"output":{"type":["object","null"],"properties":{"audio_url":{"type":["string","null"],"format":"uri"},"audio_format":{"type":["string","null"]},"duration_seconds":{"type":["number","null"]},"file_size_bytes":{"type":["number","null"]},"script":{"type":["object","null"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"text":{"type":"string"},"word_count":{"type":"number"}},"required":["title","description","text","word_count"]}},"required":["audio_url","audio_format","duration_seconds","file_size_bytes","script"]},"estimated_credits":{"type":["number","null"],"description":"Credit cost estimated before generation (based on tier defaults or target_duration). Informational — actual cost may differ."},"estimated_duration_minutes":{"type":["number","null"],"description":"Estimated audio duration in minutes (based on tier defaults or target_duration)."},"progress":{"type":["number","null"],"minimum":0,"maximum":100},"attempt":{"type":["integer","null"],"minimum":1},"max_attempts":{"type":["integer","null"],"minimum":1},"error":{"type":["object","null"],"properties":{"type":{"type":"string"},"message":{"type":"string"}},"required":["type","message"]},"urls":{"type":"object","properties":{"get":{"type":"string","format":"uri"},"cancel":{"type":"string","format":"uri"}},"required":["get","cancel"]}},"required":["id","status","created_at","started_at","completed_at","input","output","estimated_credits","estimated_duration_minutes","progress","attempt","max_attempts","error","urls"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}},"403":{"description":"Permission denied","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}}}}},"/v1/generations/{id}":{"get":{"security":[{"apiKey":[]}],"tags":["Generations"],"summary":"Get generation","description":"Returns the current status and output (if complete) of a generation.\n\n**Example:**\n```bash\ncurl /v1/generations/gen_abc123 -H \"x-api-key: YOUR_KEY\"\n```\n\n**Scope rules** — A key with `read:jobs` can read any generation owned by its user. A key with only `generate:podcast` can still read generations **it created itself** (self-read). Keys cannot read generations created by a different key on the same account unless they hold `read:jobs`. JWT-authenticated callers bypass scope checks entirely.\n\nWhen `status` is `\"succeeded\"`, the `output` object contains `audio_url`, `duration_seconds`, and the generated `script`.","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Generation object","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","succeeded","failed","canceled"]},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"input":{"type":"object","properties":{"content_preview":{"type":"string"},"tier":{"type":"string"},"coverage":{"type":"string"},"language":{"type":"string"},"style":{"type":"string"},"tts_quality":{"type":"string"},"research":{"type":"boolean"},"mode":{"type":"string","enum":["full","script_to_audio","monologue"]},"format":{"type":"string","enum":["dialogue","monologue"]},"speakers":{"type":"array","items":{"type":"object","properties":{"voice":{"type":"string"},"resolved_label":{"type":["string","null"]}},"required":["voice","resolved_label"]}}},"required":["content_preview","tier","coverage","language","style","tts_quality"]},"output":{"type":["object","null"],"properties":{"audio_url":{"type":["string","null"],"format":"uri"},"audio_format":{"type":["string","null"]},"duration_seconds":{"type":["number","null"]},"file_size_bytes":{"type":["number","null"]},"script":{"type":["object","null"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"text":{"type":"string"},"word_count":{"type":"number"}},"required":["title","description","text","word_count"]}},"required":["audio_url","audio_format","duration_seconds","file_size_bytes","script"]},"estimated_credits":{"type":["number","null"],"description":"Credit cost estimated before generation (based on tier defaults or target_duration). Informational — actual cost may differ."},"estimated_duration_minutes":{"type":["number","null"],"description":"Estimated audio duration in minutes (based on tier defaults or target_duration)."},"progress":{"type":["number","null"],"minimum":0,"maximum":100},"attempt":{"type":["integer","null"],"minimum":1},"max_attempts":{"type":["integer","null"],"minimum":1},"error":{"type":["object","null"],"properties":{"type":{"type":"string"},"message":{"type":"string"}},"required":["type","message"]},"urls":{"type":"object","properties":{"get":{"type":"string","format":"uri"},"cancel":{"type":"string","format":"uri"}},"required":["get","cancel"]}},"required":["id","status","created_at","started_at","completed_at","input","output","estimated_credits","estimated_duration_minutes","progress","attempt","max_attempts","error","urls"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}},"403":{"description":"Permission denied","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}},"404":{"description":"Generation not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}}}}},"/v1/voices":{"get":{"security":[{"apiKey":[]}],"tags":["Voices"],"summary":"List available voices","description":"Returns all 30 available TTS voices for podcast generation. Use the display name (e.g., \"Nina\", \"Kai\") when creating generations. Optionally filtered by gender.","parameters":[{"schema":{"type":"string","enum":["male","female","neutral"],"description":"Filter voices by gender","example":"female"},"required":false,"name":"gender","in":"query"}],"responses":{"200":{"description":"List of available TTS voices with display names and tone descriptors","content":{"application/json":{"schema":{"type":"object","properties":{"voices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"gender":{"type":"string","enum":["male","female","neutral"]},"tone":{"type":"string"},"language":{"type":"string"},"preview_url":{"type":["string","null"],"format":"uri"},"description":{"type":["string","null"]}},"required":["id","name","gender","tone","language","preview_url","description"]}},"total":{"type":"integer"}},"required":["voices","total"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}}}}},"/v1/credits":{"get":{"security":[{"apiKey":[]}],"tags":["Credits"],"summary":"Get credit balance","description":"Returns the current credit balance, plan allocation, and usage for the authenticated user.","responses":{"200":{"description":"Current credit balance and plan information","content":{"application/json":{"schema":{"type":"object","properties":{"balance":{"type":"number"},"used_this_month":{"type":"number"},"plan":{"type":"string"},"plan_credits":{"type":"number"},"topup_credits":{"type":"number"}},"required":["balance","used_this_month","plan","plan_credits","topup_credits"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["invalid_request_error","authentication_error","insufficient_credits","permission_error","generation_failed","rate_limit_error","payload_too_large","api_error","overloaded_error","not_found","conflict"]},"code":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"retry_after":{"type":"number"}},"required":["type","code","message","request_id"]}},"required":["error"]}}}}}}}},"webhooks":{}}