{"name":"Wizzo AI Streaming API","version":"1.0.0","base_url":"https://stream.ai.wizzo.media","auth":{"type":"API key","header":"Authorization: <API_KEY>","note":"Send the raw wizzoai API key in the Authorization header, WITHOUT the \"Bearer\" prefix. \"Bearer <jwt>\" is reserved for short-lived public tokens you sign server-side (see public_jwt_mode). PHP CORE sites get the key via market_service::api_key(\"wizzoai\")."},"endpoints":[{"method":"POST","path":"/api/ai/stream","title":"Streaming chat (Server-Sent Events)","description":"Returns the AI answer token-by-token as an SSE stream. Use this when you want to render the answer live as it is generated.","content_type":"text/event-stream","sse_events":["{\"type\":\"chunk\",\"content\":\"...\"}     - one or more, with the next text fragment","{\"type\":\"action\",\"subtype\":\"tool_calls\",\"calls\":[...]} - emitted once if the model called a tool","{\"type\":\"done\",\"input_tokens\":N,\"output_tokens\":N,\"wizzo_tokens\":N} - terminal success event","{\"type\":\"error\",\"message\":\"...\",\"code\":\"...\"} - terminal error event"]},{"method":"POST","path":"/api/ai","title":"Non-streaming chat (legacy PHP-compat)","description":"Returns the full AI answer as a STRING in `data`, matching the legacy PHP m_ai::call() shape. When settings.json=true the string is cleaned (markdown fences/Hebrew quote escapes removed) and is safe to pass through json_decode() on the client. Use /api/ai/json when you want the response already parsed.","response_example":{"data":"The full answer as a string (raw text, or a JSON string when settings.json=true, or {type:\"tool_calls\",calls:[...]} when the model called a tool)"}},{"method":"POST","path":"/api/ai/json","title":"Non-streaming chat - parsed JSON","description":"Like /api/ai but forces settings.json=true and returns the answer ALREADY PARSED as an object/array in `data`. Use this when you want to skip the json_decode() step on the client.","response_example":{"data":{"example_field":"parsed object/array - depends on the model output"}}},{"method":"POST","path":"/api/ai/image","aliases":["POST /api/ai_image"],"title":"Image generation / editing","description":"Generate an image (or edit one when settings.image_input is provided). Returns an array of public URLs hosted on stream.ai.wizzo.media (valid for 24 hours).","response_example":{"data":["https://stream.ai.wizzo.media/uploads/ai_images/openai_173...png"]}},{"method":"POST","path":"/api/embed","title":"Text embedding vector","description":"Returns an OpenAI embedding vector for the given text (defaults to text-embedding-3-small, 1536 dims).","response_example":{"data":[0.0123,-0.0456,"..."]}},{"method":"GET","path":"/api/models","aliases":["GET /api/get_models","POST /api/get_models"],"title":"List available models","description":"Returns the active model catalog (id, name, provider, features, defaults) priced for YOUR account in wizzo tokens: tokens_input / tokens_output / tokens_cached_input per 1M tokens, tokens_per_call, tokens_per_minute, tokens_per_unit for media models.","response_example":{"data":{"models":[{"id":1,"name":"gpt-5-mini","provider":"openai","tokens_input":75000,"tokens_output":600000,"features_images":false,"default_model":true}],"price_unit":"wizzo tokens ..."}}},{"method":"GET","path":"/api/realtime/session","title":"OpenAI Realtime ephemeral session","description":"Mints a short-lived OpenAI Realtime client secret (GA API) for browser WebRTC voice: client_secret.value, expires_at, model, sdp_url. Rate-limited to 5/minute per user.","query":{"voice":"shimmer (default) | alloy | echo | ..."}},{"method":"POST","path":"/api/realtime/usage","title":"Report Realtime session usage (billing)","description":"Called by the client when a voice session ends. Body: { duration_ms: number }."},{"method":"POST","path":"/api/transcribe","title":"Speech-to-text transcription","description":"Transcribes an audio file. Send multipart/form-data with a `file` field (up to 64MB; OpenAI models cap at 25MB), or JSON with base64 `audio_input` + `mime`. Optional fields: model (default: the flagged transcription default), language (ISO-639-1 hint, e.g. \"he\"), prompt (vocabulary hint), response_format (json | text | srt | vtt | verbose_json | diarized_json), timestamps (\"segment\" | \"word\" | \"both\", whisper-1 only), diarize (\"1\" for speaker labels via a *-diarize model). Billed per audio minute (price_per_minute) or by tokens for token-billed models. API-key auth only. Rate-limited to 20/minute per user.","response_example":{"success":true,"data":{"text":"...","language":"he","duration":12.4,"segments":null,"model":"gpt-transcribe","provider":"openai","wizzo_tokens":5}}},{"method":"GET","path":"/api/health","title":"Health check","description":"No auth. Returns { status: \"ok\", timestamp }.","no_auth":true},{"method":"POST","path":"/mcp","aliases":["GET /mcp (self-description, no auth)"],"title":"MCP endpoint (JSON-RPC 2.0, Streamable HTTP, stateless)","description":"The same account/usage tools as the WizzoCMS admin panel (ai_dashboard, ai_usage_logs, ai_log_detail, ai_models, ai_test_flight), for any MCP client — including the Wizzo Market gateway (mcp.wizzo.market). Auth: \"Authorization: <wizzoai api_key>\" (or \"Bearer <api_key>\"), OR a Wizzo Market MCP token (\"Bearer wzm_…\" / \"Bearer wza_…\"), resolved via the market's mcp_introspect and mapped to this account by market_id. Methods: initialize, tools/list, tools/call, notifications/* (202).","no_api_key":true}],"common_request_body":{"model":"string (optional) - e.g. \"gpt-5\", \"gemini-2.5-pro\". Defaults to provider default.","provider":"string (optional) - \"openai\" | \"gemini\" | \"anthropic\" | \"xai\". Defaults to \"openai\".","prompt":"string (required) - the user message.","prompt_ref":"string (optional) - sysname of a library prompt to use instead of prompt (rendered with vars).","vars":"object (optional) - values for the {$var} placeholders of prompt_ref.","settings":{"type":"string - log/billing tag (e.g. \"article_generation\").","system_msg":"string - system prompt.","history":"array - previous turns: [{role:\"user\"|\"assistant\"|\"tool\", content|name|tool_call_id|...}].","max_tokens":"number - response length limit (default 15000).","temperature":"number | null - sampling temperature.","top_p":"number | null - nucleus sampling (0-1).","presence_penalty":"number | null - penalize new tokens based on presence in text so far (-2 to 2). OpenAI/xAI only.","frequency_penalty":"number | null - penalize new tokens based on frequency in text so far (-2 to 2). OpenAI/xAI only.","json":"bool - if true, the answer is parsed as JSON before being returned.","json_schema":"string|object - optional JSON schema for structured output.","image_input":"string | string[] - URL / data-URI / base64 image(s) for vision or image editing. URLs are downloaded server-side with anti-bot fallbacks.","tools":"array - tool/function definitions (OpenAI-style) for tool-calling.","web_search":"bool - enable provider web-search tool when supported."}},"error_format":{"http_status":"4xx for client errors (prompt_not_set, monthly_limit, INVALID_TOOL_CALL, ...), 5xx for provider/server errors","body":{"error":"short_code","message":"human readable Hebrew message"}},"getting_started":{"title":"Getting started","steps":["Get your api key from the WizzoAI dashboard (or market_service::api_key(\"wizzoai\") in a PHP CORE site).","Send it as-is in the Authorization header on every request: \"Authorization: <API_KEY>\" (no Bearer prefix).","POST /api/ai with { prompt } for a full answer, /api/ai/json for a parsed JSON answer, /api/ai/stream for SSE.","Every success is { data: ... } plus usage fields; every failure is { error, message } with a 4xx/5xx status."],"examples":{"curl":"curl -X POST https://stream.ai.wizzo.media/api/ai \\\n  -H \"Authorization: YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"prompt\":\"כתוב כותרת לכתבה על מזג האוויר\",\"model\":\"gpt-5-mini\",\"settings\":{\"type\":\"headline\"}}'","javascript":"const res = await fetch('https://stream.ai.wizzo.media/api/ai/json', {\n  method: 'POST',\n  headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    prompt_ref: 'headline_v2',          // a prompt from your library\n    vars: { topic: 'מזג האוויר' },      // fills {$topic}\n    settings: { json: true }\n  })\n});\nconst { data, wizzo_tokens } = await res.json();","php":"$ch = curl_init('https://stream.ai.wizzo.media/api/ai');\ncurl_setopt_array($ch, [\n    CURLOPT_POST => true,\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => ['Authorization: YOUR_API_KEY', 'Content-Type: application/json'],\n    CURLOPT_POSTFIELDS => json_encode([\n        'prompt' => 'סכם את הטקסט הבא בשלוש נקודות: ...',\n        'settings' => ['type' => 'summary', 'max_tokens' => 800]\n    ])\n]);\n$res = json_decode(curl_exec($ch), true);\necho $res['data'];","python":"import requests\nr = requests.post('https://stream.ai.wizzo.media/api/ai/json',\n    headers={'Authorization': 'YOUR_API_KEY'},\n    json={'prompt': 'Return {\"ok\":true}', 'settings': {'json': True}})\nprint(r.json()['data'])","streaming":"const res = await fetch('https://stream.ai.wizzo.media/api/ai/stream', {\n  method: 'POST',\n  headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' },\n  body: JSON.stringify({ prompt: 'ספר לי בדיחה' })\n});\nconst reader = res.body.getReader(); const dec = new TextDecoder(); let buf = '';\nwhile (true) {\n  const { done, value } = await reader.read(); if (done) break;\n  buf += dec.decode(value, { stream: true });\n  let i; while ((i = buf.indexOf('\\n\\n')) !== -1) {\n    const line = buf.slice(0, i); buf = buf.slice(i + 2);\n    if (line.startsWith('data:')) { const ev = JSON.parse(line.slice(5)); if (ev.type === 'chunk') process.stdout.write(ev.content); }\n  }\n}"}},"concepts":{"wizzo_tokens":"The billing unit. Every call returns wizzo_tokens: what the call cost your account (input, cached input, output and any web search), counted at the model prices GET /api/models lists for your account. The dashboard shows monthly usage in wizzo tokens.","budget":"Each account has a monthly wizzo-token budget (change_budget). When it is exhausted the API answers 403 { error: \"monthly_limit\" } until the next month or a budget change. Listing/dashboard endpoints keep working. An account marked unlimited by the operator (general.user.unlimited = true) has no budget and no per-minute call limit; its usage is still logged.","types":"settings.type is a free tag stored on every log row (e.g. \"headline\", \"summary\"). Use it to break usage down per feature in the dashboard. prompt_ref sets it to \"prompt:<sysname>\", agents to \"agent:<slug>\".","models":"GET /api/models lists the catalog with features and the price of each model for your account, in wizzo tokens. Omit model to get the provider default. The server falls back to another provider automatically on outages unless settings.allowed_providers fences it.","prompts":"A prompt library with {$var} templating and versions. Call any endpoint with prompt_ref + vars instead of prompt.","agents":"Long-running server-side loops (model + builtin tools) with a step budget, a token budget, persisted steps, optional cron schedule and a finish webhook."},"sections":{"prompts":{"title":"Prompts (library with {$var} templating and versions)","description":"sysname ^[a-z0-9_]{2,64}$. A new version is created only when the content (whitespace-normalised) or the model changes.","endpoints":[{"method":"GET","path":"/api/prompts","title":"List prompts","response_example":{"data":{"prompts":[{"id":1,"sysname":"headline_v2","model":"gpt-5-mini","version":3,"comment":"","updated_at":"2026-09-07 10:00:00","preview":"first 120 chars"}]}}},{"method":"GET","path":"/api/prompts/:sysname","title":"Get a prompt by sysname","response_example":{"data":{"id":1,"sysname":"headline_v2","content":"...","model":"gpt-5-mini","version":3,"comment":"","updated_at":"..."}}},{"method":"POST","path":"/api/prompts","title":"Create","body":{"sysname":"headline_v2","content":"כתוב כותרת על {$topic}","model":"gpt-5-mini","comment":"optional"},"response_example":{"data":{"id":1,"version":1}},"errors":["409 exists","400 invalid_sysname"]},{"method":"PUT","path":"/api/prompts/:id","title":"Update (partial)","body":{"content":"?","model":"?","comment":"?","sysname":"?"},"response_example":{"data":{"id":1,"version":4,"changed":true}}},{"method":"DELETE","path":"/api/prompts/:id","title":"Delete (cascades versions)","response_example":{"data":true}},{"method":"GET","path":"/api/prompts/:id/versions","title":"Version history, newest first","response_example":{"data":{"versions":[{"id":9,"version_number":3,"model":"","comment":"","is_current":true,"created_at":"...","content":"..."}]}}},{"method":"POST","path":"/api/prompts/:id/restore","title":"Restore a version (creates a new version with the old content)","body":{"version_number":2},"response_example":{"data":{"version":4}}},{"method":"POST","path":"/api/prompts/:sysname/render","title":"Render with vars","body":{"vars":{"topic":"חורף"}},"response_example":{"data":{"content":"כתוב כותרת על חורף","model":"gpt-5-mini"}}},{"method":"POST","path":"/api/ai | /api/ai/json | /api/ai/stream","title":"Shortcut: prompt_ref + vars","description":"Send prompt_ref: \"<sysname>\" and vars: {} instead of prompt. The stored model is used when the request has none and settings.type defaults to \"prompt:<sysname>\". 404 prompt_not_found.","body":{"prompt_ref":"headline_v2","vars":{"topic":"חורף"}}}]},"agents":{"title":"Agents (definitions, runs, memory, scheduler)","description":"tools: [\"builtin:fetch_url\", \"builtin:web_search\"] (only builtin:<name> references are accepted; anything else -> 400 invalid_tools). trigger_type schedule needs a 5-field crontime (Asia/Jerusalem); invalid -> 400 bad_cron. limits {max_steps 1..120, max_tokens 1000..2000000}. When a run finishes and webhook_url is set, it receives POST {agent, run_id, status, summary, error, tokens}.","endpoints":[{"method":"GET","path":"/api/agents","title":"List with weekly stats, plus templates, the tool catalog (builtin + the Wizzo services of the site as <service>:*) and global_active","response_example":{"data":{"agents":[{"id":1,"slug":"daily_report","title":"","trigger_type":"schedule","crontime":"0 8 * * *","next_run":"...","last_run":"...","status":"scheduled","runs_week_ok":5,"runs_week_failed":0}]}}},{"method":"GET","path":"/api/agents/:id","title":"Get definition"},{"method":"POST","path":"/api/agents","title":"Create","body":{"title":"דוח יומי","slug":"daily_report","description":"","goal":"אסוף את החדשות של אתמול וסכם","system_prompt":"...","tools":["builtin:fetch_url","builtin:web_search"],"model":"gpt-5","trigger_type":"schedule","crontime":"0 8 * * *","limits":{"max_steps":30,"max_tokens":300000},"webhook_url":"https://example.com/hooks/agent","is_active":1},"response_example":{"data":{"id":1}}},{"method":"PUT","path":"/api/agents/:id","title":"Update (partial)"},{"method":"DELETE","path":"/api/agents/:id","title":"Delete (runs and memory too)"},{"method":"POST","path":"/api/agents/:id/run","title":"Run now","body":{"prompt":"optional extra instruction","dry_run":false},"response_example":{"data":{"run_id":12,"job_id":340,"status":"queued"}}},{"method":"POST","path":"/api/agents/:id/test","title":"Test run (dry_run: tools get __dry_run: true). body.clean = run without the previous run summary"},{"method":"POST","path":"/api/agents/:id/toggle","title":"Switch on/off. A dashboard draft (created with require_test) needs one successful test run first (409 need_test)","body":{"on":true}},{"method":"POST","path":"/api/agents/runs/:run_id/reply","title":"Answer a finished run: opens a continuation run that resumes the conversation (fresh: true = same instruction, clean head)","body":{"message":"הסיכום ארוך מדי, תן רק 3 סיפורים","fresh":false},"response_example":{"data":{"ok":true,"run_id":13}}},{"method":"POST","path":"/api/agents/builder_chat","title":"The recruiter: free text in, draft_updates out (title, goal, trigger, crontime, tools, approval_mode)","body":{"messages":[{"role":"user","content":"סוכן שמסכם לי כל בוקר חדשות טכנולוגיה"}],"draft":{}}},{"method":"POST","path":"/api/agents/pause","title":"Global switch for every agent of the account","body":{"on":false}},{"method":"GET","path":"/api/agents/:id/runs","title":"Runs, newest first (status synced from the runtime)","response_example":{"data":{"runs":[{"id":12,"job_id":340,"trigger_src":"manual","dry_run":0,"status":"done","summary":"...","error":null,"steps_count":4,"tokens_in":0,"tokens_out":0,"wizzo_tokens":0,"created_at":"...","started_at":"...","finished_at":"..."}]}}},{"method":"GET","path":"/api/agents/runs/:run_id","title":"Run detail with the step timeline (?narrate=1 adds a Hebrew one-liner per tool step, billed to the account)","response_example":{"data":{"run":{},"steps":[{"step_num":1,"type":"tool","tool_name":"builtin:fetch_url","tool_args":"{...}","tool_result":"{...}","duration_ms":120}]}}},{"method":"POST","path":"/api/agents/runs/:run_id/cancel","title":"Cancel","response_example":{"data":{"status":"cancelled"}}},{"method":"GET","path":"/api/agents/:id/memory","title":"Memory rows (playbook + insight rows are appended to the system prompt on every run)"},{"method":"POST","path":"/api/agents/:id/memory","body":{"kind":"playbook","mem_key":"style","mem_value":"כתוב קצר"},"title":"Add memory"},{"method":"DELETE","path":"/api/agents/:id/memory/:mid","title":"Delete memory row"}]},"agent_runtime":{"title":"Agent runtime (bring your own control plane)","description":"Lower-level API used by the CMS: you send the whole run definition, including MCP tool servers, and reconcile by polling.","endpoints":[{"method":"POST","path":"/api/agent/run","title":"Enqueue a run","body":{"system_prompt":"...","prompt":"?","model":"?","limits":{"max_steps":30,"max_tokens":300000},"idempotency_key":"?","tool_servers":[{"name":"cms","url":"https://.../mcp","token":"..."}],"allowed_tools":["cms:save_record","builtin:fetch_url"],"report_url":"?","run_secret":"?","dry_run":false,"type":"agent"},"response_example":{"data":{"run_id":340,"status":"queued"}}},{"method":"GET","path":"/api/agent/run/:id","title":"Run status","response_example":{"data":{"run_id":340,"status":"done","steps_count":4,"tokens_in":0,"tokens_out":0,"wizzo_tokens":0,"summary":"...","error":null}}},{"method":"POST","path":"/api/agent/run/:id/cancel","title":"Cancel a run"}]},"media":{"title":"Images, video, speech, avatars, embeddings","endpoints":[{"method":"POST","path":"/api/transcribe","title":"Speech-to-text","description":"multipart/form-data `file` (up to 64MB) or JSON {audio_input (base64/data URL), mime}. Fields: model, language (\"he\"), prompt, response_format (json|text|srt|vtt|verbose_json|diarized_json), timestamps, diarize. API key only. 300 requests/minute per account.","response_example":{"success":true,"data":{"text":"...","language":"he","duration":12.4,"wizzo_tokens":5}}},{"method":"POST","path":"/api/ai/image","title":"Image generation / editing","description":"Returns public URLs under /uploads/ai_images that EXPIRE AFTER 24 HOURS. Copy the file if you need it longer.","response_example":{"data":["https://stream.ai.wizzo.media/uploads/ai_images/....png"]}},{"method":"POST","path":"/api/media/jobs","title":"Video, speech, talking avatar (a job)","description":"Body: {model, prompt (video: the scene; speech: the text; avatar: the script), settings, webhook_url?}. Video (fal.ai: fal-ai/veo3.1, fal-ai/veo3.1/fast, fal-ai/kling-video/v3/pro/...): settings.duration (seconds), aspect_ratio, resolution, generate_audio (default true), image_url for image-to-video. Speech (ElevenLabs: eleven_v3, eleven_v4, eleven_v4_turbo, Hebrew included): settings.voice_id, language_code, output_format; finished inside the call. Avatar (HeyGen: heygen/avatar-iii|iv|v with settings.avatar_id, heygen/photo-avatar-iv with settings.image_url and voice_id). Answers 202 with the job; read it with GET /api/media/jobs/{id} until status is done or failed. Billed per the model unit (second of video, 1,000 characters, minute of avatar); a failed job costs nothing. A prepaid account must hold the estimate (402 otherwise). Files are ours, kept 7 days (expires_at). webhook_url (https) gets one POST {event: \"media.job.finished\", job} when it ends.","response_example":{"data":{"id":12,"kind":"video","model":"fal-ai/veo3.1/fast","status":"running","result_url":null,"unit":"second","units":0,"estimated_units":4,"wizzo_tokens":0}}},{"method":"GET","path":"/api/media/jobs/{id}","title":"A media job","description":"status queued|running|done|failed; when done: result_url (our copy), mime, units billed, wizzo_tokens, expires_at. GET /api/media/jobs?status=&limit= lists the account's recent jobs.","response_example":{"data":{"id":12,"status":"done","result_url":"https://stream.ai.wizzo.media/uploads/ai_media/video_....mp4","unit":"second","units":4,"wizzo_tokens":60000}}},{"method":"POST","path":"/api/media/jobs/{id}/cancel","title":"Cancel a media job","description":"Lets a video still waiting in the provider queue go: it ends failed with error \"cancelled\" and costs nothing (cancelled: true). A render the provider already started cannot be stopped: it ends done and is billed as usual, and the answer is the job as it is with cancelled: false. Avatars and speech are never cancelled.","response_example":{"data":{"id":12,"status":"failed","error":"cancelled","units":0,"cost_usd":0},"cancelled":true}},{"method":"POST","path":"/api/embed","title":"Embedding vector","body":{"text":"...","type":"article_embedding","model":"text-embedding-3-small"},"response_example":{"data":[0.01,-0.02],"input_tokens":12,"wizzo_tokens":1}},{"method":"GET","path":"/api/realtime/session","title":"OpenAI Realtime ephemeral session (voice)","description":"5 sessions/minute per account. Report usage with POST /api/realtime/usage {duration_ms}; a session is billed up to 45 minutes."}]},"account":{"title":"Account and dashboard (api key only)","endpoints":[{"method":"GET","path":"/api/general","title":"Account info + monthly usage"},{"method":"GET","path":"/api/get_dashboard_data","title":"Stats for a date_range (month_offset_0 ...)"},{"method":"GET","path":"/api/get_usage_logs","title":"Paginated prompt history (page, per_page, search, type, model)"},{"method":"POST","path":"/api/get_log_detail","title":"One log row with its settings","body":{"log_id":1}},{"method":"POST","path":"/api/change_budget","title":"Set the monthly budget","body":{"budget":100000}},{"method":"GET","path":"/api/models","title":"Model catalog"}]}},"public_jwt_mode":{"title":"Public JWT mode (browser-facing calls)","description":"Never ship your api key to a browser. Sign a short-lived HS256 token server-side with the shared WIZZO_PUBLIC_JWT_SECRET and send it as \"Authorization: Bearer <token>\". Claims: site_id (your account id), feature (\"public_chat\"), optional origin (must equal the browser Origin header), optional grants {model, provider, settings:{...}, client_keys:[...]}, exp. JWT calls are clamped: max_tokens <= 2000 unless granted, no system_msg, no image input, no dashboard endpoints, no image generation, no transcription. Errors: 401 token_expired / invalid_token, 403 origin_mismatch."},"rate_limits":{"account":"600 requests per minute per account across every endpoint (a raw key, a JWT and a browser token all count against the account), 429 rate_limit with a Retry-After header; a higher ceiling can be set per account","ip":"1200 requests per minute per client address, authenticated or not, 429 rate_limit; nginx caps bursts in front of it","bad_credentials":"10 bad keys or tokens from one address within 10 minutes block that address for 30 minutes, 429 ip_blocked; an address that also authenticated with a valid credential in the last 24 hours is blocked only past 10 times that count","realtime_session":"5 per minute per account (GET /api/realtime/session), 429 rate_limit","realtime_max_session":"45 minutes billed per session (POST /api/realtime/usage caps duration_ms)","transcribe":"300 per minute per account (POST /api/transcribe), 429 rate_limit","monthly_budget":"The monthly wizzo-token budget is checked on every billable call against the spend logged so far this month; once reached every billable call returns 403 monthly_limit (limit_reached from the auth layer) until the next month or a budget change. Not for an unlimited account (general.user.unlimited), which also has no per-minute limit","spend_brake":"An account whose spend in the last hour jumps far above its own average (or above an absolute hourly ceiling) is frozen at once and the operator is alerted; every call then answers 401 until a person releases it","agent_steps":"agents are bounded by limits.max_steps and limits.max_tokens","agent_concurrency":"3 running agent jobs per account, 8 globally; extra jobs wait in the queue","image_urls":"Generated image URLs expire after 24 hours","payload":"JSON bodies up to 50MB; transcription files up to 64MB (OpenAI models 25MB)"},"error_codes":{"auth error":"401: missing/invalid api key, or inactive account","token_expired":"401: JWT expired","invalid_token":"401: JWT malformed or missing claims","forbidden":"403: endpoint needs an api key (JWT not allowed)","origin_mismatch":"403: JWT origin claim differs from the Origin header","limit_reached":"403: monthly budget reached (auth layer)","monthly_limit":"403: monthly budget reached (during a call)","rate_limit":"429: too many requests for this endpoint, account or address (Retry-After says when)","ip_blocked":"429: this address sent too many bad credentials and is blocked for a while","prompt_not_set":"400: prompt missing","prompt_not_found":"404: prompt_ref does not exist","INVALID_TOOL_SCHEMA":"400: settings.tools definition invalid","INVALID_TOOL_CALL":"400: the model produced a tool call that failed validation","provider_not_allowed":"400: the model resolved to a provider outside settings.allowed_providers","not_found":"404: row not found","exists":"409: unique name/slug/key already used","invalid_sysname":"400: sysname/slug must match ^[a-z0-9_]{2,64}$","invalid_tools":"400: agent tools must be builtin:<name> references","invalid_webhook":"400: agent webhook_url must be https and public","bad_cron":"400: crontime is not a valid 5-field cron expression","content_required":"400: content missing","INVALID_RUN_REQUEST":"400: agent run body invalid","transcribe_not_supported":"400: model cannot transcribe","file_too_large":"400: audio exceeds the limit","server_error":"500: unexpected server error"}}